Skip to content
quarry Artifact [66870f6888]
Login

Artifact [66870f6888]

Artifact 66870f6888b495d5728078f7c793e0c24ef7bc9b962a4868f98d60f074229072:


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)
	}
}