Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
469 lines
23 KiB
C#
469 lines
23 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Domain.Statuses;
|
|
using PrivaPub.Federation.Rendering;
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
using System.Net;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json.Nodes;
|
|
|
|
using static PrivaPub.Tests.Support.Host.FederationHelpers;
|
|
|
|
namespace PrivaPub.Tests.Http
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class FederationGetTests : IAsyncLifetime
|
|
{
|
|
const string Base = PrivaPubHost.Base;
|
|
|
|
PrivaPubHost _host;
|
|
HttpClient _client;
|
|
Peer _peer;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_host = await PrivaPubHost.Shared();
|
|
_client = _host.Client();
|
|
_peer = await Peer.Start();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
_client?.Dispose();
|
|
if (_peer != default)
|
|
await _peer.DisposeAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_actor_document_is_activity_json_with_an_spki_key_on_the_actor()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "actor");
|
|
var avatar = await DB.Default.Find<Avatar>().MatchID(persona.Id).ExecuteSingleAsync(TestContext.Current.CancellationToken);
|
|
|
|
var fetched = await _client.Fetch($"/peasants/{persona.UserName}");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, fetched.Status);
|
|
Assert.Equal(ActivityJson, fetched.MediaType);
|
|
Assert.Contains("Accept", fetched.Response.Headers.Vary);
|
|
var actor = fetched.Json;
|
|
var id = persona.ActorUri();
|
|
Assert.Equal(id, actor["id"]!.GetValue<string>());
|
|
Assert.Equal("Person", actor["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.UserName, actor["preferredUsername"]!.GetValue<string>());
|
|
Assert.Equal(id + "/mouth", actor["inbox"]!.GetValue<string>());
|
|
Assert.Equal(id + "/anus", actor["outbox"]!.GetValue<string>());
|
|
Assert.Equal(id + "/groupies", actor["followers"]!.GetValue<string>());
|
|
Assert.Equal(id + "/stalking", actor["following"]!.GetValue<string>());
|
|
Assert.Equal(id + "/trophies", actor["featured"]!.GetValue<string>());
|
|
Assert.Equal(id + "/tattoos", actor["featuredTags"]!.GetValue<string>());
|
|
Assert.Equal($"{Base}/@{persona.UserName}", actor["url"]!.GetValue<string>());
|
|
Assert.Equal(Base + "/human-centipede", actor["endpoints"]!["sharedInbox"]!.GetValue<string>());
|
|
|
|
var key = actor["publicKey"]!;
|
|
Assert.Equal(id + "#main-key", key["id"]!.GetValue<string>());
|
|
Assert.Equal(id, key["owner"]!.GetValue<string>());
|
|
var pem = key["publicKeyPem"]!.GetValue<string>();
|
|
Assert.StartsWith("-----BEGIN PUBLIC KEY-----", pem);
|
|
using var rsa = RSA.Create();
|
|
rsa.ImportFromPem(pem);
|
|
Assert.True(rsa.KeySize >= 2048);
|
|
|
|
Assert.Equal(ActivityPubRenderer.Day(avatar.PublishedOn), actor["published"]!.GetValue<string>());
|
|
Assert.EndsWith("T00:00:00Z", actor["published"]!.GetValue<string>());
|
|
Assert.DoesNotContain(Keys(actor), k => k.Contains("created", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(ActivityPubRenderer.Timestamp(avatar.CreatedAt)[..16], fetched.Text);
|
|
Assert.DoesNotContain(persona.Root.Id, fetched.Text);
|
|
Assert.DoesNotContain(persona.Root.UserName, fetched.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_actor_answers_ld_json_and_sends_browsers_to_the_profile_page()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "browser");
|
|
|
|
var ld = await _client.Fetch($"/peasants/{persona.UserName}", LdJson);
|
|
var browser = await _client.Fetch($"/peasants/{persona.UserName}", Browser);
|
|
var instance = await _client.Fetch("/peasants/privapub", Browser);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, ld.Status);
|
|
Assert.Equal(persona.ActorUri(), ld.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(HttpStatusCode.Redirect, browser.Status);
|
|
Assert.Equal($"{Base}/@{persona.UserName}", browser.Location);
|
|
Assert.Contains("Accept", browser.Response.Headers.Vary);
|
|
Assert.Equal(HttpStatusCode.OK, instance.Status);
|
|
Assert.Equal("Application", instance.Json["type"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Users_moves_permanently_to_peasants_and_unknown_actors_are_404()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "users");
|
|
|
|
var moved = await _client.Fetch($"/users/{persona.UserName}");
|
|
var unknown = await _client.Fetch($"/peasants/nobody{Guid.NewGuid():N}"[..28]);
|
|
|
|
Assert.Equal(HttpStatusCode.MovedPermanently, moved.Status);
|
|
Assert.Equal($"/peasants/{persona.UserName}", moved.Location);
|
|
Assert.Equal(HttpStatusCode.NotFound, unknown.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_outbox_pages_public_posts_and_boosts_and_nothing_else()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "outbox");
|
|
var friend = await _host.Persona(await _host.SignUp(), "friend");
|
|
var boosted = await _host.Publish(friend, "worth a boost");
|
|
var shown = new List<Post>();
|
|
for (var i = 0; i < 20; i++)
|
|
shown.Add(await _host.Publish(persona, $"public number {i}"));
|
|
shown.Add(await _host.Publish(persona, "quietly public", PostVisibility.Unlisted));
|
|
var hidden = new List<Post>
|
|
{
|
|
await _host.Publish(persona, "for followers", PostVisibility.FollowersOnly),
|
|
await _host.Publish(persona, $"@{friend.UserName} just you", PostVisibility.Direct),
|
|
await _host.PublishLocated(persona, "only around here"),
|
|
await _host.FederatedCopy(persona)
|
|
};
|
|
var deleted = await _host.Publish(persona, "regretted");
|
|
await _host.Remove(persona, deleted);
|
|
hidden.Add(deleted);
|
|
var reblog = await _host.Reblog(persona, boosted);
|
|
Assert.Equal(PostVisibility.LocalGeo, hidden[2].Visibility);
|
|
|
|
var outbox = await _client.Fetch($"/peasants/{persona.UserName}/anus");
|
|
Assert.Equal(HttpStatusCode.OK, outbox.Status);
|
|
Assert.Equal(ActivityJson, outbox.MediaType);
|
|
Assert.Equal("OrderedCollection", outbox.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri() + "/anus", outbox.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(22, outbox.Json["totalItems"]!.GetValue<int>());
|
|
Assert.Null(outbox.Json["orderedItems"]);
|
|
var first = outbox.Json["first"]!.GetValue<string>();
|
|
Assert.Equal(persona.ActorUri() + "/anus?page=true", first);
|
|
|
|
var page1 = await _client.Fetch(PathOf(first));
|
|
Assert.Equal(HttpStatusCode.OK, page1.Status);
|
|
Assert.Equal("OrderedCollectionPage", page1.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri() + "/anus", page1.Json["partOf"]!.GetValue<string>());
|
|
var items1 = page1.Json["orderedItems"]!.AsArray().Select(i => i!.AsObject()).ToList();
|
|
Assert.Equal(20, items1.Count);
|
|
var next = page1.Json["next"]!.GetValue<string>();
|
|
Assert.StartsWith(persona.ActorUri() + "/anus?page=true&max_id=", next);
|
|
|
|
var page2 = await _client.Fetch(PathOf(next));
|
|
Assert.Equal(HttpStatusCode.OK, page2.Status);
|
|
Assert.Equal(next, page2.Json["id"]!.GetValue<string>());
|
|
var items2 = page2.Json["orderedItems"]!.AsArray().Select(i => i!.AsObject()).ToList();
|
|
Assert.Equal(2, items2.Count);
|
|
Assert.Null(page2.Json["next"]);
|
|
|
|
var items = items1.Concat(items2).ToList();
|
|
Assert.Equal(22, items.Select(i => i["id"]!.GetValue<string>()).Distinct().Count());
|
|
var announce = items[0];
|
|
Assert.Equal("Announce", announce["type"]!.GetValue<string>());
|
|
Assert.Equal(reblog.ObjectURI, announce["id"]!.GetValue<string>());
|
|
Assert.Equal($"{persona.ActorUri()}/grunts/announce-{boosted.ID}", announce["id"]!.GetValue<string>());
|
|
Assert.Equal(boosted.ObjectURI, announce["object"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri(), announce["actor"]!.GetValue<string>());
|
|
|
|
var creates = items.Skip(1).ToList();
|
|
Assert.All(creates, c =>
|
|
{
|
|
Assert.Equal("Create", c["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri(), c["actor"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri(), c["object"]!["attributedTo"]!.GetValue<string>());
|
|
});
|
|
Assert.Equal(shown.Select(p => p.ObjectURI).Reverse(), creates.Select(c => c["object"]!["id"]!.GetValue<string>()));
|
|
var everything = page1.Text + page2.Text;
|
|
foreach (var post in hidden)
|
|
Assert.DoesNotContain(post.ID, everything);
|
|
|
|
var junk = await _client.Fetch($"/peasants/{persona.UserName}/anus?page=true&max_id=not-an-id");
|
|
Assert.True((int)junk.Status < 500, $"a junk max_id answered {(int)junk.Status}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Followers_and_following_give_no_names_away()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var persona = await _host.Persona(await _host.SignUp(), "groupies");
|
|
var friend = await _host.Persona(await _host.SignUp(), "stalked");
|
|
var accepted = $"https://remote.example/users/fan{Guid.NewGuid():N}";
|
|
var pending = $"https://remote.example/users/hopeful{Guid.NewGuid():N}";
|
|
await DB.Default.SaveAsync(new[]
|
|
{
|
|
new Follower { LocalActorId = persona.Id, LocalActorKind = LocalActorKind.Person, ActorURI = accepted, InboxURL = accepted + "/inbox" },
|
|
new Follower { LocalActorId = persona.Id, LocalActorKind = LocalActorKind.Person, ActorURI = pending, InboxURL = pending + "/inbox", IsAccepted = false }
|
|
}, token);
|
|
await DB.Default.SaveAsync(new Following
|
|
{
|
|
AvatarId = persona.Id,
|
|
TargetAccountId = friend.Id,
|
|
TargetActorURI = friend.ActorUri(),
|
|
TargetIsLocal = true,
|
|
State = FollowState.Accepted
|
|
}, token);
|
|
|
|
var followers = await _client.Fetch($"/peasants/{persona.UserName}/groupies");
|
|
var following = await _client.Fetch($"/peasants/{persona.UserName}/stalking");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, followers.Status);
|
|
Assert.Equal("OrderedCollection", followers.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri() + "/groupies", followers.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(1, followers.Json["totalItems"]!.GetValue<int>());
|
|
Assert.DoesNotContain(accepted, followers.Text);
|
|
Assert.DoesNotContain(pending, followers.Text);
|
|
Assert.Equal(HttpStatusCode.OK, following.Status);
|
|
Assert.Equal(persona.ActorUri() + "/stalking", following.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(0, following.Json["totalItems"]!.GetValue<int>());
|
|
Assert.DoesNotContain(friend.UserName, following.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Trophies_are_the_public_pins_newest_first_and_tattoos_is_empty()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var persona = await _host.Persona(await _host.SignUp(), "trophies");
|
|
var older = await _host.Publish(persona, "pinned first");
|
|
var newer = await _host.Publish(persona, "pinned second");
|
|
var followersOnly = await _host.Publish(persona, "pinned but private", PostVisibility.FollowersOnly);
|
|
var removed = await _host.Publish(persona, "pinned then deleted");
|
|
foreach (var post in new[] { older, followersOnly, removed, newer })
|
|
await DB.Default.SaveAsync(new Pin { AvatarId = persona.Id, PostId = post.ID }, token);
|
|
await _host.Remove(persona, removed);
|
|
|
|
var featured = await _client.Fetch($"/peasants/{persona.UserName}/trophies");
|
|
var tags = await _client.Fetch($"/peasants/{persona.UserName}/tattoos");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, featured.Status);
|
|
Assert.Equal(persona.ActorUri() + "/trophies", featured.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(2, featured.Json["totalItems"]!.GetValue<int>());
|
|
var notes = featured.Json["orderedItems"]!.AsArray();
|
|
Assert.Equal(new[] { newer.ObjectURI, older.ObjectURI }, notes.Select(n => n!["id"]!.GetValue<string>()));
|
|
Assert.All(notes, n => Assert.Equal("Note", n!["type"]!.GetValue<string>()));
|
|
Assert.DoesNotContain(followersOnly.ID, featured.Text);
|
|
Assert.DoesNotContain(removed.ID, featured.Text);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, tags.Status);
|
|
Assert.Equal(persona.ActorUri() + "/tattoos", tags.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(0, tags.Json["totalItems"]!.GetValue<int>());
|
|
Assert.Empty(tags.Json["orderedItems"]!.AsArray());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_scribble_is_served_while_public_and_a_tombstone_once_deleted()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "scribble");
|
|
var other = await _host.Persona(await _host.SignUp(), "other");
|
|
var shown = await _host.Publish(persona, "hello fediverse");
|
|
var unlisted = await _host.Publish(persona, "hello quietly", PostVisibility.Unlisted);
|
|
var followersOnly = await _host.Publish(persona, "hello followers", PostVisibility.FollowersOnly);
|
|
var direct = await _host.Publish(persona, $"@{other.UserName} hello you", PostVisibility.Direct);
|
|
var located = await _host.PublishLocated(persona, "hello neighbours");
|
|
var deleted = await _host.Publish(persona, "goodbye");
|
|
await _host.Remove(persona, deleted);
|
|
string Path(Post post) => $"/peasants/{persona.UserName}/scribbles/{post.ID}";
|
|
|
|
var note = await _client.Fetch(Path(shown));
|
|
Assert.Equal(HttpStatusCode.OK, note.Status);
|
|
Assert.Equal(ActivityJson, note.MediaType);
|
|
Assert.Equal(shown.ObjectURI, note.Json["id"]!.GetValue<string>());
|
|
Assert.Equal("Note", note.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(persona.ActorUri(), note.Json["attributedTo"]!.GetValue<string>());
|
|
Assert.NotNull(note.Json["@context"]);
|
|
Assert.Contains(ActivityPubRenderer.Public, Strings(note.Json["to"]));
|
|
Assert.Equal(HttpStatusCode.OK, (await _client.Fetch(Path(unlisted))).Status);
|
|
|
|
var browser = await _client.Fetch(Path(shown), Browser);
|
|
Assert.Equal(HttpStatusCode.Redirect, browser.Status);
|
|
Assert.Equal($"{Base}/@{persona.UserName}/{shown.ID}", browser.Location);
|
|
|
|
foreach (var post in new[] { followersOnly, direct, located })
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(Path(post))).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{other.UserName}/scribbles/{shown.ID}")).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{persona.UserName}/scribbles/{Guid.NewGuid():N}"[..48])).Status);
|
|
|
|
var tombstone = await _client.Fetch(Path(deleted));
|
|
Assert.Equal(HttpStatusCode.Gone, tombstone.Status);
|
|
Assert.Equal(ActivityJson, tombstone.MediaType);
|
|
Assert.Equal("Tombstone", tombstone.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(deleted.ObjectURI, tombstone.Json["id"]!.GetValue<string>());
|
|
Assert.Equal("Note", tombstone.Json["formerType"]!.GetValue<string>());
|
|
Assert.NotNull(tombstone.Json["deleted"]);
|
|
Assert.DoesNotContain("goodbye", tombstone.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_circle_post_is_served_only_to_a_member_or_its_instance_actor()
|
|
{
|
|
var owner = await _host.Persona(await _host.SignUp(), "circleowner");
|
|
var circle = await _host.FederatedGroup(owner, community: false);
|
|
var member = new RemoteActor(_peer, "member");
|
|
var memberServer = new RemoteActor(_peer, "instance", type: "Application");
|
|
var neighbour = new RemoteActor(_peer, "neighbour");
|
|
var stranger = new RemoteActor(_peer, "stranger", _peer.B);
|
|
var strangerServer = new RemoteActor(_peer, "instance", _peer.B, "Application");
|
|
await circle.AddForeignMember(member.Id);
|
|
var post = await _host.Publish(owner, new StatusDraft { Text = "just us", PlainText = true, GroupId = circle.Id });
|
|
Assert.Equal(PostVisibility.Circle, post.Visibility);
|
|
var path = $"/peasants/{owner.UserName}/scribbles/{post.ID}";
|
|
|
|
var asMember = await _client.Fetch(member.SignedGet(path));
|
|
var asMemberServer = await _client.Fetch(memberServer.SignedGet(path));
|
|
|
|
Assert.Equal(HttpStatusCode.OK, asMember.Status);
|
|
Assert.Equal(post.ObjectURI, asMember.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(new[] { circle.Uri, circle.Uri + "/flock" }, Strings(asMember.Json["to"]));
|
|
Assert.DoesNotContain(ActivityPubRenderer.Public, asMember.Text);
|
|
Assert.Equal(HttpStatusCode.OK, asMemberServer.Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(path)).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(path, Browser)).Status);
|
|
foreach (var outsider in new[] { neighbour, stranger, strangerServer })
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(outsider.SignedGet(path))).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{owner.UserName}/grunts/create-{post.ID}")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_circles_followers_members_and_wardens_are_shown_only_to_its_members()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var owner = await _host.Persona(await _host.SignUp(), "circlelists");
|
|
var circle = await _host.FederatedGroup(owner, community: false);
|
|
var community = await _host.FederatedGroup(owner, community: true);
|
|
var member = new RemoteActor(_peer, "member");
|
|
var stranger = new RemoteActor(_peer, "stranger", _peer.B);
|
|
await circle.AddForeignMember(member.Id);
|
|
foreach (var group in new[] { circle, community })
|
|
await DB.Default.SaveAsync(new Follower
|
|
{
|
|
LocalActorId = group.Id,
|
|
LocalActorKind = LocalActorKind.Group,
|
|
ActorURI = member.Id,
|
|
InboxURL = member.Id + "/inbox"
|
|
}, token);
|
|
|
|
foreach (var collection in new[] { "groupies", "flock", "wardens" })
|
|
{
|
|
var path = $"/peasants/{circle.UserName}/{collection}";
|
|
Assert.True((await _client.Fetch(path)).Status == HttpStatusCode.NotFound, $"the circle's {collection} answered an unsigned GET");
|
|
Assert.True((await _client.Fetch(stranger.SignedGet(path))).Status == HttpStatusCode.NotFound, $"the circle's {collection} answered a stranger");
|
|
var asMember = await _client.Fetch(member.SignedGet(path));
|
|
Assert.True(asMember.Status == HttpStatusCode.OK, $"the circle's {collection} refused a member");
|
|
Assert.Equal(circle.Uri + "/" + collection, asMember.Json["id"]!.GetValue<string>());
|
|
var open = await _client.Fetch($"/peasants/{community.UserName}/{collection}");
|
|
Assert.True(open.Status == HttpStatusCode.OK, $"the community's {collection} answered {(int)open.Status}");
|
|
}
|
|
Assert.Equal(1, (await _client.Fetch(member.SignedGet($"/peasants/{circle.UserName}/groupies"))).Json["totalItems"]!.GetValue<int>());
|
|
Assert.Equal(2, (await _client.Fetch(member.SignedGet($"/peasants/{circle.UserName}/flock"))).Json["totalItems"]!.GetValue<int>());
|
|
var wardens = await _client.Fetch($"/peasants/{community.UserName}/wardens");
|
|
Assert.Equal(new[] { owner.ActorUri() }, Strings(wardens.Json["orderedItems"]));
|
|
Assert.Equal(community.Uri + "/wardens", (await _client.Fetch($"/peasants/{community.UserName}")).Json["attributedTo"]!.GetValue<string>());
|
|
Assert.Null((await _client.Fetch($"/peasants/{circle.UserName}")).Json["attributedTo"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Grunts_resolve_creates_and_boosts()
|
|
{
|
|
var persona = await _host.Persona(await _host.SignUp(), "grunts");
|
|
var friend = await _host.Persona(await _host.SignUp(), "boosted");
|
|
var post = await _host.Publish(persona, "a grunt");
|
|
var followersOnly = await _host.Publish(persona, "a private grunt", PostVisibility.FollowersOnly);
|
|
var original = await _host.Publish(friend, "boost me");
|
|
var reblog = await _host.Reblog(persona, original);
|
|
string Path(string id) => $"/peasants/{persona.UserName}/grunts/{id}";
|
|
|
|
var create = await _client.Fetch(Path($"create-{post.ID}"));
|
|
var announce = await _client.Fetch(Path($"announce-{original.ID}"));
|
|
|
|
Assert.Equal(HttpStatusCode.OK, create.Status);
|
|
Assert.Equal(ActivityJson, create.MediaType);
|
|
Assert.Equal("Create", create.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(Base + Path($"create-{post.ID}"), create.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(post.ObjectURI, create.Json["object"]!["id"]!.GetValue<string>());
|
|
Assert.Equal(post.ActivityURI, create.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(HttpStatusCode.OK, announce.Status);
|
|
Assert.Equal("Announce", announce.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(reblog.ObjectURI, announce.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(original.ObjectURI, announce.Json["object"]!.GetValue<string>());
|
|
|
|
foreach (var unknown in new[] { $"create-{followersOnly.ID}", $"create-{Guid.NewGuid():N}"[..31], $"announce-{post.ID}", $"like-{post.ID}", "x" })
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch(Path(unknown))).Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await _client.Fetch($"/peasants/{friend.UserName}/grunts/announce-{original.ID}")).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_parrot_licence_is_served_by_its_author_until_revoked()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var quoted = await _host.Persona(await _host.SignUp(), "quoted");
|
|
var quoter = await _host.Persona(await _host.SignUp(), "quoter");
|
|
var original = await _host.Publish(quoted, "quote me");
|
|
var quoting = await _host.Publish(quoter, new StatusDraft { Text = "well said", PlainText = true, QuotedStatusId = original.ID });
|
|
Assert.Equal(QuoteState.Accepted, quoting.QuoteState);
|
|
var licenceUri = quoting.QuoteAuthorizationURI;
|
|
Assert.StartsWith(quoted.ActorUri() + "/parrot-licences/", licenceUri);
|
|
var licenceId = licenceUri.Split('/')[^1];
|
|
|
|
var licence = await _client.Fetch(PathOf(licenceUri));
|
|
var wrongAuthor = await _client.Fetch($"/peasants/{quoter.UserName}/parrot-licences/{licenceId}");
|
|
var unknown = await _client.Fetch($"/peasants/{quoted.UserName}/parrot-licences/{Guid.NewGuid():N}"[..48]);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, licence.Status);
|
|
Assert.Equal(ActivityJson, licence.MediaType);
|
|
Assert.Equal("QuoteAuthorization", licence.Json["type"]!.GetValue<string>());
|
|
Assert.Equal(licenceUri, licence.Json["id"]!.GetValue<string>());
|
|
Assert.Equal(quoted.ActorUri(), licence.Json["attributedTo"]!.GetValue<string>());
|
|
Assert.Equal(quoting.ObjectURI, licence.Json["interactingObject"]!.GetValue<string>());
|
|
Assert.Equal(original.ObjectURI, licence.Json["interactionTarget"]!.GetValue<string>());
|
|
Assert.Equal(HttpStatusCode.NotFound, wrongAuthor.Status);
|
|
Assert.Equal(HttpStatusCode.NotFound, unknown.Status);
|
|
|
|
Assert.True(await _host.Get<IQuoteService>().RevokeLicence(original, quoting, token));
|
|
var revoked = await _client.Fetch(PathOf(licenceUri));
|
|
|
|
Assert.Equal(HttpStatusCode.Gone, revoked.Status);
|
|
Assert.Equal(ActivityJson, revoked.MediaType);
|
|
Assert.Equal("Tombstone", revoked.Json["type"]!.GetValue<string>());
|
|
Assert.Equal("QuoteAuthorization", revoked.Json["formerType"]!.GetValue<string>());
|
|
Assert.Equal(licenceUri, revoked.Json["id"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Secure_mode_refuses_unsigned_gets_except_for_the_instance_actor()
|
|
{
|
|
var secure = await SecureModeHost.Shared();
|
|
using var client = secure.Client();
|
|
var persona = await _host.Persona(await _host.SignUp(), "secure");
|
|
var post = await _host.Publish(persona, "only for the signed");
|
|
var reader = new RemoteActor(_peer, "reader");
|
|
var paths = new[]
|
|
{
|
|
$"/peasants/{persona.UserName}",
|
|
$"/peasants/{persona.UserName}/anus",
|
|
$"/peasants/{persona.UserName}/anus?page=true",
|
|
$"/peasants/{persona.UserName}/groupies",
|
|
$"/peasants/{persona.UserName}/stalking",
|
|
$"/peasants/{persona.UserName}/trophies",
|
|
$"/peasants/{persona.UserName}/tattoos",
|
|
$"/peasants/{persona.UserName}/scribbles/{post.ID}",
|
|
$"/peasants/{persona.UserName}/grunts/create-{post.ID}",
|
|
$"/peasants/nobody{Guid.NewGuid():N}"[..28]
|
|
};
|
|
|
|
foreach (var path in paths)
|
|
Assert.True((await client.Fetch(path)).Status == HttpStatusCode.Unauthorized, $"{path} answered an unsigned GET");
|
|
var instance = await client.Fetch("/peasants/privapub");
|
|
Assert.Equal(HttpStatusCode.OK, instance.Status);
|
|
Assert.Equal("Application", instance.Json["type"]!.GetValue<string>());
|
|
foreach (var path in paths[..^1])
|
|
Assert.True((await client.Fetch(reader.SignedGet(path))).Status == HttpStatusCode.OK, $"{path} refused a signed GET");
|
|
}
|
|
}
|
|
}
|