using MongoDB.Entities; using PrivaPub.Models.Social; using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; using System.Net; namespace PrivaPub.Tests.Http { // Mastodon's notification policy and requests (4.3): accepted by default; a policy filters what strangers, new accounts // or unasked private mentions send into a request per account, which the persona lets in (and that account from then // on) or dismisses; or drops it [Trait("Category", "Integration")] public sealed class NotificationPolicyTests : IAsyncLifetime { PrivaPubHost _host; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; static async Task> Notifiers(Mastodon persona, string query = "") => (await persona.Client.Get("/api/v1/notifications" + query)).Ok().Array.Select(n => n!["account"].Text("id")); [Fact] public async Task Everything_is_accepted_until_the_persona_chooses() { var alice = await _host.Mastodon("alice"); var stranger = await _host.Mastodon("stranger"); var post = (await alice.Status("like me")).Text("id"); var policy = (await alice.Client.Get("/api/v2/notifications/policy")).Ok().Body; (await stranger.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok(); Assert.Equal(["accept"], new[] { "for_not_following", "for_not_followers", "for_new_accounts", "for_private_mentions", "for_limited_accounts" } .Select(key => policy.Text(key)).Distinct()); Assert.Contains(stranger.Id, await Notifiers(alice)); Assert.Empty((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array); } [Fact] public async Task A_strangers_notifications_wait_in_a_request_which_lets_them_and_the_stranger_in() { var alice = await _host.Mastodon("alice"); var friend = await _host.Mastodon("friend"); var stranger = await _host.Mastodon("stranger"); (await alice.Client.Post($"/api/v1/accounts/{friend.Id}/follow")).Ok(); var post = (await alice.Status("like me")).Text("id"); var policy = (await alice.Client.Patch("/api/v2/notifications/policy", ("for_not_following", "filter"))).Ok().Body; Assert.Equal("filter", policy.Text("for_not_following")); (await friend.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok(); (await stranger.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok(); Assert.Equal([friend.Id], await Notifiers(alice)); Assert.Equal(1, (await alice.Client.Get("/api/v1/notifications/unread_count")).Ok().Body["count"]!.GetValue()); Assert.DoesNotContain(stranger.Id, (await alice.Client.Get("/api/v2/notifications")).Ok().Body["accounts"]!.AsArray().Select(a => a.Text("id"))); var request = Assert.Single((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array)!; Assert.Equal((stranger.Id, "1", post), (request["account"].Text("id"), request.Text("notifications_count"), request["last_status"].Text("id"))); var summary = (await alice.Client.Get("/api/v2/notifications/policy")).Ok().Body["summary"]!; Assert.Equal((1, 1), (summary["pending_requests_count"]!.GetValue(), summary["pending_notifications_count"]!.GetValue())); Assert.Equal([stranger.Id], await Notifiers(alice, $"?account_id={stranger.Id}&include_filtered=true")); (await alice.Client.Post($"/api/v1/notifications/requests/{request.Text("id")}/accept")).Ok(); Assert.Contains(stranger.Id, await Notifiers(alice)); Assert.Empty((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array); var again = (await alice.Status("like me again")).Text("id"); (await stranger.Client.Post($"/api/v1/statuses/{again}/favourite")).Ok(); Assert.Equal(2, (await Notifiers(alice, "?types[]=favourite")).Count(id => id == stranger.Id)); } [Fact] public async Task A_dismissed_request_takes_its_notifications_with_it_and_a_dropped_one_is_never_kept() { var alice = await _host.Mastodon("alice"); var stranger = await _host.Mastodon("stranger"); var newcomer = await _host.Mastodon("newcomer"); var post = (await alice.Status("like me")).Text("id"); // (v1 speaks of filtering only) Assert.True((await alice.Client.Put("/api/v1/notifications/policy", ("filter_not_following", "true"))).Ok().Body.Flag("filter_not_following")); (await stranger.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok(); var request = Assert.Single((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array)!; (await alice.Client.Post($"/api/v1/notifications/requests/{request.Text("id")}/dismiss")).Ok(); Assert.Empty((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array); Assert.Empty(await Notifiers(alice, $"?account_id={stranger.Id}&include_filtered=true")); Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Get($"/api/v1/notifications/requests/{request.Text("id")}")).Status); (await alice.Client.Patch("/api/v2/notifications/policy", ("for_new_accounts", "drop"))).Ok(); (await newcomer.Client.Post($"/api/v1/statuses/{post}/favourite")).Ok(); Assert.Empty(await Notifiers(alice, $"?account_id={newcomer.Id}&include_filtered=true")); Assert.Empty((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array); } [Fact] public async Task An_unasked_private_mention_is_filtered_and_one_answering_the_persona_is_not() { var alice = await _host.Mastodon("alice"); var stranger = await _host.Mastodon("stranger"); (await alice.Client.Patch("/api/v2/notifications/policy", ("for_private_mentions", "filter"))).Ok(); var asked = (await alice.Status($"@{stranger.UserName} write to me", ("visibility", "direct"))).Text("id"); await stranger.Status($"@{alice.UserName} hello out of nowhere", ("visibility", "direct")); await stranger.Status($"@{alice.UserName} as you asked", ("visibility", "direct"), ("in_reply_to_id", asked)); var mentions = (await alice.Client.Get("/api/v1/notifications?types[]=mention")).Ok().Array.Select(n => n!["status"].Text("content")).ToList(); Assert.Contains(mentions, c => c.Contains("as you asked")); Assert.DoesNotContain(mentions, c => c.Contains("out of nowhere")); Assert.Single((await alice.Client.Get("/api/v1/notifications/requests")).Ok().Array); } } }