using MongoDB.Entities; using PrivaPub.Models.Federation; using PrivaPub.Models.Post; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Federation { // relays the configuration names: the instance actor follows Public there, and the public posts the relay passes on, // forwarded or announced, come to the federated timeline as their authors' posts; nothing else is taken from it [Trait("Category", "Integration")] [Xunit.Collection(nameof(Exclusive))] public sealed class RelayTests : IAsyncLifetime { Harness _harness; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _harness = await Harness.Start(); } public async ValueTask DisposeAsync() { if (_harness != default) { _harness.RelayOptions.Relays.Clear(); await _harness.Relays.Reconcile(TestContext.Current.CancellationToken); await _harness.DisposeAsync(); } } static CancellationToken Token => TestContext.Current.CancellationToken; static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority); string Note(RemoteActor author, string text, string to = "https://www.w3.org/ns/activitystreams#Public") { var path = $"/notes/{Guid.NewGuid():N}"; _harness.Peer.Serve(path, new JsonObject { ["id"] = Origin(author) + path, ["type"] = "Note", ["attributedTo"] = author.Id, ["content"] = $"

{text}

", ["to"] = new JsonArray(to), ["published"] = DateTime.UtcNow.ToString("O") }.ToJsonString()); return Origin(author) + path; } JsonObject Create(RemoteActor author, string noteId) => new() { ["id"] = noteId + "/activity", ["type"] = "Create", ["actor"] = author.Id, ["to"] = new JsonArray("https://www.w3.org/ns/activitystreams#Public"), ["object"] = new JsonObject { ["id"] = noteId, ["type"] = "Note", ["attributedTo"] = author.Id } }; // a relay the instance actor subscribes to, which accepts async Task Subscribed() { var relay = new RemoteActor(_harness.Peer, "relay", type: "Service"); _harness.RelayOptions.Relays.Add(relay.Id); await _harness.Relays.Reconcile(Token); var follow = Assert.Single(await _harness.Outgoing(relay.Id + "/inbox"), a => a["type"]!.GetValue() == "Follow"); await _harness.Deliver(relay, "/human-centipede", new JsonObject { ["id"] = $"{relay.Id}#accept-{Guid.NewGuid():N}", ["type"] = "Accept", ["actor"] = relay.Id, ["object"] = follow.DeepClone() }); return relay; } [Fact] public async Task The_instance_actor_follows_public_at_a_configured_relay_and_takes_its_answer() { var relay = await Subscribed(); var follow = Assert.Single(await _harness.Outgoing(relay.Id + "/inbox")); Assert.Equal(("Follow", "https://www.w3.org/ns/activitystreams#Public"), (follow["type"]!.GetValue(), follow["object"]!.GetValue())); Assert.Equal((await _harness.Local.GetInstanceActor(Token)).Uri, follow["actor"]!.GetValue()); Assert.Equal(RelayState.Accepted, (await DB.Default.Find().Match(s => s.ActorURI == relay.Id).ExecuteSingleAsync(Token)).State); Assert.True(await _harness.Relays.Passes(relay.Id, Token)); _harness.RelayOptions.Relays.Clear(); await _harness.Relays.Reconcile(Token); Assert.Contains(await _harness.Outgoing(relay.Id + "/inbox"), a => a["type"]!.GetValue() == "Undo"); Assert.False(await DB.Default.Find().Match(s => s.ActorURI == relay.Id).ExecuteAnyAsync(Token)); } [Fact] public async Task A_public_post_a_subscribed_relay_forwards_is_kept_and_one_from_anyone_else_is_not() { var relay = await Subscribed(); var stranger = new RemoteActor(_harness.Peer, "stranger", type: "Service"); var author = new RemoteActor(_harness.Peer, "author", _harness.Peer.B); var relayed = Note(author, "through the relay"); var forwarded = Note(author, "through a stranger"); var followersOnly = Note(author, "for followers", author.Id + "/followers"); await _harness.Deliver(relay, "/human-centipede", Create(author, relayed)); await _harness.Deliver(stranger, "/human-centipede", Create(author, forwarded)); await _harness.Deliver(relay, "/human-centipede", Create(author, followersOnly)); var kept = await DB.Default.Find().Match(p => p.ActorURI == author.Id).ExecuteAsync(Token); Assert.Equal(new[] { relayed }, kept.Select(p => p.ObjectURI)); Assert.Equal(PostVisibility.Public, kept[0].Visibility); } [Fact] public async Task A_post_a_relay_announces_is_its_authors_never_the_relays_boost() { var relay = await Subscribed(); var author = new RemoteActor(_harness.Peer, "author", _harness.Peer.B); var noteId = Note(author, "announced by the relay"); await _harness.Deliver(relay, "/human-centipede", new JsonObject { ["id"] = $"{relay.Id}/activities/{Guid.NewGuid():N}", ["type"] = "Announce", ["actor"] = relay.Id, ["to"] = new JsonArray(relay.Id + "/followers"), ["object"] = noteId }); var post = await DB.Default.Find().Match(p => p.ObjectURI == noteId).ExecuteSingleAsync(Token); Assert.Equal(author.Id, post.ActorURI); Assert.False(await DB.Default.Find().Match(p => p.ReblogOfPostId == post.ID).ExecuteAnyAsync(Token)); } } }