using PrivaPub.Tests.Support; using PrivaPub.Tests.Support.Host; using System.Net; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Http { // a public thread names its replies and its conversation (owner decision 2026-10-06), and they list only what is public // or unlisted; a followers-only post names neither [Trait("Category", "Integration")] public sealed class ThreadCollectionsTests : IAsyncLifetime { PrivaPubHost _host; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _host = await PrivaPubHost.Shared(); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; static string PathOf(JsonObject status) => new Uri(status.Text("uri")).AbsolutePath; static string[] Items(JsonNode collection) => collection["orderedItems"]!.AsArray().Select(i => i!.GetValue()).ToArray(); [Fact] public async Task A_public_post_names_its_replies_and_context_which_list_only_public_answers() { var alice = await _host.Mastodon("alice"); var carol = await _host.Mastodon("carol"); var root = await alice.Status("a public question"); var answer = await carol.Status("a public answer", ("in_reply_to_id", root.Text("id"))); var hidden = await carol.Status("for my followers", ("in_reply_to_id", root.Text("id")), ("visibility", "private")); var deeper = await alice.Status("and a deeper one", ("in_reply_to_id", answer.Text("id")), ("visibility", "unlisted")); var note = (await _host.ActivityPub(PathOf(root))).Ok().Object; Assert.Equal(root.Text("uri") + "/replies", note.Text("replies")); Assert.Equal(root.Text("uri") + "/context", note.Text("context")); var answerNote = (await _host.ActivityPub(PathOf(deeper))).Ok().Object; Assert.Equal(root.Text("uri") + "/context", answerNote.Text("context")); var replies = (await _host.ActivityPub(PathOf(root) + "/replies")).Ok().Object; Assert.Equal(new[] { answer.Text("uri") }, Items(replies)); var context = (await _host.ActivityPub(PathOf(root) + "/context")).Ok().Object; Assert.Equal(new[] { root.Text("uri"), answer.Text("uri"), deeper.Text("uri") }, Items(context)); Assert.Equal(alice.Uri, context.Text("attributedTo")); Assert.DoesNotContain(hidden.Text("uri"), Items(context)); } [Fact] public async Task A_followers_only_post_names_neither_and_serves_neither() { var alice = await _host.Mastodon("alice"); var quiet = await alice.Status("for my followers", ("visibility", "private")); Assert.Equal(HttpStatusCode.NotFound, (await _host.ActivityPub(PathOf(quiet) + "/replies")).Status); Assert.Equal(HttpStatusCode.NotFound, (await _host.ActivityPub(PathOf(quiet) + "/context")).Status); } } }