Many personas share a published day, and a page cut through such a day could take a different few of them each time, skipping some between two pages: the cut now takes the whole day at its edge. The test reads the directory page by page until it finds its persona and checks each page's order, since other tests make personas between two pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
134 lines
5.4 KiB
C#
134 lines
5.4 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
namespace PrivaPub.Tests.Http
|
|
{
|
|
// Trends from what PrivaPub holds (hashtags and links by how many authors use them, posts by their interactions,
|
|
// from discoverable authors only) and the directory of accounts that let themselves be discovered
|
|
[Trait("Category", "Integration")]
|
|
public sealed class MastodonTrendsTests : IAsyncLifetime
|
|
{
|
|
PrivaPubHost _host;
|
|
|
|
static CancellationToken Token => TestContext.Current.CancellationToken;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_host = await PrivaPubHost.Shared();
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
|
|
void Fresh() => _host.Get<Trends>().Forget();
|
|
|
|
static string NewTag() => $"tr{Guid.NewGuid():N}"[..12];
|
|
|
|
[Fact]
|
|
public async Task A_hashtag_trends_once_two_authors_use_it_today_and_never_from_one_or_behind_a_warning()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var bob = await _host.Mastodon("bob");
|
|
var carol = await _host.Mastodon("carol");
|
|
var shared = NewTag();
|
|
var lonely = NewTag();
|
|
var warned = NewTag();
|
|
await alice.Status($"#{shared} is good");
|
|
await bob.Status($"#{shared} indeed");
|
|
for (var i = 0; i < 4; i++)
|
|
await carol.Status($"#{lonely} again {i}");
|
|
await alice.Status($"#{warned}", ("spoiler_text", "careful"));
|
|
await bob.Status($"#{warned}", ("spoiler_text", "careful"));
|
|
Fresh();
|
|
|
|
var trends = (await _host.Client().Get("/api/v1/trends/tags?limit=20")).Ok().Array;
|
|
var trend = Assert.Single(trends, t => t.Text("name") == shared)!;
|
|
Assert.Equal(("2", "2"), (trend["history"]![0].Text("uses"), trend["history"]![0].Text("accounts")));
|
|
Assert.DoesNotContain(trends, t => t.Text("name") == lonely);
|
|
Assert.DoesNotContain(trends, t => t.Text("name") == warned);
|
|
Assert.Contains((await alice.Client.Get("/api/v1/trends?limit=20")).Ok().Array, t => t.Text("name") == shared);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_post_trends_by_its_interactions_when_its_author_is_discoverable()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var hidden = await _host.Mastodon("hidden");
|
|
var bob = await _host.Mastodon("bob");
|
|
(await hidden.Client.Patch("/api/v1/accounts/update_credentials", ("discoverable", "false"))).Ok();
|
|
var liked = (await alice.Status("worth a like")).Text("id");
|
|
var quiet = (await hidden.Status("worth a like too")).Text("id");
|
|
var answer = (await alice.Status("a reply", ("in_reply_to_id", liked))).Text("id");
|
|
foreach (var id in new[] { liked, quiet, answer })
|
|
{
|
|
(await bob.Client.Post($"/api/v1/statuses/{id}/favourite")).Ok();
|
|
(await bob.Client.Post($"/api/v1/statuses/{id}/reblog")).Ok();
|
|
}
|
|
Fresh();
|
|
|
|
var trending = (await _host.Client().Get("/api/v1/trends/statuses?limit=40")).Ok().Ids.ToList();
|
|
Assert.Contains(liked, trending);
|
|
Assert.DoesNotContain(quiet, trending);
|
|
Assert.DoesNotContain(answer, trending);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_link_trends_once_two_authors_share_it()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var bob = await _host.Mastodon("bob");
|
|
var url = $"https://news.example/{Guid.NewGuid():N}";
|
|
foreach (var account in new[] { alice, bob })
|
|
{
|
|
var id = (await account.Status($"read {url}")).Text("id");
|
|
await DB.Default.Update<Post>().MatchID(id).Modify(p => p.Link, new PostLink { Href = url, Title = "The news" }).ExecuteAsync(Token);
|
|
}
|
|
Fresh();
|
|
|
|
var link = Assert.Single((await _host.Client().Get("/api/v1/trends/links?limit=20")).Ok().Array, l => l.Text("url") == url)!;
|
|
Assert.Equal("The news", link.Text("title"));
|
|
Assert.Equal("2", link["history"]![0].Text("accounts"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_directory_lists_discoverable_accounts_by_activity_or_by_joining_and_never_by_creation()
|
|
{
|
|
var alice = await _host.Mastodon("alice");
|
|
var hidden = await _host.Mastodon("hidden");
|
|
(await hidden.Client.Patch("/api/v1/accounts/update_credentials", ("discoverable", "false"))).Ok();
|
|
await hidden.Status("not listed");
|
|
await alice.Status("listed");
|
|
|
|
// by their latest public post, newest first: other tests post too, so the directory is read until alice
|
|
var active = new List<string>();
|
|
for (var offset = 0; offset < 4000 && !active.Contains(alice.Id); offset += 80)
|
|
{
|
|
var page = (await _host.Client().Get($"/api/v1/directory?order=active&local=true&limit=80&offset={offset}")).Ok().Ids.ToList();
|
|
active.AddRange(page);
|
|
if (page.Count < 80)
|
|
break;
|
|
}
|
|
Assert.Contains(alice.Id, active);
|
|
Assert.DoesNotContain(hidden.Id, active);
|
|
// by the day each says it joined, newest first: a persona's is its published day, set some days back
|
|
// (each page in order: other tests make personas between two pages)
|
|
var joined = new List<System.Text.Json.Nodes.JsonNode>();
|
|
for (var offset = 0; offset < 4000; offset += 80)
|
|
{
|
|
var page = (await _host.Client().Get($"/api/v1/directory?order=new&local=true&limit=80&offset={offset}")).Ok().Array;
|
|
var days = page.Select(a => a.Text("created_at")[..10]).ToList();
|
|
Assert.Equal(days.OrderByDescending(d => d, StringComparer.Ordinal), days);
|
|
joined.AddRange(page!);
|
|
if (page.Count < 80 || joined.Any(a => a.Text("id") == alice.Id))
|
|
break;
|
|
}
|
|
Assert.Contains(joined, a => a.Text("id") == alice.Id);
|
|
Assert.DoesNotContain(joined, a => a.Text("id") == hidden.Id);
|
|
}
|
|
}
|
|
}
|