T7: the federation surface over HTTP
Tests through the real routes of PeasantsController, WellKnownController and UsersController, on the whole server under test (34 new tests): - FederationGetTests: the actor document (activity+json, SPKI key at #main-key owned by the actor, sharedInbox, published = PublishedOn's day, no creation date, no root); ld+json; browsers sent to /@name; Vary: Accept; /users 301; the outbox's totalItems and ?page=true&max_id paging across the 20-item boundary, boosts as Announces, and no followers-only, direct, located, federated-copy or deleted post; /groupies and /stalking naming nobody; /trophies (public pins, newest first) and /tattoos; /scribbles (public and unlisted 200, browsers redirected, followers-only, direct and located 404, deleted 410 Tombstone); a circle post only for a signed member or its instance actor; a circle's /groupies, /flock and /wardens only for members; /grunts create- and announce- ids; /parrot-licences 200, revoked 410, wrong author 404; secure mode's 401 for every unsigned GET but the instance actor's. - WellKnownTests: WebFinger by acct:, @-prefixed, bare, upper-case and actor URI; other domains, unknown names, a root's login name and no resource; the instance actor, a community, a circle (answered: current behaviour); NodeInfo links, 2.0 and 2.1 naming no root, unknown versions 404; usage counting only public, unlisted, non-boost local posts (Exclusive). - InboxRouteTests: all three inboxes accept a signed delivery and refuse junk (400), unsigned (401), a bad Digest, a two-hour-old Date, a signature for another host and a swapped body (401); an unknown persona's /mouth is 404; ld+json with the ActivityStreams profile is accepted; a signer whose actor answers 503 gets 503 with Retry-After; the 301st unsigned POST from one address is 429 while a signed server from it is not. - PersonaSeparationHttpTests: with a sibling persona and its community on the same login, every GET under /api (filled with the persona's ids) plus search, lookup and relationships, and a crawl of everything federation publishes about the persona (actor, outbox pages, collections, scribbles, grunts, WebFinger, NodeInfo, /@ pages), never name the sibling, its community or the login. Fixed: - A circle's /groupies told anyone how many followers (members) it has, while its /flock and /wardens were already for members only; it now answers 404 to anyone but a signed member or a member's instance actor. - WebFinger answered 404 to a bare user@domain or @user@domain resource, which Mastodon, GoToSocial and Pleroma all accept; it now treats them as acct: (noted in docs/INTEROP.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
c5e4934ba6
commit
2cfea7b60c
8 files changed
+1295
-3
No files matched your search
@@ -0,0 +1,468 @@
|
||||
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.Group(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.Group(owner, community: false);
|
||||
var community = await _host.Group(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user