package api
import (
"context"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// heldForumStub is the ForumStore surface this test needs: one author's
// own held submissions, nothing else. The embedded interface leaves
// every other method unimplemented on purpose -- if forum.list starts
// calling one, this test should fail loudly rather than quietly widen.
type heldForumStub struct {
ForumStore
staged []oltp.Staged
}
// The other two methods forum.list asks of any Forum it is given. They
// answer empty, so the listing behaves as it does on a forum with no
// categories and no read marks. Everything else on ForumStore stays
// unimplemented on purpose: if forum.list starts calling another, this
// test panics rather than quietly passing on a wider surface.
func (s *heldForumStub) ThreadCategories(string) (map[string]string, error) { return nil, nil }
// A THIRD method, added deliberately rather than to quiet a panic: the
// listing now answers whether the forum is moderated and whether it
// would hold THIS caller's post, so it asks. The tripwire above did its
// job — it caught the widened surface on the first run — and false is
// the honest answer for a stub standing in for a forum with no
// moderation configured.
func (s *heldForumStub) ModerationEnabled(string) bool { return false }
func (s *heldForumStub) ForumRead(_, _ string, _ map[string]string) (map[string]bool, error) {
return nil, nil
}
func (s *heldForumStub) StagedMine(_, author string) ([]oltp.Staged, error) {
var out []oltp.Staged
for _, x := range s.staged {
if x.Author == author {
out = append(out, x)
}
}
return out, nil
}
// The forum listing carries the VIEWER'S OWN held submissions, and
// nobody else's.
//
// A staged post is in forum_staged and never in the DAG, and every row
// forum.list assembles comes from the DAG -- so the author's own post
// was invisible to them by construction rather than by any filter
// somebody wrote, which is why the silence was total. jva measured it
// from a non-moderator account and concluded the post had failed
// (G-118). The rule this implements is theirs: the author should always
// see their own held content and its state, because today they see LESS
// than a stranger does (G-119).
//
// Worth knowing for anyone re-measuring this by hand: TrustedAuthor is
// EffectiveCaps.HasAny("45") and Admin expands to everything but sxy,
// so an admin can never produce a held post from their own account. jva
// had to lower an account to Developer to test it at all.
func TestForumListShowsTheViewerTheirOwnHeldPosts(t *testing.T) {
forum := &heldForumStub{staged: []oltp.Staged{
{ID: 1, Kind: "post", Title: "eve's held post", Author: "eve", At: "2026-09-19T10:00:00", State: "pending"},
{ID: 2, Kind: "reply", Root: "abc", Title: "jan's held reply", Author: "jan", At: "2026-09-19T10:05:00", State: "pending"},
}}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(forumTechIndex(t)), Forum: forum}); err != nil {
t.Fatal(err)
}
// The author sees their own, with its state.
got, err := r.Invoke(ops.WithUser(context.Background(), "eve"), "forum.list", forumListIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
out := got.(forumListOut)
if len(out.Held) != 1 {
t.Fatalf("eve sees %d of her own held posts, want 1: %+v", len(out.Held), out.Held)
}
if out.Held[0].Title != "eve's held post" || out.Held[0].State != "pending" {
t.Errorf("held row = %+v, want eve's post marked pending", out.Held[0])
}
// And nobody else's. This is the half that must not become a queue
// read without the moderation letter.
for _, h := range out.Held {
if h.Title == "jan's held reply" {
t.Error("eve was shown jan's held submission: the listing is a queue read by " +
"another name")
}
}
// A signed-out reader sees none, and the field is omitted entirely.
anon, err := r.Invoke(context.Background(), "forum.list", forumListIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
if h := anon.(forumListOut).Held; len(h) != 0 {
t.Errorf("a signed-out reader was shown held submissions: %+v", h)
}
// The threads the listing already carried are untouched: this is
// additive to what the author could not see, not a filter on what
// everyone could.
if out.Count != 1 || len(out.Threads) != 1 {
t.Errorf("the ordinary listing changed: count=%d threads=%d", out.Count, len(out.Threads))
}
}