114 lines
4.5 KiB
C#
114 lines
4.5 KiB
C#
using MongoDB.Entities;
|
|||
|
|
|
||
|
|
using PrivaPub.Domain.Statuses;
|
||
|
|
using PrivaPub.Models.Post;
|
||
|
|
using PrivaPub.Tests.Support;
|
||
|
|
|
||
|
|
using System.Text.Json.Nodes;
|
||
|
|
|
||
|
|
using static PrivaPub.Tests.Support.FederatedSeeds;
|
||
|
|
|
||
|
|
namespace PrivaPub.Tests.Federation
|
||
|
|
{
|
||
|
|
// Inbox forwarding of replies (owner decision 2026-10-05): a reply to a persona's post reaches the persona's followers,
|
||
|
|
// who are not among those its author's server sends it to
|
||
|
|
[Trait("Category", "Integration")]
|
||
|
|
public sealed class ReplyRelayTests : 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();
|
||
|
|
}
|
||
|
|
|
||
|
|
// alice's post; bob replies from his server, fan follows alice from another, pal from bob's
|
||
|
|
async Task<(PrivaPub.Federation.Actors.LocalActor Alice, Post Post, RemoteActor Bob, RemoteActor Fan, RemoteActor Pal)> Thread()
|
||
|
|
{
|
||
|
|
var token = TestContext.Current.CancellationToken;
|
||
|
|
var (_, alice) = await _harness.Persona("alice");
|
||
|
|
var bob = new RemoteActor(_harness.Peer, "bob");
|
||
|
|
var fan = new RemoteActor(_harness.Peer, "fan", _harness.Peer.B);
|
||
|
|
var pal = new RemoteActor(_harness.Peer, "pal");
|
||
|
|
await _harness.FollowedBy(alice, fan);
|
||
|
|
await _harness.FollowedBy(alice, pal);
|
||
|
|
var post = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "what do you think?" }, token)).Post;
|
||
|
|
return (alice, post, bob, fan, pal);
|
||
|
|
}
|
||
|
|
|
||
|
|
JsonObject Reply(RemoteActor bob, Post post, string visibility = "public")
|
||
|
|
{
|
||
|
|
var reply = PublicNote(bob, "<p>I think so</p>", post.ActorURI);
|
||
|
|
reply["inReplyTo"] = post.ObjectURI;
|
||
|
|
if (visibility == "followers")
|
||
|
|
{
|
||
|
|
reply["to"] = new JsonArray(bob.Id + "/followers");
|
||
|
|
reply["cc"] = new JsonArray(post.ActorURI);
|
||
|
|
}
|
||
|
|
return reply;
|
||
|
|
}
|
||
|
|
|
||
|
|
Task<List<JsonObject>> At(RemoteActor follower) => _harness.Outgoing(follower.SharedInbox);
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_public_reply_to_a_personas_post_reaches_its_followers_as_it_came_and_not_its_authors_server()
|
||
|
|
{
|
||
|
|
var (_, post, bob, fan, pal) = await Thread();
|
||
|
|
var create = Create(bob, Reply(bob, post));
|
||
|
|
create["signature"] = new JsonObject { ["type"] = "RsaSignature2017", ["signatureValue"] = "kept as it came" };
|
||
|
|
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", create);
|
||
|
|
|
||
|
|
var relayed = Assert.Single(await At(fan), a => a["id"]!.GetValue<string>() == IdOf(create));
|
||
|
|
Assert.Equal(bob.Id, relayed["actor"]!.GetValue<string>());
|
||
|
|
Assert.Equal("kept as it came", relayed["signature"]!["signatureValue"]!.GetValue<string>());
|
||
|
|
Assert.DoesNotContain(await At(pal), a => a["id"]!.GetValue<string>() == IdOf(create));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Its_edit_and_deletion_follow_it_and_a_followers_only_reply_stays_where_it_was_sent()
|
||
|
|
{
|
||
|
|
var (_, post, bob, fan, _) = await Thread();
|
||
|
|
var reply = Reply(bob, post);
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", Create(bob, reply));
|
||
|
|
var edited = (JsonObject)reply.DeepClone();
|
||
|
|
edited["content"] = "<p>I think so, on second thought</p>";
|
||
|
|
edited["updated"] = DateTime.UtcNow.AddMinutes(1).ToString("O");
|
||
|
|
var update = Activity(bob, "Update", edited);
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", update);
|
||
|
|
var delete = Activity(bob, "Delete", JsonValue.Create(IdOf(reply))!);
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", delete);
|
||
|
|
|
||
|
|
var relayed = (await At(fan)).Select(a => a["id"]!.GetValue<string>()).ToList();
|
||
|
|
Assert.Contains(IdOf(update), relayed);
|
||
|
|
Assert.Contains(IdOf(delete), relayed);
|
||
|
|
|
||
|
|
var quiet = Create(bob, Reply(bob, post, "followers"));
|
||
|
|
await _harness.Deliver(bob, "/human-centipede", quiet);
|
||
|
|
Assert.DoesNotContain(await At(fan), a => a["id"]!.GetValue<string>() == IdOf(quiet));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_reply_that_reached_us_forwarded_is_not_passed_on_again()
|
||
|
|
{
|
||
|
|
var (_, post, bob, fan, _) = await Thread();
|
||
|
|
var other = new RemoteActor(_harness.Peer, "other", _harness.Peer.B);
|
||
|
|
var reply = Reply(bob, post);
|
||
|
|
_harness.Peer.Serve(new Uri(IdOf(reply)).AbsolutePath, reply.ToJsonString());
|
||
|
|
var create = Create(bob, reply);
|
||
|
|
|
||
|
|
var result = await _harness.Deliver(other, "/human-centipede", create);
|
||
|
|
|
||
|
|
Assert.Equal("forwarded", result.Reason);
|
||
|
|
Assert.True(await DB.Default.Find<Post>().Match(p => p.ObjectURI == IdOf(reply)).ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
||
|
|
Assert.DoesNotContain(await At(fan), a => a["id"]!.GetValue<string>() == IdOf(create));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|