package web
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
func inboxOpenForge(t *testing.T) (*attnStub, *memRead, *httptest.Server) {
t.Helper()
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
attn, read := &attnStub{}, &memRead{at: map[string]uint64{}}
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "t",
Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix, Code: "c1"}}),
Attention: attn, AttnRead: read}); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
WithSessions(fakeSessions{user: "dan"}), WithPrefs(&prefStub{})}))
t.Cleanup(srv.Close)
return attn, read, srv
}
// openRow follows no redirect: the point is where it was sent, not what
// is there when you arrive.
func openRow(t *testing.T, srv *httptest.Server, path string) (int, string) {
t.Helper()
req, err := http.NewRequest("GET", srv.URL+path, nil)
if err != nil {
t.Fatal(err)
}
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
return resp.StatusCode, resp.Header.Get("Location")
}
// Opening a row is reading it. An inbox whose count only moves from a
// separate button is an inbox whose badge means nothing.
func TestOpeningARowMarksItRead(t *testing.T) {
attn, read, srv := inboxOpenForge(t)
seq, err := attn.Attend("dan", api.AttentionEvent{Kind: "mention", Summary: "look",
At: "2026-09-08T10:00:00Z", Link: "/r/alpha/forum/abc"})
if err != nil {
t.Fatal(err)
}
if read.AttentionReadSeq("dan") >= seq {
t.Fatal("FAIL: the row started out read")
}
code, where := openRow(t, srv, "/inbox/go?id=local:1&to=%2Fr%2Falpha%2Fforum%2Fabc")
if code != 303 || where != "/r/alpha/forum/abc" {
t.Fatalf("FAIL: opening sent %d to %q, want 303 to the row's own destination", code, where)
}
if read.AttentionReadSeq("dan") < seq {
t.Errorf("FAIL: the row is still unread after being opened")
}
t.Log("ok: opening a row marks it read and lands on its destination")
}
// A row's destination comes from a workflow payload, so it is not
// quarry's to trust: a redirect that forwards anywhere is an open
// redirect wearing the forge's own domain.
func TestOpeningRefusesAForeignDestination(t *testing.T) {
_, read, srv := inboxOpenForge(t)
for _, bad := range []string{
"https%3A%2F%2Fevil.test%2Fx",
"%2F%2Fevil.test%2Fx",
"javascript%3Aalert(1)",
} {
code, where := openRow(t, srv, "/inbox/go?id=local:1&to="+bad)
if code != 303 || where != "/inbox" {
t.Errorf("FAIL: %s sent %d to %q, want the inbox", bad, code, where)
}
}
// Refusing the destination must not refuse the reading: they did
// open it, whatever the link claimed.
if read.AttentionReadSeq("dan") == 0 {
t.Error("FAIL: a refused destination also skipped the mark")
}
t.Log("ok: only this forge is a destination, and the row is read either way")
}
// The rendered row links through the marking route rather than straight out.
func TestInboxRowsLinkThroughTheMarkingRoute(t *testing.T) {
attn, _, srv := inboxOpenForge(t)
if _, err := attn.Attend("dan", api.AttentionEvent{Kind: "mention", Summary: "look",
At: "2026-09-08T10:00:00Z", Link: "/r/alpha/forum/abc"}); err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("GET", srv.URL+"/inbox", nil)
if err != nil {
t.Fatal(err)
}
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
resp, err := srv.Client().Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
body := string(raw)
if !strings.Contains(body, `href="/inbox/go?id=`) {
t.Errorf("FAIL: a row still links straight out:\n%s", body)
}
t.Log("ok: rows open through the marking route")
}