using MongoDB.Entities; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Federation { // pins across servers: an account elsewhere features its own posts (Add and Remove on its `featured` collection, and // the collection itself), a community features posts made in it, and a persona's pin is told to its followers [Trait("Category", "Integration")] public sealed class FeaturedTests : IAsyncLifetime { Harness _harness; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _harness = await Harness.Start(); } public async ValueTask DisposeAsync() { if (_harness != default) await _harness.DisposeAsync(); } static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority); // a public note of the actor's, served where its id points string Note(RemoteActor author, string text, string audience = default) { var path = $"/notes/{Guid.NewGuid():N}"; var note = new JsonObject { ["id"] = Origin(author) + path, ["type"] = "Note", ["attributedTo"] = author.Id, ["content"] = $"

{text}

", ["to"] = new JsonArray("https://www.w3.org/ns/activitystreams#Public"), ["published"] = DateTime.UtcNow.ToString("O") }; if (audience != default) note["audience"] = audience; _harness.Peer.Serve(path, note.ToJsonString()); return Origin(author) + path; } JsonObject Featuring(RemoteActor actor, string type, string objectUri, string target = default) => new() { ["id"] = $"{actor.Id}#{type}-{Guid.NewGuid():N}", ["type"] = type, ["actor"] = actor.Id, ["object"] = objectUri, ["target"] = target ?? actor.Id + "/featured" }; Task> FeaturedBy(RemoteActor actor) => DB.Default.Find().Match(f => f.ActorURI == actor.Id).Sort(f => f.Position, Order.Ascending).ExecuteAsync(TestContext.Current.CancellationToken); [Fact] public async Task An_account_features_its_own_post_and_takes_it_back_but_nothing_else() { var bob = new RemoteActor(_harness.Peer, "bob"); var carol = new RemoteActor(_harness.Peer, "carol", _harness.Peer.B); var mine = Note(bob, "pinned"); var theirs = Note(carol, "not bob's"); var older = Note(bob, "pinned before"); await _harness.Deliver(bob, "/human-centipede", Featuring(bob, "Add", older)); await _harness.Deliver(bob, "/human-centipede", Featuring(bob, "Add", mine)); await _harness.Deliver(bob, "/human-centipede", Featuring(bob, "Add", theirs)); // Smithereen's wall is a collection of the actor's too, but no pin await _harness.Deliver(bob, "/human-centipede", Featuring(bob, "Add", Note(bob, "on the wall"), bob.Id + "/wall")); Assert.Equal(new[] { mine, older }, (await FeaturedBy(bob)).Select(f => f.ObjectURI)); var post = await DB.Default.Find().Match(p => p.ObjectURI == mine).ExecuteSingleAsync(TestContext.Current.CancellationToken); Assert.Equal(post.ID, (await FeaturedBy(bob))[0].PostId); await _harness.Deliver(bob, "/human-centipede", Featuring(bob, "Remove", mine)); Assert.Equal(new[] { older }, (await FeaturedBy(bob)).Select(f => f.ObjectURI)); } [Fact] public async Task The_featured_collection_is_read_in_its_order_from_the_accounts_own_server() { var token = TestContext.Current.CancellationToken; var bob = new RemoteActor(_harness.Peer, "bob"); var first = Note(bob, "first pin"); var second = Note(bob, "second pin"); _harness.Peer.Serve(new Uri(bob.Id).AbsolutePath + "/featured", new JsonObject { ["id"] = bob.Id + "/featured", ["type"] = "OrderedCollection", ["totalItems"] = 2, ["orderedItems"] = new JsonArray(new JsonObject { ["id"] = first, ["type"] = "Note" }, second) }.ToJsonString()); var account = await _harness.Remote.GetActor(bob.Id, refresh: false, token); await _harness.Featured.Sync(account, token); Assert.Equal(new[] { first, second }, (await FeaturedBy(bob)).Select(f => f.ObjectURI)); } [Fact] public async Task A_personas_pin_and_unpin_reach_its_followers_as_add_and_remove() { var token = TestContext.Current.CancellationToken; var (root, alice) = await _harness.Persona("alice"); var follower = new RemoteActor(_harness.Peer, "fan"); await _harness.FollowedBy(alice, follower); await _harness.Posts.InsertPost(root, new PrivaPub.ClientModels.Post.InsertPostForm { AvatarId = alice.Id, Text = "worth a pin" }, token); var post = await DB.Default.Find().Match(p => p.GroupUserId == alice.Id).ExecuteFirstAsync(token); await _harness.Outbox.PublishFeatured(alice.Id, post, featured: true, token); await _harness.Outbox.PublishFeatured(alice.Id, post, featured: false, token); var sent = (await _harness.Outgoing(follower.SharedInbox)).Concat(await _harness.Outgoing(follower.Id + "/inbox")) .Where(a => a["type"]!.GetValue() is "Add" or "Remove").ToList(); Assert.Equal(new[] { "Add", "Remove" }, sent.Select(a => a["type"]!.GetValue())); Assert.All(sent, a => Assert.Equal((alice.PostUri(post.ID), alice.Featured), (a["object"]!.GetValue(), a["target"]!.GetValue()))); } [Fact] public async Task A_community_features_a_post_made_in_it() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var community = new RemoteActor(_harness.Peer, "books", type: "Group"); var member = new RemoteActor(_harness.Peer, "reader", _harness.Peer.B); var account = await _harness.Remote.GetActor(community.Id, refresh: false, token); await DB.Default.SaveAsync(new Following { AvatarId = alice.Id, TargetActorURI = community.Id, TargetAccountId = account.ID, State = FollowState.Accepted }, token); var post = Note(member, "a thread in the community", community.Id); await _harness.Deliver(community, "/human-centipede", new JsonObject { ["id"] = $"{community.Id}#announce-{Guid.NewGuid():N}", ["type"] = "Announce", ["actor"] = community.Id, ["to"] = new JsonArray("https://www.w3.org/ns/activitystreams#Public"), ["object"] = new JsonObject { ["id"] = $"{Origin(member)}/activities/{Guid.NewGuid():N}", ["type"] = "Add", ["actor"] = member.Id, ["object"] = post, ["target"] = community.Id + "/featured" } }); Assert.Equal(new[] { post }, (await FeaturedBy(community)).Select(f => f.ObjectURI)); } } }