Skip to content
quarry Artifact [1801ac043a]
Login

Artifact [1801ac043a]

Artifact 1801ac043adb72b1d417871d3bb4c6ad36099531a7f704afd5319f6e418c194e:


package web

import (
	"io"
	"net/http/httptest"
	"net/url"
	"regexp"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// webBuckets is an api.BucketStore in memory for the page tests.
type webBuckets map[string]oltp.BucketBinding

func (m webBuckets) BindBucket(repo string, b oltp.BucketBinding) error { m[repo] = b; return nil }
func (m webBuckets) Bucket(repo string) (oltp.BucketBinding, bool, error) {
	b, ok := m[repo]
	return b, ok, nil
}
func (m webBuckets) UnbindBucket(repo string) error { delete(m, repo); return nil }
func (m webBuckets) SetBucketProbe(repo string, at time.Time, probeErr string) error {
	b := m[repo]
	b.ProbedAt, b.ProbeErr = at.UTC().Format(time.RFC3339), probeErr
	m[repo] = b
	return nil
}

// webMirrors is an api.MirrorStore with one tracking mirror.
type webMirrors map[string]api.Mirror

func (m webMirrors) Mirrors() ([]api.Mirror, error) {
	var out []api.Mirror
	for _, v := range m {
		out = append(out, v)
	}
	return out, nil
}
func (m webMirrors) MirrorFor(repo string) (api.Mirror, bool, error) {
	v, ok := m[repo]
	return v, ok, nil
}
func (m webMirrors) SetMirror(repo, url string, every int) error {
	m[repo] = api.Mirror{Repo: repo, URL: url, Every: every, Enabled: true}
	return nil
}
func (m webMirrors) DetachMirror(string) error           { return nil }
func (m webMirrors) SetMirrorWrites(string, bool) error  { return nil }
func (m webMirrors) RecordMirrorRun(string, error) error { return nil }

// webCompanions binds one parent to one forum store.
type webCompanions map[string]string

func (c webCompanions) Companion(repo, _ string) (string, bool, error) {
	s, ok := c[repo]
	return s, ok, nil
}
func (c webCompanions) CompanionParent(store string) (string, string, bool, error) {
	for p, s := range c {
		if s == store {
			return p, "forum", true, nil
		}
	}
	return "", "", false, nil
}
func (c webCompanions) BindCompanion(repo, _, store string) error { c[repo] = store; return nil }

func bucketServer(t *testing.T, mutate func(*api.Deps)) *httptest.Server {
	t.Helper()
	d := api.Deps{Version: "v", Buckets: webBuckets{},
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}, "alpha-forum": {Index: landingIndex(t, false)}})}
	if mutate != nil {
		mutate(&d)
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin, "alpha-forum": ops.Admin}}),
		WithCaps(&memCaps{stored: map[string]string{"dan": "a"}})}))
	t.Cleanup(srv.Close)
	return srv
}

// The section carries its hooks: the status line with data-ok, the
// three forms, a password field that is never filled in, and an entry
// in the rail. Unbound, there is nothing to probe or unbind.
func TestAttachmentsSectionHooks(t *testing.T) {
	bound := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Bucket: &bucketPanel{
		Bound: true, Endpoint: "https://acct.r2.cloudflarestorage.com", Bucket: "files", Region: "auto",
		AccessKey: "AK", LimitMiB: 100, ProbedAt: "2026-09-11T10:00:00Z"}})
	for _, want := range []string{
		`<section id="attachments">`, `href="#attachments"`,
		`data-hook="bucket" data-ok="true"`,
		`action="/r/alpha/settings/bucket"`, `action="/r/alpha/settings/bucket/probe"`,
		`action="/r/alpha/settings/bucket/unbind"`,
		`name="secret" type="password"`, `placeholder="unchanged"`,
		`name="limitMiB" type="number" min="1" max="10240" size="6" value="100"`,
	} {
		if !strings.Contains(bound, want) {
			t.Errorf("FAIL: bound section lacks %s", want)
		}
	}
	if m := regexp.MustCompile(`name="secret"[^>]*value=`).FindString(bound); m != "" {
		t.Errorf("FAIL: the secret field carries a value: %s", m)
	}
	unbind := regexp.MustCompile(`(?s)<form[^>]*bucket/unbind.*?</form>`).FindString(bound)
	if !strings.Contains(unbind, `data-confirm=`) || !strings.Contains(unbind, `type="submit"`) {
		t.Errorf("FAIL: unbind is not a confirmed submit: %s", unbind)
	}
	failing := renderSettings(t, pageData{Repo: "alpha", Bucket: &bucketPanel{Bound: true, Bucket: "files",
		Endpoint: "https://x.example", ProbedAt: "2026-09-11T10:00:00Z", ProbeErr: "PUT returned 403 AccessDenied"}})
	if !strings.Contains(failing, `data-hook="bucket" data-ok="false"`) {
		t.Error("FAIL: a failed probe does not read data-ok=false")
	}
	unbound := renderSettings(t, pageData{Repo: "alpha", Bucket: &bucketPanel{LimitMiB: 100}})
	for _, absent := range []string{`data-hook="bucket"`, `bucket/unbind`, `bucket/probe`, `placeholder="unchanged"`} {
		if strings.Contains(unbound, absent) {
			t.Errorf("FAIL: unbound section carries %s", absent)
		}
	}
	if !strings.Contains(unbound, `name="secret" type="password"`) || !regexp.MustCompile(`name="secret"[^>]*required`).MatchString(unbound) {
		t.Error("FAIL: a first binding does not require the secret")
	}
	none := renderSettings(t, pageData{Repo: "alpha"})
	if strings.Contains(none, `id="attachments"`) || strings.Contains(none, `href="#attachments"`) {
		t.Error("FAIL: the section draws with no panel")
	}
	t.Log("ok: hooks present when bound, absent when unbound, section gone with no panel")
}

