using MongoDB.Entities; using PrivaPub.ClientModels.Post; using PrivaPub.Domain.Statuses; 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 QuoteTests : 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 Delivered(RemoteActor author, string mentioned, Action shape = default) { var note = new JsonObject { ["id"] = NewId(author, "notes"), ["type"] = "Note", ["attributedTo"] = author.Id, ["content"] = "

hello

", ["to"] = new JsonArray(Addressing.Public), ["cc"] = new JsonArray(mentioned), ["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = mentioned, ["name"] = "@someone" }) }; shape?.Invoke(note); await _harness.Deliver(author, "/human-centipede", new JsonObject { ["id"] = NewId(author, "activities"), ["type"] = "Create", ["actor"] = author.Id, ["object"] = note }); return await DB.Default.Find().Match(p => p.ObjectURI == note["id"]!.GetValue()).ExecuteFirstAsync(TestContext.Current.CancellationToken); } string Stamp(RemoteActor quotedAuthor, string quoting, string quoted) { var path = $"/stamps/{Guid.NewGuid():N}"; var id = Origin(quotedAuthor) + path; _harness.Peer.Serve(path, new JsonObject { ["@context"] = "https://www.w3.org/ns/activitystreams", ["id"] = id, ["type"] = "QuoteAuthorization", ["attributedTo"] = quotedAuthor.Id, ["interactingObject"] = quoting, ["interactionTarget"] = quoted }.ToJsonString()); return id; } [Fact] public async Task A_misskey_quote_of_a_public_post_is_shown_and_counted() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var ann = new RemoteActor(_harness.Peer, "ann"); var kitty = new RemoteActor(_harness.Peer, "kitty"); var original = await Delivered(ann, alice.Uri); var quoting = await Delivered(kitty, alice.Uri, n => { n["_misskey_quote"] = original.ObjectURI; n["content"] = $"

look
RE: {original.ObjectURI}

"; }); Assert.Equal(QuoteState.Accepted, quoting.QuoteState); Assert.Equal(original.ID, quoting.QuotedPostId); Assert.Equal(1, (await DB.Default.Find().OneAsync(original.ID, token)).QuotesCount); var status = await new PrivaPub.Api.Mastodon.Mappers.MastodonMapper(_harness.Db, _harness.Local).Status(quoting, alice.Id, token); Assert.Equal("accepted", status.Quote.State); Assert.Equal(original.ID, status.Quote.QuotedStatus.Id); Assert.DoesNotContain("RE:", status.Content); } [Fact] public async Task A_stamped_quote_is_accepted_a_forged_one_is_not_and_revoking_takes_it_back() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var ann = new RemoteActor(_harness.Peer, "ann"); var bob = new RemoteActor(_harness.Peer, "bob"); var original = await Delivered(ann, alice.Uri); var quotingId = NewId(bob, "notes"); var stamp = Stamp(ann, quotingId, original.ObjectURI); var forged = Stamp(ann, NewId(bob, "notes"), original.ObjectURI); var quoting = await Delivered(bob, alice.Uri, n => { n["id"] = quotingId; n["quote"] = original.ObjectURI; n["quoteAuthorization"] = stamp; }); var cheat = await Delivered(bob, alice.Uri, n => { n["quote"] = original.ObjectURI; n["quoteAuthorization"] = forged; }); var waiting = await Delivered(bob, alice.Uri, n => n["quote"] = original.ObjectURI); Assert.Equal(QuoteState.Accepted, quoting.QuoteState); Assert.Equal(QuoteState.Unauthorized, cheat.QuoteState); Assert.Equal(QuoteState.Pending, waiting.QuoteState); await _harness.Deliver(ann, "/human-centipede", new JsonObject { ["id"] = NewId(ann, "activities"), ["type"] = "Delete", ["actor"] = ann.Id, ["object"] = stamp }); Assert.Equal(QuoteState.Revoked, (await DB.Default.Find().OneAsync(quoting.ID, token)).QuoteState); Assert.Equal(0, (await DB.Default.Find().OneAsync(original.ID, token)).QuotesCount); } [Fact] public async Task A_persona_asks_before_quoting_and_is_stamped_when_accepted() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var ann = new RemoteActor(_harness.Peer, "ann"); var original = await Delivered(ann, alice.Uri, n => n["interactionPolicy"] = new JsonObject { ["canQuote"] = new JsonObject { ["automaticApproval"] = new JsonArray(Addressing.Public) } }); var outcome = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "this", QuotedStatusId = original.ID }, token); Assert.Equal(QuoteState.Pending, outcome.Post.QuoteState); var request = Assert.Single(await _harness.Outgoing(ann.Id + "/inbox"), a => a["type"]!.GetValue() == "QuoteRequest"); Assert.Equal(original.ObjectURI, request["object"]!.GetValue()); Assert.Equal(original.ObjectURI, request["instrument"]!["quote"]!.GetValue()); await _harness.Deliver(ann, "/human-centipede", new JsonObject { ["id"] = NewId(ann, "activities"), ["type"] = "Accept", ["actor"] = ann.Id, ["object"] = request["id"]!.GetValue(), ["result"] = Stamp(ann, outcome.Post.ObjectURI, original.ObjectURI) }); var quoting = await DB.Default.Find().OneAsync(outcome.Post.ID, token); Assert.Equal(QuoteState.Accepted, quoting.QuoteState); Assert.NotNull(quoting.QuoteAuthorizationURI); Assert.Contains(await _harness.Outgoing(ann.Id + "/inbox"), a => a["type"]!.GetValue() == "Update" && a["object"]!["quoteAuthorization"]?.GetValue() == quoting.QuoteAuthorizationURI); } [Fact] public async Task A_legacy_post_is_quoted_without_asking_and_another_persona_cannot_be_quoted_yet() { var token = TestContext.Current.CancellationToken; var (aliceRoot, alice) = await _harness.Persona("alice"); var (bobRoot, bob) = await _harness.Persona("bob"); var kitty = new RemoteActor(_harness.Peer, "kitty"); var legacy = await Delivered(kitty, alice.Uri); await _harness.Posts.InsertPost(bobRoot, new InsertPostForm { AvatarId = bob.Id, Text = "mine" }, token); var bobs = await DB.Default.Find().Match(p => p.GroupUserId == bob.Id).ExecuteFirstAsync(token); var outcome = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "nice", QuotedStatusId = legacy.ID }, token); Assert.Equal(QuoteState.Accepted, outcome.Post.QuoteState); Assert.False(outcome.Post.QuoteByConsent); Assert.False((await _harness.Statuses.Publish(alice, new StatusDraft { Text = "and this", QuotedStatusId = bobs.ID }, token)).Ok); Assert.True((await _harness.Statuses.Publish(bob, new StatusDraft { Text = "me again", QuotedStatusId = bobs.ID }, token)).Ok); } } }