package web
import (
"net/url"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/artifact"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// A reader on the default timeline sees check-ins. Their URL carries no
// query at all to say so, and the live stream is opened with that same
// empty query — so the resolution of "no filter" to "check-ins" has to
// live somewhere both the page and the stream reach. It used to live in
// the page handler alone, and the first stream tick after a landing
// swapped their check-ins for everything.
func TestTimelineQueryDefaultsToCheckins(t *testing.T) {
vals, _ := timelineQuery(url.Values{}, "demo")
if got := vals.Get("y"); got != "ci" {
t.Fatalf("empty query resolved y=%q, want ci", got)
}
if got := vals.Get("repo"); got != "demo" {
t.Fatalf("repo=%q, want demo", got)
}
}
// The page and the stream must agree for every filter, not just the
// default one — the stream re-renders what the page rendered.
func TestTimelineQueryKeepsChosenFilter(t *testing.T) {
for _, y := range []string{"all", "ci", "w", "t", "e", "f"} {
vals, _ := timelineQuery(url.Values{"y": {y}}, "demo")
if got := vals.Get("y"); got != y {
t.Errorf("y=%q survived as %q", y, got)
}
}
vals, _ := timelineQuery(url.Values{"branch": {"trunk"}, "tag": {"rel"}, "user": {"dm"}}, "demo")
for k, want := range map[string]string{"branch": "trunk", "tag": "rel", "user": "dm"} {
if got := vals.Get(k); got != want {
t.Errorf("%s=%q, want %q", k, got, want)
}
}
}
// n= is the page's name for the op's limit, and the page size shown in
// the Max control has to be the one actually asked for. Out-of-range
// values fall back rather than reaching the op's validator.
func TestTimelineQueryMapsPageSize(t *testing.T) {
vals, limit := timelineQuery(url.Values{"n": {"100"}}, "demo")
if vals.Get("limit") != "100" || limit != 100 {
t.Fatalf("n=100 gave limit=%q/%d, want 100/100", vals.Get("limit"), limit)
}
if _, limit := timelineQuery(url.Values{}, "demo"); limit != 50 {
t.Fatalf("default limit=%d, want 50", limit)
}
if _, limit := timelineQuery(url.Values{"n": {"9000"}}, "demo"); limit != 50 {
t.Fatalf("out-of-range n gave limit=%d, want the 50 default", limit)
}
}
// timelineQuery must not write through to its caller's values: the page
// handler passes req.URL.Query() and then reads the raw query back for
// the filter controls.
func TestTimelineQueryDoesNotMutateInput(t *testing.T) {
in := url.Values{"branch": {"trunk"}}
timelineQuery(in, "demo")
if in.Get("y") != "" || in.Get("repo") != "" {
t.Fatalf("input mutated: %v", in)
}
}
// The decisive one: the live stream must re-render the view the reader
// is actually looking at. A reader on the default timeline sees
// check-ins and has no query string saying so, so this drives the same
// path the stream drives — timelineFragment with the empty query the
// page handed it — and asserts a wiki edit does not appear in it.
func TestTimelineFragmentHonoursTheDefaultView(t *testing.T) {
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
if err := ix.FoldNow([]index.Parsed{
{Hash: "c0de", Art: &artifact.Artifact{Type: artifact.Manifest,
Comment: "MARKER-CHECKIN", Date: "2026-08-15T10:00:00", User: "dan"}},
{Hash: "w1k1", Art: &artifact.Artifact{Type: artifact.Wiki,
WikiTitle: "MARKER-WIKI", Date: "2026-08-15T11:00:00", User: "dan",
Mimetype: "text/x-markdown", Body: []byte("body\n")}},
}); err != nil {
t.Fatal(err)
}
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test",
Repos: api.NewRepos(map[string]*api.Repo{"test": {Index: ix}})}); err != nil {
t.Fatal(err)
}
// Empty query: exactly what the page passes when the reader has
// touched no filter, which is the overwhelmingly common case.
frag, err := timelineFragment(&noHooks, r, "test", url.Values{})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(frag, "MARKER-CHECKIN") {
t.Errorf("default stream fragment dropped the check-in:\n%s", frag)
}
if strings.Contains(frag, "MARKER-WIKI") {
t.Errorf("default stream fragment widened to everything — the wiki edit\n" +
"leaked into a check-ins timeline, which is what a reader saw a\n" +
"few seconds after any landing")
}
// And the widening is still available when it is asked for.
all, err := timelineFragment(&noHooks, r, "test", url.Values{"y": {"all"}})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(all, "MARKER-WIKI") || !strings.Contains(all, "MARKER-CHECKIN") {
t.Errorf("y=all did not carry both:\n%s", all)
}
}