2026-10-05 03:14:56 +02:00
|
|
|
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");
|
|
|
|
|
|
2026-10-05 03:35:10 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
2026-10-05 03:14:56 +02:00
|
|
|
Assert.Contains(alice.Id, active);
|
|
|
|
|
Assert.DoesNotContain(hidden.Id, active);
|
2026-10-05 03:35:10 +02:00
|
|
|
// 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);
|
2026-10-05 03:14:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|