Federation:Relays names relays by their actor (or inbox) address; the instance actor follows Public at each, as Mastodon subscribes, a minute after start and every six hours (asked again a day after no answer or a refusal, undone when a relay is no longer named). What an accepted relay passes on comes to the federated timeline and nobody's home: a public post it forwards (Activity-Relay), read again from its origin like any forwarded post, and a post it announces (aode-relay), kept as its author's and never as the relay's boost. Nothing of a persona's is sent to a relay; sending public posts there waits for the owner. The pasture gains both relays (peers/relay.sh, peers/aoderelay.sh) and scenarios/relay.sh, 13 checks. The village backlog is clean: 2574 checks pass, one known gap (Misskey's). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
126 lines
5.2 KiB
C#
126 lines
5.2 KiB
C#
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"] = $"<p>{text}</p>",
|
|
["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<RemoteActor> 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<string>() == "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<string>(), follow["object"]!.GetValue<string>()));
|
|
Assert.Equal((await _harness.Local.GetInstanceActor(Token)).Uri, follow["actor"]!.GetValue<string>());
|
|
Assert.Equal(RelayState.Accepted, (await DB.Default.Find<RelaySubscription>().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<string>() == "Undo");
|
|
Assert.False(await DB.Default.Find<RelaySubscription>().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<Post>().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<Post>().Match(p => p.ObjectURI == noteId).ExecuteSingleAsync(Token);
|
|
Assert.Equal(author.Id, post.ActorURI);
|
|
Assert.False(await DB.Default.Find<Post>().Match(p => p.ReblogOfPostId == post.ID).ExecuteAnyAsync(Token));
|
|
}
|
|
}
|
|
}
|