OutboxPublisher works out who a post goes to, Mastodon's way: followers'
shared inboxes for public, unlisted and followers-only; every mentioned
remote account's own inbox; the recipients only for direct; the parent's
author for a reply; a community's followers for a post in it; nobody for
a circle. Creates, updates and deletes all use it.
- Local posts take a visibility (public, unlisted, followers-only) and a
spoiler text next to the content-warning flag.
- /clientapi/post/update keeps the previous version as a revision,
re-renders, and sends Update{Note} with `updated` to the same to/cc.
- Deleting is now soft: the content, title, spoiler, media and revisions
are cleared, timeline entries removed, the parent's reply count goes
down, Delete goes to the stored audience, and the object answers 410
with a Tombstone instead of 404.
- Editing a persona's profile sends Update{Person} to its followers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
93 lines
4.0 KiB
C#
93 lines
4.0 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.ClientModels.Post;
|
|
using PrivaPub.ClientModels;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
namespace PrivaPub.Tests.Domain
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class OutboxTests : 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();
|
|
}
|
|
|
|
async Task<ForeignAvatar> Known(RemoteActor actor) =>
|
|
await _harness.Remote.GetActor(actor.Id, refresh: false, TestContext.Current.CancellationToken);
|
|
|
|
[Fact]
|
|
public async Task The_audience_follows_mastodons_table()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var follower = new RemoteActor(_harness.Peer, "follower");
|
|
var mentioned = await Known(new RemoteActor(_harness.Peer, "mentioned"));
|
|
var parentAuthor = await Known(new RemoteActor(_harness.Peer, "parent"));
|
|
await _harness.FollowedBy(alice, follower);
|
|
var mention = new PostMention { ActorURI = mentioned.ActorURI };
|
|
|
|
async Task<IReadOnlyList<string>> Audience(PostVisibility visibility, string replyTo = default) =>
|
|
await _harness.Outbox.Audience(alice, new Post { Visibility = visibility, Mentions = new() { mention }, To = new() { mentioned.ActorURI }, InReplyToAccountId = replyTo }, token);
|
|
|
|
Assert.Equal(new[] { follower.SharedInbox, mentioned.InboxURL }, await Audience(PostVisibility.Public));
|
|
Assert.Equal(new[] { follower.SharedInbox, mentioned.InboxURL, parentAuthor.InboxURL }, await Audience(PostVisibility.Unlisted, parentAuthor.ID));
|
|
Assert.Equal(new[] { follower.SharedInbox, mentioned.InboxURL }, await Audience(PostVisibility.FollowersOnly));
|
|
Assert.Equal(new[] { mentioned.InboxURL }, await Audience(PostVisibility.Direct, parentAuthor.ID));
|
|
Assert.Empty(await Audience(PostVisibility.Circle));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_post_is_created_edited_and_deleted_for_its_audience()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (root, alice) = await _harness.Persona("alice");
|
|
var follower = new RemoteActor(_harness.Peer, "follower");
|
|
await _harness.FollowedBy(alice, follower);
|
|
|
|
var created = await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = "first", Visibility = "followersonly" }, token);
|
|
var postId = ((ViewPost)created.Data).Id;
|
|
await _harness.Posts.UpdatePost(root, new UpdatePostForm { AvatarId = alice.Id, PostId = postId, Text = "second", SpoilerText = "cw" }, token);
|
|
await _harness.Posts.DeletePost(root, new DeletePostForm { AvatarId = alice.Id, PostId = postId }, token);
|
|
|
|
var sent = await _harness.Outgoing(follower.SharedInbox);
|
|
Assert.Equal(new[] { "Create", "Update", "Delete" }, sent.Select(a => a["type"]!.GetValue<string>()));
|
|
Assert.DoesNotContain(sent[0]["to"]!.AsArray(), n => n!.GetValue<string>().EndsWith("#Public"));
|
|
Assert.Equal("cw", sent[1]["object"]!["summary"]!.GetValue<string>());
|
|
Assert.NotNull(sent[1]["object"]!["updated"]);
|
|
Assert.Equal(sent[0]["to"]!.ToJsonString(), sent[2]["to"]!.ToJsonString());
|
|
|
|
var stored = await DB.Default.Find<Post>().OneAsync(postId, token);
|
|
Assert.NotNull(stored.DeletedAt);
|
|
Assert.Null(stored.ContentHtml);
|
|
Assert.Empty(stored.Revisions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Someone_elses_post_cannot_be_edited()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (root, alice) = await _harness.Persona("alice");
|
|
var (otherRoot, bob) = await _harness.Persona("bob");
|
|
var created = await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = "mine" }, token);
|
|
|
|
var result = await _harness.Posts.UpdatePost(otherRoot, new UpdatePostForm { AvatarId = bob.Id, PostId = ((ViewPost)created.Data).Id, Text = "yours" }, token);
|
|
|
|
Assert.False(result.IsValid);
|
|
Assert.Equal(404, result.StatusCode);
|
|
}
|
|
}
|
|
}
|