// A mirror accepts no writes, so its settings page has no Attachments
// section; the same page on a native repository has one.
func TestAttachmentsSectionAbsentOnAMirror(t *testing.T) {
	ms := webMirrors{}
	_ = ms.SetMirror("alpha", "https://upstream.example/home", 3600)
	srv := bucketServer(t, func(d *api.Deps) { d.Mirrors = ms })
	if body := asDan(t, srv, "/r/alpha/settings"); strings.Contains(body, `id="attachments"`) {
		t.Fatal("FAIL: a mirror's settings page draws the Attachments section")
	}
	if body := asDan(t, srv, "/r/alpha-forum/settings"); !strings.Contains(body, `id="attachments"`) {
		t.Fatal("FAIL: a native repository's settings page has no Attachments section")
	}
	t.Log("ok: hidden on the mirror, present on the native store")
}

// A companion store says whose bucket it uses and links there; it
// draws no form of its own.
func TestAttachmentsCompanionNamesItsParent(t *testing.T) {
	buckets := webBuckets{"alpha": {Endpoint: "https://x.example", Bucket: "files", Region: "auto", AccessKey: "AK", Secret: "s"}}
	srv := bucketServer(t, func(d *api.Deps) {
		d.Buckets = buckets
		d.Companions = webCompanions{"alpha": "alpha-forum"}
	})
	body := asDan(t, srv, "/r/alpha-forum/settings")
	if !strings.Contains(body, `data-hook="bucket-inherited"`) || !strings.Contains(body, `href="/r/alpha/settings#attachments"`) {
		t.Fatalf("FAIL: the companion does not name its parent's bucket")
	}
	if strings.Contains(body, `action="/r/alpha-forum/settings/bucket"`) {
		t.Fatal("FAIL: the companion draws a bucket form")
	}
	t.Log("ok: companion points at its parent")
}

// A refused save comes back as the settings page with the reason in
// the section and the typed fields kept — all but the secret.
func TestAttachmentsRefusedSaveRedraws(t *testing.T) {
	srv := bucketServer(t, nil)
	resp := postAsDan(t, srv, "/r/alpha/settings/bucket", url.Values{
		"endpoint": {"http://bucket.example"}, "bucket": {"files"}, "region": {"auto"},
		"accessKey": {"AK"}, "secret": {"topsecret"}, "limitMiB": {"25"}})
	if resp.StatusCode != 422 {
		t.Fatalf("FAIL: status %d, want 422", resp.StatusCode)
	}
	b, _ := io.ReadAll(resp.Body)
	body := string(b)
	for _, want := range []string{`<section id="attachments">`, `data-hook="bucket-refused"`, `value="http://bucket.example"`, `value="AK"`, `value="25"`} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: redraw lacks %s", want)
		}
	}
	if strings.Contains(body, "topsecret") {
		t.Error("FAIL: the secret was drawn back")
	}
	if strings.Contains(body, "invalid request") {
		t.Error("FAIL: the sentinel leaked into the reason")
	}
	t.Log("ok: refused save redraws the section with its reason")
}

// Fleet shows a bound bucket's last probe on the store's row the way
// it shows a mirror's last pull, with the verdict as data-ok.
func TestFleetRowShowsTheBucketProbe(t *testing.T) {
	var row fleetRow
	row.Name, row.Bucket, row.BucketProbedAt = "alpha", "files", "2026-09-11T10:00:00Z"
	var sb strings.Builder
	if err := fleetAdminPage.ExecuteTemplate(&sb, "fleet-row", row); err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(sb.String(), `data-hook="bucket-probe" data-ok="true"`) {
		t.Fatalf("FAIL: no probe on the row:\n%s", sb.String())
	}
	row.BucketErr = "PUT returned 403 AccessDenied"
	sb.Reset()
	if err := fleetAdminPage.ExecuteTemplate(&sb, "fleet-row", row); err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(sb.String(), `data-hook="bucket-probe" data-ok="false"`) || !strings.Contains(sb.String(), `class="row-err"`) {
		t.Fatalf("FAIL: a failing probe is not on the row:\n%s", sb.String())
	}
	row.Bucket = ""
	sb.Reset()
	_ = fleetAdminPage.ExecuteTemplate(&sb, "fleet-row", row)
	if strings.Contains(sb.String(), `bucket-probe`) {
		t.Fatal("FAIL: an unbound store shows a probe")
	}
	t.Log("ok: the fleet row carries the probe and its verdict")
}