Files

236 lines
11 KiB
C#
Raw Permalink Normal View History

2026-10-03 11:56:39 +02:00
using PrivaPub.Tests.Support;
using PrivaPub.Tests.Support.Host;
using System.Net;
using System.Text.Json.Nodes;
namespace PrivaPub.Tests.Http
{
[Trait("Category", "Integration")]
public sealed class MastodonTimelinesTests : IAsyncLifetime
{
PrivaPubHost _host;
Peer _peer;
public async ValueTask InitializeAsync()
{
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
_host = await PrivaPubHost.Shared();
_peer = await Peer.Start();
}
public async ValueTask DisposeAsync()
{
if (_peer != default)
await _peer.DisposeAsync();
}
[Fact]
public async Task The_home_timeline_pages_with_max_since_and_min_id()
{
var alice = await _host.Mastodon("alice");
var bob = await _host.Mastodon("bob");
var posts = new List<string>();
for (var i = 1; i <= 4; i++)
posts.Add((await alice.Status($"post {i}")).Text("id"));
(await bob.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
var afterFollow = (await alice.Status("post 5")).Text("id");
var (p1, p2, p3, p4) = (posts[0], posts[1], posts[2], posts[3]);
var first = (await alice.Client.Get("/api/v1/timelines/home?limit=2")).Ok();
Assert.Equal(new[] { afterFollow, p4 }, first.Ids);
Assert.Equal(new[] { p3, p2 }, (await alice.Client.Get(first.Link("next"))).Ok().Ids);
Assert.Empty((await alice.Client.Get(first.Link("prev"))).Ok().Array);
Assert.Equal(new[] { p2, p1 }, (await alice.Client.Get($"/api/v1/timelines/home?max_id={p3}&limit=2")).Ok().Ids);
Assert.Equal(new[] { afterFollow, p4 }, (await alice.Client.Get($"/api/v1/timelines/home?since_id={p1}&limit=2")).Ok().Ids);
Assert.Equal(new[] { p3, p2 }, (await alice.Client.Get($"/api/v1/timelines/home?min_id={p1}&limit=2")).Ok().Ids);
Assert.Equal(new[] { p3 }, (await alice.Client.Get($"/api/v1/timelines/home?min_id={p2}&max_id={p4}")).Ok().Ids);
Assert.Equal(new[] { afterFollow }, (await bob.Client.Get("/api/v1/timelines/home")).Ok().Ids);
Assert.Equal(HttpStatusCode.Unauthorized, (await _host.Client().Get("/api/v1/timelines/home")).Status);
}
static async Task<List<string>> Window(HttpClient client, string filter, string since, string until)
{
var ids = new List<string>();
for (var page = 0; page < 50; page++)
{
var batch = (await client.Get($"/api/v1/timelines/public?{filter}&since_id={since}&max_id={until}&limit=40")).Ok().Ids.ToList();
if (batch.Count == 0)
break;
ids.AddRange(batch);
until = batch[^1];
}
return ids;
}
[Fact]
public async Task The_public_timeline_splits_local_from_remote_and_leaves_out_unlisted_posts()
{
var alice = await _host.Mastodon("alice");
var bob = new RemoteActor(_peer, "bob");
var since = MastodonHelpers.IdBefore(DateTime.UtcNow);
var remote = await _host.PublicPostFrom(bob, alice);
var open = (await alice.Status("hello world")).Text("id");
var unlisted = (await alice.Status("hello quietly", ("visibility", "unlisted"))).Text("id");
var until = MastodonHelpers.IdBefore(DateTime.UtcNow.AddSeconds(1));
var anonymous = _host.Client();
var everything = await Window(anonymous, "local=false", since, until);
Assert.Contains(open, everything);
Assert.Contains(remote.ID, everything);
Assert.DoesNotContain(unlisted, everything);
var local = await Window(anonymous, "local=true", since, until);
Assert.Contains(open, local);
Assert.DoesNotContain(remote.ID, local);
var federated = await Window(alice.Client, "remote=true", since, until);
Assert.Contains(remote.ID, federated);
Assert.DoesNotContain(open, federated);
}
[Fact]
public async Task The_tag_timeline_is_open_to_anonymous_readers_and_shows_only_public_posts()
{
var alice = await _host.Mastodon("alice");
var tag = $"t{Guid.NewGuid():N}"[..16];
var open = (await alice.Status($"about #{tag}")).Text("id");
await alice.Status($"quietly about #{tag}", ("visibility", "unlisted"));
await alice.Status($"privately about #{tag}", ("visibility", "private"));
var later = (await alice.Status($"more about #{tag.ToUpperInvariant()}")).Text("id");
var tagged = (await _host.Client().Get($"/api/v1/timelines/tag/{tag}")).Ok();
Assert.Equal(new[] { later, open }, tagged.Ids);
Assert.Equal(new[] { later }, (await _host.Client().Get($"/api/v1/timelines/tag/{tag}?local=true&limit=1")).Ok().Ids);
Assert.Equal(new[] { later, open }, (await alice.Client.Get($"/api/v1/timelines/tag/%23{tag}")).Ok().Ids);
}
[Fact]
public async Task A_list_timeline_is_an_empty_stub()
{
var alice = await _host.Mastodon("alice");
Assert.Empty((await alice.Client.Get("/api/v1/timelines/list/anything")).Ok().Array);
}
[Fact]
public async Task Favourites_list_what_the_persona_liked_newest_first()
{
var alice = await _host.Mastodon("alice");
var carol = await _host.Mastodon("carol");
var first = (await carol.Status("one")).Text("id");
var second = (await carol.Status("two")).Text("id");
(await alice.Client.Post($"/api/v1/statuses/{first}/favourite")).Ok();
(await alice.Client.Post($"/api/v1/statuses/{second}/favourite")).Ok();
Assert.Equal(new[] { second, first }, (await alice.Client.Get("/api/v1/favourites")).Ok().Ids);
var page = (await alice.Client.Get("/api/v1/favourites?limit=1")).Ok();
Assert.Equal(new[] { second }, page.Ids);
Assert.Equal(new[] { first }, (await alice.Client.Get(page.Link("next"))).Ok().Ids);
Assert.Empty((await carol.Client.Get("/api/v1/favourites")).Ok().Array);
}
[Fact]
public async Task Conversations_list_direct_messages_to_their_participants_only()
{
var alice = await _host.Mastodon("alice");
var bob = await _host.Mastodon("bob");
var stranger = await _host.Mastodon("stranger");
(await alice.Status($"@{bob.UserName} psst", ("visibility", "direct"))).Text("id");
var answer = (await bob.Status($"@{alice.UserName} what?", ("visibility", "direct"))).Text("id");
var mine = Assert.Single((await alice.Client.Get("/api/v1/conversations")).Ok().Array);
Assert.Equal(new[] { bob.Id }, mine!["accounts"]!.AsArray().Select(a => a.Text("id")));
Assert.Equal(answer, mine["last_status"].Text("id"));
var theirs = Assert.Single((await bob.Client.Get("/api/v1/conversations")).Ok().Array);
Assert.Equal(mine.Text("id"), theirs.Text("id"));
Assert.Equal(new[] { alice.Id }, theirs!["accounts"]!.AsArray().Select(a => a.Text("id")));
Assert.Empty((await stranger.Client.Get("/api/v1/conversations")).Ok().Array);
var read = (await bob.Client.Post($"/api/v1/conversations/{mine.Text("id")}/read")).Ok();
Assert.Equal(mine.Text("id"), read.Body.Text("id"));
Assert.Equal(answer, read.Body["last_status"].Text("id"));
Assert.Equal(HttpStatusCode.NotFound, (await stranger.Client.Post($"/api/v1/conversations/{mine.Text("id")}/read")).Status);
Assert.DoesNotContain(answer, (await stranger.Client.Get($"/api/v1/accounts/{bob.Id}/statuses")).Ok().Ids);
}
[Fact]
public async Task Markers_are_saved_versioned_and_read_back_per_timeline()
{
var alice = await _host.Mastodon("alice");
var saved = (await alice.Client.Post("/api/v1/markers", ("home[last_read_id]", "100"))).Ok();
Assert.Equal("100", saved.Body["home"].Text("last_read_id"));
Assert.Equal(1, saved.Body["home"].Number("version"));
Assert.Null(saved.Body["notifications"]);
var again = (await alice.Client.Json(HttpMethod.Post, "/api/v1/markers", new JsonObject
{
["home"] = new JsonObject { ["last_read_id"] = "200" },
["notifications"] = new JsonObject { ["last_read_id"] = "7" }
})).Ok();
Assert.Equal(("200", 2), (again.Body["home"].Text("last_read_id"), again.Body["home"].Number("version")));
Assert.Equal(("7", 1), (again.Body["notifications"].Text("last_read_id"), again.Body["notifications"].Number("version")));
var home = (await alice.Client.Get("/api/v1/markers?timeline[]=home")).Ok().Object;
Assert.Equal(new[] { "home" }, home.Select(p => p.Key));
Assert.Equal("200", home["home"].Text("last_read_id"));
Assert.Equal(new[] { "home", "notifications" }, (await alice.Client.Get("/api/v1/markers")).Ok().Object.Select(p => p.Key).Order());
Assert.Empty((await (await _host.Mastodon("other")).Client.Get("/api/v1/markers")).Ok().Object);
}
[Fact]
public async Task A_notification_about_a_deleted_post_is_gone_not_null()
{
var alice = await _host.Mastodon("alice");
var carol = await _host.Mastodon("carol");
var post = (await alice.Status("short lived")).Text("id");
(await carol.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok();
var id = Assert.Single((await alice.Client.Get("/api/v1/notifications")).Ok().Array).Text("id");
(await alice.Client.Delete($"/api/v1/statuses/{post}")).Ok();
Assert.Empty((await alice.Client.Get("/api/v1/notifications")).Ok().Array);
var gone = await alice.Client.Get($"/api/v1/notifications/{id}");
Assert.Equal(HttpStatusCode.NotFound, gone.Status);
Assert.Equal("Record not found", gone.Body.Text("error"));
}
[Fact]
public async Task Notifications_filter_page_dismiss_and_clear()
{
var alice = await _host.Mastodon("alice");
var carol = await _host.Mastodon("carol");
var post = (await alice.Status("notice me")).Text("id");
(await carol.Client.Post($"/api/v1/accounts/{alice.Id}/follow")).Ok();
(await carol.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok();
var mention = (await carol.Status($"hey @{alice.UserName}")).Text("id");
(await carol.Client.Post($"/api/v1/statuses/{post}/reblog")).Ok();
var all = (await alice.Client.Get("/api/v1/notifications")).Ok();
Assert.Equal(new[] { "reblog", "mention", "favourite", "follow" }, all.Array.Select(n => n.Text("type")));
Assert.All(all.Array, n => Assert.Equal(carol.Id, n!["account"].Text("id")));
Assert.Equal(mention, all.Array[1]!["status"].Text("id"));
Assert.Equal(new[] { "mention" }, (await alice.Client.Get("/api/v1/notifications?types[]=mention")).Ok().Array.Select(n => n.Text("type")));
Assert.Equal(new[] { "mention", "favourite" },
(await alice.Client.Get("/api/v1/notifications?exclude_types[]=follow&exclude_types[]=reblog")).Ok().Array.Select(n => n.Text("type")));
Assert.Equal(4, (await alice.Client.Get($"/api/v1/notifications?account_id={carol.Id}")).Ok().Array.Count);
var page = (await alice.Client.Get("/api/v1/notifications?limit=3")).Ok();
Assert.Equal(new[] { "follow" }, (await alice.Client.Get(page.Link("next"))).Ok().Array.Select(n => n.Text("type")));
Assert.Equal(4, (await alice.Client.Get("/api/v1/notifications/unread_count")).Ok().Body.Number("count"));
var followId = all.Array[3].Text("id");
Assert.Equal("follow", (await alice.Client.Get($"/api/v1/notifications/{followId}")).Ok().Body.Text("type"));
Assert.Equal(HttpStatusCode.NotFound, (await carol.Client.Get($"/api/v1/notifications/{followId}")).Status);
(await carol.Client.Post($"/api/v1/notifications/{followId}/dismiss")).Ok();
Assert.Equal(HttpStatusCode.OK, (await alice.Client.Get($"/api/v1/notifications/{followId}")).Status);
(await alice.Client.Post($"/api/v1/notifications/{followId}/dismiss")).Ok();
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Get($"/api/v1/notifications/{followId}")).Status);
Assert.Equal(3, (await alice.Client.Get("/api/v1/notifications")).Ok().Array.Count);
(await alice.Client.Post("/api/v1/notifications/clear")).Ok();
Assert.Empty((await alice.Client.Get("/api/v1/notifications")).Ok().Array);
Assert.Equal(0, (await alice.Client.Get("/api/v1/notifications/unread_count")).Ok().Body.Number("count"));
}
}
}