Files
SocialPub/PrivaPub.Tests/Http/MastodonTrendsTests.cs
T
thepraandClaude Opus 5.5 83d4148de1 Trends and the directory, from what PrivaPub holds
trends/tags, trends/statuses and trends/links replace the stubs. Hashtags
and links rise with the number of different authors using them today
against the week before, two at least; posts with their favourites, boosts
and replies, halving every twelve hours. Only public posts PrivaPub already
holds count, nothing behind a content warning, and a post trends only if
its author is discoverable. Computed at most every ten minutes.

The directory lists discoverable accounts by their latest public post, or
by the day they say they joined: a persona's published day, never its
creation, with ties broken by a hash of the name, so personas made together
are not told apart by their order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 03:14:56 +02:00

115 lines
4.5 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");
var active = (await _host.Client().Get("/api/v1/directory?order=active&local=true&limit=80")).Ok().Ids.ToList();
Assert.Contains(alice.Id, active);
Assert.DoesNotContain(hidden.Id, active);
var joined = (await _host.Client().Get("/api/v1/directory?order=new&local=true&limit=80")).Ok().Ids.ToList();
Assert.Contains(alice.Id, joined);
Assert.DoesNotContain(hidden.Id, joined);
}
}
}