using PrivaPub.Domain.Statuses; using PrivaPub.Models.Post; using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; using System.Net; using System.Text.Json.Nodes; using static PrivaPub.Tests.Support.Host.FederationHelpers; namespace PrivaPub.Tests.Http { [Trait("Category", "Integration")] public sealed class WellKnownTests : IAsyncLifetime { const string Base = PrivaPubHost.Base; const string Domain = PrivaPubHost.Host; PrivaPubHost _host; HttpClient _client; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); _client = _host.Client(); } public ValueTask DisposeAsync() { _client?.Dispose(); return ValueTask.CompletedTask; } Task WebFinger(string resource) => _client.Fetch("/.well-known/webfinger" + (resource == default ? string.Empty : "?resource=" + Uri.EscapeDataString(resource)), "application/jrd+json"); static string Link(JsonObject document, string rel) => document["links"]!.AsArray().Single(l => l!["rel"]!.GetValue() == rel)!["href"]!.GetValue(); [Fact] public async Task WebFinger_finds_a_persona_by_acct_and_by_actor_uri() { var persona = await _host.Persona(await _host.SignUp(), "finger"); var resources = new[] { $"acct:{persona.UserName}@{Domain}", $"acct:@{persona.UserName}@{Domain}", $"acct:{persona.UserName.ToUpperInvariant()}@{Domain.ToUpperInvariant()}", $"{persona.UserName}@{Domain}", $"@{persona.UserName}@{Domain}", persona.ActorUri() }; foreach (var resource in resources) { var found = await WebFinger(resource); Assert.True(found.Status == HttpStatusCode.OK, $"{resource} answered {(int)found.Status}"); Assert.Equal("application/jrd+json", found.MediaType); var document = found.Json; Assert.Equal($"acct:{persona.UserName}@{Domain}", document["subject"]!.GetValue()); Assert.Equal(new[] { $"{Base}/@{persona.UserName}", persona.ActorUri() }, Strings(document["aliases"])); Assert.Equal(persona.ActorUri(), Link(document, "self")); Assert.Equal("application/activity+json", document["links"]!.AsArray().Single(l => l!["rel"]!.GetValue() == "self")!["type"]!.GetValue()); Assert.Equal($"{Base}/@{persona.UserName}", Link(document, "http://webfinger.net/rel/profile-page")); Assert.DoesNotContain(persona.Root.Id, found.Text); Assert.DoesNotContain(persona.Root.UserName, found.Text); } } [Fact] public async Task WebFinger_refuses_other_domains_unknown_names_and_a_missing_resource() { var persona = await _host.Persona(await _host.SignUp(), "nofinger"); Assert.Equal(HttpStatusCode.NotFound, (await WebFinger($"acct:{persona.UserName}@elsewhere.example")).Status); Assert.Equal(HttpStatusCode.NotFound, (await WebFinger($"acct:{persona.UserName}")).Status); Assert.Equal(HttpStatusCode.NotFound, (await WebFinger($"acct:nobody{Guid.NewGuid():N}"[..20] + "@" + Domain)).Status); Assert.Equal(HttpStatusCode.NotFound, (await WebFinger($"https://elsewhere.example/peasants/{persona.UserName}")).Status); Assert.Equal(HttpStatusCode.NotFound, (await WebFinger($"acct:{persona.Root.UserName}@{Domain}")).Status); Assert.Equal(HttpStatusCode.BadRequest, (await WebFinger(default)).Status); Assert.Equal(HttpStatusCode.BadRequest, (await WebFinger(string.Empty)).Status); } [Fact] public async Task WebFinger_finds_the_instance_actor_and_a_community() { var owner = await _host.Persona(await _host.SignUp(), "fingerowner"); var community = await _host.Group(owner, community: true); var instance = await WebFinger($"acct:privapub@{Domain}"); var group = await WebFinger($"acct:{community.UserName}@{Domain}"); Assert.Equal(HttpStatusCode.OK, instance.Status); Assert.Equal($"acct:privapub@{Domain}", instance.Json["subject"]!.GetValue()); Assert.Equal($"{Base}/peasants/privapub", Link(instance.Json, "self")); Assert.Single(instance.Json["links"]!.AsArray()); Assert.Equal(HttpStatusCode.OK, group.Status); Assert.Equal($"acct:{community.UserName}@{Domain}", group.Json["subject"]!.GetValue()); Assert.Equal(community.Uri, Link(group.Json, "self")); Assert.DoesNotContain(owner.UserName, group.Text); Assert.DoesNotContain(owner.Id, group.Text); } // Current behaviour, for the owner to decide: WebFinger answers for a circle, because Mastodon resolves a handle // through WebFinger before it can send the Follow a circle takes members through, while /api/v1/accounts/lookup, // search and the profile page do not show circles at all. [Fact] public async Task WebFinger_answers_for_a_circle() { var owner = await _host.Persona(await _host.SignUp(), "circlefinger"); var circle = await _host.Group(owner, community: false); var found = await WebFinger($"acct:{circle.UserName}@{Domain}"); Assert.Equal(HttpStatusCode.OK, found.Status); Assert.Equal(circle.Uri, Link(found.Json, "self")); Assert.DoesNotContain(owner.UserName, found.Text); } [Fact] public async Task NodeInfo_is_linked_and_served_in_both_versions_without_naming_anyone() { var root = await _host.SignUp(); var persona = await _host.Persona(root, "nodeinfo"); await _host.Publish(persona, "counted"); var links = await _client.Fetch("/.well-known/nodeinfo", "application/json"); Assert.Equal(HttpStatusCode.OK, links.Status); Assert.Equal("application/json", links.MediaType); var hrefs = links.Json["links"]!.AsArray().ToDictionary(l => l!["rel"]!.GetValue(), l => l!["href"]!.GetValue()); Assert.Equal($"{Base}/nodeinfo/2.1", hrefs["http://nodeinfo.diaspora.software/ns/schema/2.1"]); Assert.Equal($"{Base}/nodeinfo/2.0", hrefs["http://nodeinfo.diaspora.software/ns/schema/2.0"]); foreach (var version in new[] { "2.0", "2.1" }) { var info = await _client.Fetch(PathOf(hrefs[$"http://nodeinfo.diaspora.software/ns/schema/{version}"]), "application/json"); Assert.Equal(HttpStatusCode.OK, info.Status); Assert.Equal("application/json", info.MediaType); Assert.Contains($"profile=\"http://nodeinfo.diaspora.software/ns/schema/{version}#\"", info.Response.Content.Headers.ContentType!.ToString()); var document = info.Json; Assert.Equal(version, document["version"]!.GetValue()); Assert.Equal("privapub", document["software"]!["name"]!.GetValue()); Assert.Equal(version == "2.1", document["software"]!["repository"] != default); Assert.Equal(new[] { "activitypub" }, Strings(document["protocols"])); Assert.True(document["usage"]!["users"]!["total"]!.GetValue() >= 1); Assert.True(document["usage"]!["localPosts"]!.GetValue() >= 1); Assert.DoesNotContain(root.Id, info.Text); Assert.DoesNotContain(root.UserName, info.Text); Assert.DoesNotContain(persona.UserName, info.Text); } } [Fact] public async Task Unknown_nodeinfo_versions_are_404() { foreach (var version in new[] { "1.0", "2.2", "3.0", "2.0.json", "2" }) Assert.True((await _client.Fetch($"/nodeinfo/{version}", "application/json")).Status == HttpStatusCode.NotFound, $"/nodeinfo/{version} was served"); } } [Trait("Category", "Integration")] [Collection(nameof(Exclusive))] public sealed class NodeInfoUsageTests : IAsyncLifetime { PrivaPubHost _host; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; async Task LocalPosts() { using var client = _host.Client(); return (await client.Fetch("/nodeinfo/2.1", "application/json")).Json["usage"]!["localPosts"]!.GetValue(); } [Fact] public async Task Local_posts_count_only_what_anyone_may_see() { var persona = await _host.Persona(await _host.SignUp(), "usage"); var friend = await _host.Persona(await _host.SignUp(), "usagefriend"); var start = await LocalPosts(); var shown = await _host.Publish(persona, "counted"); await _host.Publish(persona, "counted too", PostVisibility.Unlisted); Assert.Equal(start + 2, await LocalPosts()); await _host.Publish(persona, "for followers", PostVisibility.FollowersOnly); await _host.Publish(persona, $"@{friend.UserName} between us", PostVisibility.Direct); await _host.PublishLocated(persona, "around here"); await _host.FederatedCopy(persona); await _host.Reblog(friend, shown); var circle = await _host.Group(persona, community: false); await _host.Publish(persona, new StatusDraft { Text = "circle only", PlainText = true, GroupId = circle.Id }); Assert.Equal(start + 2, await LocalPosts()); await _host.Remove(persona, shown); Assert.Equal(start + 1, await LocalPosts()); } } }