224 lines
11 KiB
C#
224 lines
11 KiB
C#
using MongoDB.Entities;
|
|||
|
|
|
||
|
|
using PrivaPub.Models.Jobs;
|
||
|
|
using PrivaPub.Models.Post;
|
||
|
|
using PrivaPub.Tests.Support;
|
||
|
|
using PrivaPub.Tests.Support.Host;
|
||
|
|
|
||
|
|
using System.Net;
|
||
|
|
using System.Text.Json.Nodes;
|
||
|
|
|
||
|
|
namespace PrivaPub.Tests.Http
|
||
|
|
{
|
||
|
|
[Trait("Category", "Integration")]
|
||
|
|
public sealed class ProvenanceAndReactionsTests : IAsyncLifetime
|
||
|
|
{
|
||
|
|
PrivaPubHost _host;
|
||
|
|
Peer _peer;
|
||
|
|
|
||
|
|
public async ValueTask InitializeAsync()
|
||
|
|
{
|
||
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
||
|
|
_host = await PrivaPubHost.Shared();
|
||
|
|
_peer = await Peer.Start();
|
||
|
|
}
|
||
|
|
|
||
|
|
public async ValueTask DisposeAsync()
|
||
|
|
{
|
||
|
|
if (_peer != default)
|
||
|
|
await _peer.DisposeAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
static CancellationToken Token => TestContext.Current.CancellationToken;
|
||
|
|
|
||
|
|
static string Provenance(string id) => $"/api/privapub/v1/statuses/{id}/provenance";
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_local_post_has_a_local_provenance()
|
||
|
|
{
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var status = await alice.Status("made here");
|
||
|
|
|
||
|
|
var provenance = (await _host.Client().Get(Provenance(status.Text("id")))).Ok();
|
||
|
|
|
||
|
|
Assert.Equal("local", provenance.Body.Text("path"));
|
||
|
|
Assert.Equal(status.Text("uri"), provenance.Body.Text("object_uri"));
|
||
|
|
Assert.Equal(status.Text("url"), provenance.Body.Text("url"));
|
||
|
|
Assert.Null(provenance.Body["signature"]);
|
||
|
|
Assert.Null(provenance.Body["raw"]);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_delivered_post_shows_its_signature_and_raw_form_and_reading_it_asks_nobody()
|
||
|
|
{
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var bob = new RemoteActor(_peer, "bob");
|
||
|
|
var create = bob.Create("<p>signed and sealed</p>", new[] { MastodonHelpers.Public }, new[] { alice.Uri },
|
||
|
|
note => note["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@" + alice.UserName }));
|
||
|
|
await _host.Deliver(bob, alice.Mouth, create);
|
||
|
|
var noteId = create["object"].Text("id");
|
||
|
|
var post = await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(Token);
|
||
|
|
var asked = _peer.Requests.Count;
|
||
|
|
|
||
|
|
var provenance = (await alice.Client.Get(Provenance(post.ID))).Ok().Body;
|
||
|
|
var anonymous = (await _host.Client().Get(Provenance(post.ID))).Ok().Body;
|
||
|
|
|
||
|
|
Assert.Equal("delivered", provenance.Text("path"));
|
||
|
|
Assert.Equal(noteId, provenance.Text("object_uri"));
|
||
|
|
Assert.Equal("Note", provenance.Text("object_type"));
|
||
|
|
Assert.Null(provenance["fetched_by"]);
|
||
|
|
Assert.Equal((create.Text("id"), "Create", bob.Id), (provenance["activity"].Text("id"), provenance["activity"].Text("type"), provenance["activity"].Text("actor")));
|
||
|
|
Assert.Equal(bob.KeyId, provenance["signature"].Text("key_id"));
|
||
|
|
Assert.Equal("rsa-sha256", provenance["signature"].Text("algorithm"));
|
||
|
|
Assert.Contains("digest", provenance["signature"]!["headers"]!.AsArray().Select(h => h!.GetValue<string>()));
|
||
|
|
Assert.Equal(noteId, provenance["raw"].Text("id"));
|
||
|
|
Assert.False(string.IsNullOrEmpty(provenance.Text("raw_hash")));
|
||
|
|
Assert.True(provenance.Number("raw_bytes") > 0);
|
||
|
|
Assert.NotNull(provenance.Text("received_at"));
|
||
|
|
Assert.Equal(provenance.ToJsonString(), anonymous.ToJsonString());
|
||
|
|
Assert.Equal(asked, _peer.Requests.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task A_fetched_parent_shows_no_signature_and_was_fetched_by_the_instance_actor()
|
||
|
|
{
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var bob = new RemoteActor(_peer, "bob");
|
||
|
|
var parentPath = $"/notes/{Guid.NewGuid():N}";
|
||
|
|
var parentId = _peer.A + parentPath;
|
||
|
|
_peer.Serve(parentPath, new JsonObject
|
||
|
|
{
|
||
|
|
["id"] = parentId,
|
||
|
|
["type"] = "Note",
|
||
|
|
["attributedTo"] = bob.Id,
|
||
|
|
["to"] = new JsonArray(MastodonHelpers.Public),
|
||
|
|
["content"] = "<p>the start of it</p>",
|
||
|
|
["published"] = DateTime.UtcNow.AddMinutes(-5).ToString("O")
|
||
|
|
}.ToJsonString());
|
||
|
|
var reply = bob.Create("<p>and a reply</p>", new[] { MastodonHelpers.Public }, new[] { alice.Uri }, note =>
|
||
|
|
{
|
||
|
|
note["inReplyTo"] = parentId;
|
||
|
|
note["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@" + alice.UserName });
|
||
|
|
});
|
||
|
|
await _host.Deliver(bob, alice.Mouth, reply);
|
||
|
|
var parent = await DB.Default.Find<Post>().Match(p => p.ObjectURI == parentId).ExecuteFirstAsync(Token);
|
||
|
|
Assert.NotNull(parent);
|
||
|
|
var asked = _peer.Requests.Count;
|
||
|
|
|
||
|
|
var provenance = (await alice.Client.Get(Provenance(parent.ID))).Ok().Body;
|
||
|
|
|
||
|
|
Assert.Equal("fetched", provenance.Text("path"));
|
||
|
|
Assert.Equal("instance-actor", provenance.Text("fetched_by"));
|
||
|
|
Assert.Null(provenance["signature"]);
|
||
|
|
Assert.Equal((reply.Text("id"), "Create"), (provenance["activity"].Text("id"), provenance["activity"].Text("type")));
|
||
|
|
Assert.Null(provenance["inbox"]);
|
||
|
|
Assert.Equal(parentId, provenance["raw"].Text("id"));
|
||
|
|
Assert.Contains(_peer.Requests, r => r.Path == parentPath && r.Signature.Contains("/peasants/privapub#main-key"));
|
||
|
|
Assert.Equal(asked, _peer.Requests.Count);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Provenance_follows_who_may_see_the_post()
|
||
|
|
{
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var other = await _host.Mastodon("other");
|
||
|
|
var bob = new RemoteActor(_peer, "bob");
|
||
|
|
var direct = bob.Create("<p>between us</p>", new[] { alice.Uri }, shape: note =>
|
||
|
|
note["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@" + alice.UserName }));
|
||
|
|
await _host.Deliver(bob, alice.Mouth, direct);
|
||
|
|
var noteId = direct["object"].Text("id");
|
||
|
|
var post = await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(Token);
|
||
|
|
|
||
|
|
Assert.Equal("delivered", (await alice.Client.Get(Provenance(post.ID))).Ok().Body.Text("path"));
|
||
|
|
Assert.Equal(HttpStatusCode.NotFound, (await other.Client.Get(Provenance(post.ID))).Status);
|
||
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _host.Client().Get(Provenance(post.ID))).Status);
|
||
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _host.Client().Get(Provenance("000000000000000000000000"))).Status);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task An_instance_is_described_from_what_was_stored_and_reading_it_asks_nobody()
|
||
|
|
{
|
||
|
|
var host = $"described{Guid.NewGuid():N}.example";
|
||
|
|
await DB.Default.SaveAsync(new RemoteInstance
|
||
|
|
{
|
||
|
|
Host = host,
|
||
|
|
Software = "gotosocial",
|
||
|
|
SoftwareVersion = "0.20.0",
|
||
|
|
NodeName = "a test pasture",
|
||
|
|
Protocols = new List<string> { "activitypub" },
|
||
|
|
OpenRegistrations = false,
|
||
|
|
DescribedAt = DateTime.UtcNow.AddDays(-1),
|
||
|
|
NodeInfo = "{\"software\":{\"name\":\"gotosocial\",\"version\":\"0.20.0\"}}",
|
||
|
|
ConsecutiveFailures = 2,
|
||
|
|
LastFailureAt = DateTime.UtcNow.AddHours(-1)
|
||
|
|
}, Token);
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
|
||
|
|
var described = (await alice.Client.Get($"/api/privapub/v1/instances/{host.ToUpperInvariant()}")).Ok().Body;
|
||
|
|
|
||
|
|
Assert.Equal(host, described.Text("host"));
|
||
|
|
Assert.Equal(("gotosocial", "0.20.0", "a test pasture"), (described.Text("software"), described.Text("version"), described.Text("node_name")));
|
||
|
|
Assert.Equal("activitypub", Assert.Single(described["protocols"]!.AsArray())!.GetValue<string>());
|
||
|
|
Assert.False(described.Flag("open_registrations"));
|
||
|
|
Assert.Equal("gotosocial", described["node_info"]!["software"].Text("name"));
|
||
|
|
Assert.Equal(2, described["delivery"].Number("consecutive_failures"));
|
||
|
|
Assert.NotNull(described["delivery"].Text("last_failure_at"));
|
||
|
|
Assert.Null(described["delivery"]["last_success_at"]);
|
||
|
|
Assert.Equal(described.ToJsonString(), (await _host.Client().Get($"/api/privapub/v1/instances/{host}")).Ok().Body.ToJsonString());
|
||
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _host.Client().Get($"/api/privapub/v1/instances/unknown{Guid.NewGuid():N}.example")).Status);
|
||
|
|
Assert.Empty(_peer.Requests);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Reacting_to_a_remote_post_federates_an_emoji_react_and_its_undo()
|
||
|
|
{
|
||
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var carol = await _host.Mastodon("carol");
|
||
|
|
var bob = new RemoteActor(_peer, "bob");
|
||
|
|
var post = await _host.PublicPostFrom(bob, alice);
|
||
|
|
var path = $"/api/v1/pleroma/statuses/{post.ID}/reactions";
|
||
|
|
|
||
|
|
var reacted = (await alice.Client.Put(path + "/%F0%9F%91%8D")).Ok();
|
||
|
|
|
||
|
|
var reaction = Assert.Single(reacted.Body["emoji_reactions"]!.AsArray());
|
||
|
|
Assert.Equal(("👍", 1, true), (reaction.Text("name"), reaction.Number("count"), reaction.Flag("me")));
|
||
|
|
Assert.Equal("👍", Assert.Single(reacted.Body["pleroma"]!["emoji_reactions"]!.AsArray()).Text("name"));
|
||
|
|
(await carol.Client.Put(path + "/%F0%9F%91%8D")).Ok();
|
||
|
|
var listed = Assert.Single((await _host.Client().Get(path)).Ok().Array);
|
||
|
|
Assert.Equal(("👍", 2, false), (listed.Text("name"), listed.Number("count"), listed.Flag("me")));
|
||
|
|
Assert.Equal(new[] { alice.Id, carol.Id }.Order(), listed!["accounts"]!.AsArray().Select(a => a.Text("id")).Order());
|
||
|
|
Assert.True(Assert.Single((await alice.Client.Get(path)).Ok().Array).Flag("me"));
|
||
|
|
var react = Assert.Single(await bob.Delivered(since), d => d.Type() == "EmojiReact" && d.Text("actor") == alice.Uri);
|
||
|
|
Assert.Equal(("👍", post.ObjectURI), (react.Text("content"), react.Text("object")));
|
||
|
|
|
||
|
|
var withdrawn = (await alice.Client.Delete(path + "/%F0%9F%91%8D")).Ok();
|
||
|
|
|
||
|
|
Assert.Equal(1, Assert.Single(withdrawn.Body["emoji_reactions"]!.AsArray()).Number("count"));
|
||
|
|
var undo = Assert.Single(await bob.Delivered(since), d => d.Type() == "Undo" && d.Text("actor") == alice.Uri);
|
||
|
|
Assert.Equal(react.Text("id"), undo["object"].Text("id"));
|
||
|
|
Assert.Equal("EmojiReact", undo["object"].Text("type"));
|
||
|
|
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await alice.Client.Put(path + "/abc")).Status);
|
||
|
|
Assert.Equal(HttpStatusCode.NotFound, (await alice.Client.Put("/api/v1/pleroma/statuses/000000000000000000000000/reactions/%F0%9F%91%8D")).Status);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task Reacting_to_a_local_post_notifies_its_author_and_federates_nothing()
|
||
|
|
{
|
||
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
||
|
|
var alice = await _host.Mastodon("alice");
|
||
|
|
var carol = await _host.Mastodon("carol");
|
||
|
|
var bob = new RemoteActor(_peer, "bob");
|
||
|
|
await _host.FollowedBy(carol, bob);
|
||
|
|
var post = await carol.Status("react to me");
|
||
|
|
|
||
|
|
(await alice.Client.Put($"/api/v1/pleroma/statuses/{post.Text("id")}/reactions/%F0%9F%8E%89")).Ok();
|
||
|
|
|
||
|
|
var notification = Assert.Single((await carol.Client.Get("/api/v1/notifications?types[]=pleroma:emoji_reaction")).Ok().Array);
|
||
|
|
Assert.Equal(("🎉", alice.Id, post.Text("id")), (notification.Text("emoji"), notification["account"].Text("id"), notification["status"].Text("id")));
|
||
|
|
Assert.DoesNotContain(await bob.Delivered(since), d => d.Type() is "EmojiReact" or "Like");
|
||
|
|
(await alice.Client.Delete($"/api/v1/pleroma/statuses/{post.Text("id")}/reactions/%F0%9F%8E%89")).Ok();
|
||
|
|
Assert.Empty((await carol.Client.Get($"/api/v1/pleroma/statuses/{post.Text("id")}/reactions")).Ok().Array);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|