- Dislike and its Undo are kept as downvotes (Lemmy, PieFed, Mbin, Friendica) and shown with favourites as votes. - ChatMessage (Lemmy 0.19, Mbin, PieFed to those two) arrives as a direct message. - A deleted object's id is remembered for 90 days, so a Create that arrives after its Delete cannot bring the post back; the post's ObjectRecord goes with it. - A Join of one of our objects is answered with Ignore, as FEP-8a8e asks of a server without RSVP. - A 503 with Retry-After is waited out like a 429 instead of counting as a failure of the host (GoToSocial throttling, Mastodon's temporary key failures). - Status.privapub carries what a Mastodon Status cannot: object type, title, excerpt, cover, the author's source, link, video, audio and event details, and up/down votes, with every media URL proxied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
125 lines
4.7 KiB
C#
125 lines
4.7 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.ClientModels.Post;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Federation
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class InboundRoutingTests : 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);
|
|
|
|
static string NewId(RemoteActor actor, string kind) => $"{Origin(actor)}/{kind}/{Guid.NewGuid():N}";
|
|
|
|
async Task<Post> LocalPost(string text)
|
|
{
|
|
var (root, alice) = await _harness.Persona("alice");
|
|
await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = text }, TestContext.Current.CancellationToken);
|
|
return await DB.Default.Find<Post>().Match(p => p.GroupUserId == alice.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_downvote_counts_and_its_undo_takes_it_back()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var post = await LocalPost("vote on me");
|
|
var lemmy = new RemoteActor(_harness.Peer, "voter");
|
|
var dislike = new JsonObject { ["id"] = NewId(lemmy, "activities/dislike"), ["type"] = "Dislike", ["actor"] = lemmy.Id, ["object"] = post.ObjectURI };
|
|
|
|
await _harness.Deliver(lemmy, "/human-centipede", dislike);
|
|
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).DownvotesCount);
|
|
|
|
await _harness.Deliver(lemmy, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(lemmy, "activities/undo"), ["type"] = "Undo", ["actor"] = lemmy.Id, ["object"] = dislike.DeepClone()
|
|
});
|
|
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).DownvotesCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_delete_that_arrives_first_keeps_the_post_from_coming_back()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
|
var noteId = NewId(mallory, "notes");
|
|
|
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(mallory, "activities/delete"), ["type"] = "Delete", ["actor"] = mallory.Id,
|
|
["object"] = new JsonObject { ["id"] = noteId, ["type"] = "Tombstone" }
|
|
});
|
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(mallory, "activities/create"), ["type"] = "Create", ["actor"] = mallory.Id,
|
|
["object"] = new JsonObject
|
|
{
|
|
["id"] = noteId, ["type"] = "Note", ["attributedTo"] = mallory.Id, ["content"] = "<p>late</p>",
|
|
["to"] = new JsonArray(Addressing.Public), ["cc"] = new JsonArray(alice.Uri),
|
|
["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@alice" })
|
|
}
|
|
});
|
|
|
|
Assert.False(await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteAnyAsync(token));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_lemmy_private_message_arrives_as_a_direct_message()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var lemmy = new RemoteActor(_harness.Peer, "pm");
|
|
var messageId = NewId(lemmy, "private_message");
|
|
|
|
await _harness.Deliver(lemmy, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(lemmy, "activities/create"), ["type"] = "Create", ["actor"] = lemmy.Id, ["to"] = new JsonArray(alice.Uri),
|
|
["object"] = new JsonObject
|
|
{
|
|
["id"] = messageId, ["type"] = "ChatMessage", ["attributedTo"] = lemmy.Id, ["to"] = new JsonArray(alice.Uri),
|
|
["content"] = "<p>psst</p>", ["mediaType"] = "text/html", ["published"] = DateTime.UtcNow.ToString("O")
|
|
}
|
|
});
|
|
|
|
var message = await DB.Default.Find<Post>().Match(p => p.ObjectURI == messageId).ExecuteFirstAsync(token);
|
|
Assert.NotNull(message);
|
|
Assert.Equal(PostVisibility.Direct, message.Visibility);
|
|
Assert.NotNull(message.ConversationId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_join_of_a_local_post_is_answered_with_ignore()
|
|
{
|
|
var post = await LocalPost("not an event");
|
|
var mobilizon = new RemoteActor(_harness.Peer, "joiner");
|
|
|
|
await _harness.Deliver(mobilizon, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(mobilizon, "join"), ["type"] = "Join", ["actor"] = mobilizon.Id, ["object"] = post.ObjectURI
|
|
});
|
|
|
|
var answer = Assert.Single(await _harness.Outgoing(mobilizon.Id + "/inbox"));
|
|
Assert.Equal("Ignore", answer["type"]!.GetValue<string>());
|
|
}
|
|
}
|
|
}
|