Pins across servers, both ways
An account elsewhere that pins or unpins one of its posts (Add or Remove on its `featured`) now shows those pins on its profile here (`pinned=true`), in its order and its public posts only; a community's announce of a moderator's Add does the same for the community. The `featured` collection itself is read with the account's counts, at most once a day, so pins made before PrivaPub ever saw an account show too. Any other target (Smithereen's wall, a community's moderators) is dropped. A persona's pin and unpin go to the post's audience as Add and Remove on /trophies, as Mastodon sends them. Checked live against Mastodon (scenarios/pins.sh, 8 checks). The town's checker learnt three peer rules from the village: Misskey and Sharkey keep a forwarded reply only with its author's LD signature (only Mastodon signs), they count no renote by a bot, and Mastodon never sees a Lemmy vote on a post in a community. The village of 2026-10-05 checks clean, 2454 of 2454. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
5f150b7a59
commit
1c7d1ece9c
23 files changed
+510
-19
No files matched your search
@@ -0,0 +1,143 @@
|
||||
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"] = $"<p>{text}</p>",
|
||||
["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<List<RemoteFeatured>> FeaturedBy(RemoteActor actor) =>
|
||||
DB.Default.Find<RemoteFeatured>().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<Post>().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<Post>().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<string>() is "Add" or "Remove").ToList();
|
||||
Assert.Equal(new[] { "Add", "Remove" }, sent.Select(a => a["type"]!.GetValue<string>()));
|
||||
Assert.All(sent, a => Assert.Equal((alice.PostUri(post.ID), alice.Featured), (a["object"]!.GetValue<string>(), a["target"]!.GetValue<string>())));
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user