Owner decision of 2026-10-06: a persona's public or unlisted post outside any group names its replies
(…/scribbles/{id}/replies) and its conversation's context (FEP-7888), the root's …/context that a reply inherits from
its parent, ours or another server's. Both list only the public and unlisted posts PrivaPub holds; followers-only,
circle, direct and local-only posts name neither and their collections answer 404. Checked live: opening alice's
thread on Mastodon finds carol's reply, which nobody there follows (scenarios/threads.sh).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
64 lines
2.7 KiB
C#
64 lines
2.7 KiB
C#
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<string>()).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);
|
|
}
|
|
}
|
|
}
|