236 lines
11 KiB
C#
236 lines
11 KiB
C#
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"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|