package web
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
type claimLookIn struct {
Code string `json:"code"`
}
type claimIn struct {
Code string `json:"code"`
Repos string `json:"repos"`
Role string `json:"role"`
}
type claimOut struct {
Request string `json:"request"`
}
var nonceRe = regexp.MustCompile(`nonce="[^"]*"|<span class="f-time".*</span></span>`)
func repoRows(names ...string) []setupRepo {
var out []setupRepo
for _, n := range names {
out = append(out, setupRepo{Name: n, Handle: n, Visibility: "private", Namespace: "ann"})
}
return out
}
type claimNone struct{}
type claimVersion struct {
Version string `json:"version"`
}
// claimForge is the /setup pages over stand-ins for the three ops, so
// what is tested is the pages' own reading of them.
type claimForge struct {
h http.Handler
signed bool
kind string
repos []setupRepo
// addr is the address the stub says the start came from; limited makes
// the look op answer rate-limited.
addr string
limited bool
// problem is said on the found code; refuse is the sentence a claim
// is refused with.
problem, refuse string
claimed []claimIn
discarded int
}
func newClaimForge(t *testing.T) *claimForge {
t.Helper()
f := &claimForge{signed: true, kind: "read", repos: repoRows("ctx", "suit"), addr: "198.51.100.4"}
r := ops.NewRegistry()
// The page chrome asks the forge which build it is.
if err := ops.Register(r, ops.Op[claimNone, claimVersion]{Name: "meta.version", Doc: "d", Kind: ops.Query, Auth: ops.Anonymous, Route: "GET /api/v1/version",
Run: func(context.Context, claimNone) (claimVersion, error) { return claimVersion{Version: "t"}, nil }}); err != nil {
t.Fatal(err)
}
notFound := func() error { return fmt.Errorf("no such code: %w", ops.ErrNotFound) }
if err := ops.Register(r, ops.Op[claimLookIn, setupLook]{Name: "setup.start.look", Doc: "d", Kind: ops.Command, Auth: ops.Token, Route: "POST /api/v1/setup/look",
Run: func(_ context.Context, in claimLookIn) (setupLook, error) {
if f.limited {
return setupLook{}, fmt.Errorf("slow down: %w", ops.ErrRateLimited)
}
if strings.ToUpper(in.Code) != "ABCD-EFGH" {
return setupLook{}, notFound()
}
l := setupLook{Code: "ABCD-EFGH", Name: "e-desktop", Kind: f.kind, Address: f.addr, Created: time.Now().UTC().Format(time.RFC3339),
Repos: f.repos, Roles: []string{}, Problem: f.problem}
if f.kind == "push" {
l.Roles = []string{"developer", "publisher", "maintainer"}
}
return l, nil
}}); err != nil {
t.Fatal(err)
}
if err := ops.Register(r, ops.Op[claimIn, claimOut]{Name: "setup.start.claim", Doc: "d", Kind: ops.Command, Auth: ops.Token, Route: "POST /api/v1/setup/claim",
Run: func(_ context.Context, in claimIn) (claimOut, error) {
if strings.ToUpper(in.Code) != "ABCD-EFGH" {
return claimOut{}, notFound()
}
if f.refuse != "" {
return claimOut{}, fmt.Errorf("%s: %w", f.refuse, ops.ErrInvalid)
}
f.claimed = append(f.claimed, in)
return claimOut{Request: "req123"}, nil
}}); err != nil {
t.Fatal(err)
}
if err := ops.Register(r, ops.Op[claimLookIn, claimNone]{Name: "setup.start.discard", Doc: "d", Kind: ops.Command, Auth: ops.Token, Route: "POST /api/v1/setup/discard",
Run: func(_ context.Context, in claimLookIn) (claimNone, error) {
if strings.ToUpper(in.Code) != "ABCD-EFGH" {
return claimNone{}, notFound()
}
f.discarded++
return claimNone{}, nil
}}); err != nil {
t.Fatal(err)
}
f.h = HandlerWith(r, nil, nil, []Option{WithSessions(fakeSessions{user: "jva"})})
return f
}
func (f *claimForge) do(method, path string, form url.Values, signed bool) *httptest.ResponseRecorder {
var req *http.Request
if method == "POST" {
req = httptest.NewRequest("POST", path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else {
req = httptest.NewRequest("GET", path, nil)
}
req.RemoteAddr = "198.51.100.4:1234"
if signed {
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
}
rec := httptest.NewRecorder()
f.h.ServeHTTP(rec, req)
return rec
}
func TestTheCodePagesAreForAPersonSignedIn(t *testing.T) {
f := newClaimForge(t)
for _, c := range []struct{ method, path string }{{"GET", "/setup"}, {"POST", "/setup"}, {"POST", "/setup/claim"}} {
rec := f.do(c.method, c.path, url.Values{"code": {"ABCD-EFGH"}, "repos": {"ctx"}}, false)
if rec.Code != http.StatusSeeOther || !strings.HasPrefix(rec.Header().Get("Location"), "/login") {
t.Errorf("FAIL: %s %s without a session answered %d to %q", c.method, c.path, rec.Code, rec.Header().Get("Location"))
}
}
if len(f.claimed) != 0 {
t.Errorf("FAIL: a claim ran without a session")
}
rec := f.do("GET", "/setup", nil, true)
body := rec.Body.String()
if rec.Code != 200 || !hasHook(body, "setup-entry") || !hasHook(body, "setup-code") || !hasHook(body, "setup-look") || hasHook(body, "setup-nocode") {
t.Errorf("FAIL: the code page: %d", rec.Code)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestEveryCodeNotHeldIsOnePageState(t *testing.T) {
f := newClaimForge(t)
var first string
for i, code := range []string{"ZZZZ-ZZZZ", "not a code", "", strings.Repeat("A", 200), "abcd efgh!"} {
rec := f.do("POST", "/setup", url.Values{"code": {code}}, true)
body := nonceRe.ReplaceAllString(rec.Body.String(), "")
if rec.Code != http.StatusNotFound || !hasHook(body, "setup-nocode") || hasHook(body, "setup-claim") {
t.Errorf("FAIL: code %q answered %d", code, rec.Code)
}
if i == 0 {
first = body
} else if body != first {
t.Errorf("FAIL: code %q draws a different page", code)
}
}
// A claim and a discard of a code not held read the same.
for _, form := range []url.Values{{"code": {"ZZZZ-ZZZZ"}, "do": {"continue"}, "repos": {"ctx"}}, {"code": {"ZZZZ-ZZZZ"}, "do": {"discard"}}} {
rec := f.do("POST", "/setup/claim", form, true)
if rec.Code != http.StatusNotFound && rec.Code != http.StatusTooManyRequests || strings.Contains(rec.Body.String(), "req123") {
t.Errorf("FAIL: %v answered %d", form, rec.Code)
}
}
if !t.Failed() {
t.Log("ok")
}
}
func TestARateLimitedLookIsItsOwnState(t *testing.T) {
f := newClaimForge(t)
f.limited = true
rec := f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true)
if rec.Code != http.StatusTooManyRequests || rec.Header().Get("Retry-After") == "" || !hasHook(rec.Body.String(), "setup-held") {
t.Errorf("FAIL: a limited look answered %d (Retry-After %q)", rec.Code, rec.Header().Get("Retry-After"))
}
if !t.Failed() {
t.Log("ok")
}
}
func TestTheClaimPageShowsWhatWasAskedForAndOffersOnlyTheListedRepositories(t *testing.T) {
f := newClaimForge(t)
rec := f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true)
body := rec.Body.String()
if rec.Code != 200 {
t.Fatalf("FAIL: %d", rec.Code)
}
for _, hook := range []string{"setup-claim", "setup-claim-name", "setup-claim-kind", "setup-claim-code", "setup-claim-from", "setup-claim-when", "setup-claim-warn", "setup-claim-discard", "setup-claim-continue"} {
if !hasHook(body, hook) {
t.Errorf("FAIL: the page has no %s", hook)
}
}
if hookCount(body, "setup-claim-repo") != 2 || strings.Count(body, `type="checkbox" name="repos"`) != 2 || strings.Contains(body, `type="radio"`) || hasHook(body, "setup-claim-role") {
t.Errorf("FAIL: a read client's picker: %d repositories", hookCount(body, "setup-claim-repo"))
}
if v, _ := hookAttr(t, body, "setup-claim", "data-kind"); v != "read" {
t.Errorf("FAIL: data-kind %q", v)
}
if v, _ := hookAttr(t, body, "setup-claim-from", "data-address"); v != "198.51.100.4" {
t.Errorf("FAIL: data-address %q", v)
}
// The start came from the network the person is on: no warning.
if v, _ := hookAttr(t, body, "setup-claim-from", "data-match"); v != "true" || hasHook(body, "setup-claim-elsewhere") {
t.Errorf("FAIL: a start from this network: match %q, warning %v", v, hasHook(body, "setup-claim-elsewhere"))
}
// A start from another network is said, as data, not refused.
f.addr = "203.0.113.5"
body = f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true).Body.String()
if v, _ := hookAttr(t, body, "setup-claim-from", "data-match"); v != "false" || !hasHook(body, "setup-claim-elsewhere") || !hasHook(body, "setup-claim-continue") {
t.Errorf("FAIL: a start from elsewhere: match %q", v)
}
if v, _ := hookAttr(t, body, "setup-claim-elsewhere", "data-match"); v != "false" {
t.Errorf("FAIL: the warning's data-match %q", v)
}
// Two addresses of one IPv6 /64 are one network.
f.addr = oltp.AddrGroup("2001:db8:5:6::1")
req := httptest.NewRequest("POST", "/setup", strings.NewReader(url.Values{"code": {"ABCD-EFGH"}}.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.RemoteAddr = "[2001:db8:5:6:aaaa::7]:1234"
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
rec6 := httptest.NewRecorder()
f.h.ServeHTTP(rec6, req)
if v, _ := hookAttr(t, rec6.Body.String(), "setup-claim-from", "data-match"); v != "true" {
t.Errorf("FAIL: an address in the same /64 did not match: %q", v)
}
f.addr = "198.51.100.4"
if strings.Contains(rec.Header().Get("Location"), "ABCD") {
t.Errorf("FAIL: the code is in a header")
}
f.kind, f.repos = "push", repoRows("suit", "ctx", "wiki")
body = f.do("POST", "/setup", url.Values{"code": {"abcd-efgh"}}, true).Body.String()
if body == "" || hookCount(body, "setup-claim") == 0 {
t.Fatalf("FAIL: a code typed in lower case is not read")
}
if strings.Count(body, `type="radio" name="repos"`) != 3 || strings.Contains(body, `type="checkbox"`) || !hasHook(body, "setup-claim-role") ||
strings.Count(body, "<option") != 3 || !strings.Contains(body, `<option value="developer" selected>`) {
t.Errorf("FAIL: a pushing client's picker: radios %d, options %d", strings.Count(body, `type="radio"`), strings.Count(body, "<option"))
}
f.repos, f.problem = nil, "There is nothing."
body = f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true).Body.String()
if hookCount(body, "setup-claim-repo") != 0 || hasHook(body, "setup-claim-continue") || !hasHook(body, "setup-claim-empty") || !hasHook(body, "setup-claim-discard") || hasHook(body, "setup-claim-repos") {
t.Errorf("FAIL: a code with nothing to offer still offers")
}
if !t.Failed() {
t.Log("ok")
}
}
func TestContinueFilesAndLandsOnTheRequestAndDiscardThrowsTheStartAway(t *testing.T) {
f := newClaimForge(t)
rec := f.do("POST", "/setup/claim", url.Values{"code": {"ABCD-EFGH"}, "do": {"continue"}, "repos": {"ctx", "suit"}}, true)
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/me/requests/req123" || len(f.claimed) != 1 ||
f.claimed[0].Repos != "ctx,suit" || strings.Contains(rec.Header().Get("Location"), "ABCD") {
t.Errorf("FAIL: continue answered %d to %q with %+v", rec.Code, rec.Header().Get("Location"), f.claimed)
}
rec = f.do("POST", "/setup/claim", url.Values{"code": {"ABCD-EFGH"}, "do": {"discard"}}, true)
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/setup" || f.discarded != 1 {
t.Errorf("FAIL: discard answered %d to %q (%d discarded)", rec.Code, rec.Header().Get("Location"), f.discarded)
}
// Refused for what was picked: the page again, the picks kept and the
// sentence in its own element.
f.refuse = "ctx is not yours"
rec = f.do("POST", "/setup/claim", url.Values{"code": {"ABCD-EFGH"}, "do": {"continue"}, "repos": {"suit"}}, true)
body := rec.Body.String()
if rec.Code != http.StatusBadRequest || !hasHook(body, "setup-claim-error") || !strings.Contains(body, `value="suit" data-hook="setup-claim-repo" checked`) || strings.Contains(body, `value="ctx" data-hook="setup-claim-repo" checked`) {
t.Errorf("FAIL: a refused claim: %d", rec.Code)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestAPickerOfRowsFiltersWhenLong(t *testing.T) {
f := newClaimForge(t)
body := f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true).Body.String()
if hasHook(body, "setup-claim-filter") || hookCount(body, "setup-claim-row") != 2 || !hasHook(body, "setup-claim-count") {
t.Errorf("FAIL: a short list: rows %d, filter %v", hookCount(body, "setup-claim-row"), hasHook(body, "setup-claim-filter"))
}
var names []string
for i := 0; i < 15; i++ {
names = append(names, fmt.Sprintf("repo-%02d", i))
}
f.repos = repoRows(names...)
body = f.do("POST", "/setup", url.Values{"code": {"ABCD-EFGH"}}, true).Body.String()
if !hasHook(body, "setup-claim-filter") || hookCount(body, "setup-claim-row") != 15 || strings.Count(body, `<label class="claim-row">`) != 15 {
t.Errorf("FAIL: a long list: rows %d, filter %v", hookCount(body, "setup-claim-row"), hasHook(body, "setup-claim-filter"))
}
if !t.Failed() {
t.Log("ok")
}
}
func TestAClientStartedRequestNamesNoAccount(t *testing.T) {
render := func(agent string) string {
var b strings.Builder
s := meSignOff{ID: "abc", Agent: agent, Summary: "Set up agent e to read x", State: "pending", Requested: "2026-11-13T10:00:00Z"}
if err := noHooks.template(signOffPageTmpl).ExecuteTemplate(&b, "content", pageData{Title: "Approve?", SignOff: &s}); err != nil {
t.Fatal(err)
}
return b.String()
}
client := render("client of ann")
if v, _ := hookAttr(t, client, "signoff-agent", "data-client"); v != "true" || strings.Contains(client, "client of ann") {
t.Errorf("FAIL: a client's request: data-client %q, label shown %v", v, strings.Contains(client, "client of ann"))
}
if v, ok := hookAttr(t, render("zeitgeist"), "signoff-agent", "data-client"); ok && v != "" {
t.Errorf("FAIL: an agent's request is marked client")
}
var b strings.Builder
if err := noHooks.template(requestsPageTmpl).ExecuteTemplate(&b, "content", pageData{Title: "Requests", Requests: &requestsView{Tab: "waiting",
Waiting: []meSignOff{{ID: "abc", Agent: "client of ann", Summary: "x", State: "pending", Requested: "2026-11-13T10:00:00Z"}}}}); err != nil {
t.Fatal(err)
}
if v, _ := hookAttr(t, b.String(), "request-asker", "data-client"); v != "true" || strings.Contains(b.String(), "client of ann") {
t.Errorf("FAIL: the list row: data-client %q", v)
}
if !t.Failed() {
t.Log("ok")
}
}