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_a_persona_that_allows_nobody_cannot_be_quoted() { 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); bob.Settings.QuotePolicy = PrivaPub.Models.User.QuotePolicies.Nobody; var bobs = (await _harness.Statuses.Publish(bob, new StatusDraft { Text = "mine" }, token)).Post; 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); } JsonObject Request(RemoteActor quoter, string quotedUri, string quotingId) => new() { ["id"] = NewId(quoter, "activities"), ["type"] = "QuoteRequest", ["actor"] = quoter.Id, ["object"] = quotedUri, ["instrument"] = new JsonObject { ["id"] = quotingId, ["type"] = "Note", ["attributedTo"] = quoter.Id, ["quote"] = quotedUri, ["content"] = "

so true

" } }; [Fact] public async Task Anyone_may_quote_a_persona_by_default_and_the_licence_can_be_revoked() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var mallory = new RemoteActor(_harness.Peer, "mallory"); await _harness.Deliver(mallory, "/human-centipede", new JsonObject { ["id"] = NewId(mallory, "follows"), ["type"] = "Follow", ["actor"] = mallory.Id, ["object"] = alice.Uri }); var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "quote me" }, token)).Post; var created = (await _harness.Outgoing(mallory.Id + "/inbox")).Single(a => a["type"]!.GetValue() == "Create")["object"]!; Assert.Equal(Addressing.Public, created["interactionPolicy"]!["canQuote"]!["automaticApproval"]![0]!.GetValue()); var quotingId = NewId(mallory, "notes"); await _harness.Deliver(mallory, "/human-centipede", Request(mallory, original.ObjectURI, quotingId)); var accept = Assert.Single(await _harness.Outgoing(mallory.Id + "/inbox"), a => a["type"]!.GetValue() == "Accept" && a["result"] != null); var licence = accept["result"]!.GetValue(); Assert.StartsWith(alice.Uri + "/parrot-licences/", licence); var quoting = await Delivered(mallory, alice.Uri, n => { n["id"] = quotingId; n["quote"] = original.ObjectURI; n["quoteAuthorization"] = licence; }); Assert.Equal(QuoteState.Accepted, quoting.QuoteState); Assert.Equal(1, (await DB.Default.Find().OneAsync(original.ID, token)).QuotesCount); Assert.True(await _harness.Quotes.RevokeLicence(original, quoting, token)); Assert.Equal(QuoteState.Revoked, (await DB.Default.Find().OneAsync(quoting.ID, token)).QuoteState); Assert.Contains(await _harness.Outgoing(mallory.Id + "/inbox"), a => a["type"]!.GetValue() == "Delete" && a["object"]!.GetValue() == licence); } [Fact] public async Task A_followers_only_persona_turns_a_stranger_down() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); alice.Settings.QuotePolicy = PrivaPub.Models.User.QuotePolicies.Followers; var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "friends only quoting" }, token)).Post; var stranger = new RemoteActor(_harness.Peer, "stranger"); await _harness.Deliver(stranger, "/human-centipede", Request(stranger, original.ObjectURI, NewId(stranger, "notes"))); Assert.Equal("Reject", Assert.Single(await _harness.Outgoing(stranger.Id + "/inbox"))["type"]!.GetValue()); } [Fact] public async Task One_persona_quotes_another_with_a_licence() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var (_, bob) = await _harness.Persona("bob"); var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "public thought" }, token)).Post; var outcome = await _harness.Statuses.Publish(bob, new StatusDraft { Text = "agreed", QuotedStatusId = original.ID }, token); Assert.True(outcome.Ok); Assert.Equal(QuoteState.Accepted, outcome.Post.QuoteState); Assert.StartsWith(alice.Uri + "/parrot-licences/", outcome.Post.QuoteAuthorizationURI); Assert.Equal(1, (await DB.Default.Find().OneAsync(original.ID, token)).QuotesCount); } } }