Skip to content
quarry Artifact [3c9d449136]
Login

Artifact [3c9d449136]

Artifact 3c9d449136f1fbaa3ca7b8316f00ec033889704562647b5c8b415f71c5ae827b:


package main

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

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

const fmPassword = "pw-unique-4c1f9e2a"

// The mirror to Fossil across the doors: a repository admin sets it
// from the API or the settings page; a reader without `a` cannot; a
// private repository is refused at both doors; an agent holding `a`
// may; the password goes in and never comes back out of any door, the
// settings log or an error; stopping discards it.
func TestARepositoryAdminMirrorsAPublicRepositoryToFossil(t *testing.T) {
	g := newSiteRig(t)
	if err := g.db.AddAccount("bob", doorPassword, false); err != nil {
		t.Fatal(err)
	}
	var seen []string
	note := func(label, out string) {
		t.Helper()
		seen = append(seen, out)
		if strings.Contains(out, fmPassword) {
			t.Errorf("FAIL: the password is in the answer of %s: %s", label, out)
		}
	}
	set := "/api/v1/r/pub/fossil-mirror"
	body := map[string]string{"remote": "https://fossil.example.org/pub", "user": "pusher", "password": fmPassword}

	code, out := g.doAs("bob", http.MethodPost, set, doorPassword, "", body)
	note("a reader's set", out)
	if code == http.StatusOK {
		t.Errorf("FAIL: a reader without a mirrored it: %s", out)
	}
	code, out = g.doAs("alice", http.MethodPost, set, doorPassword, "", map[string]string{"remote": body["remote"], "user": "pusher"})
	note("a set with no password", out)
	if code != http.StatusBadRequest {
		t.Errorf("FAIL: a first enable with no password: %d %s", code, out)
	}
	for label, remote := range map[string]string{
		"http":     "http://fossil.example.org/pub",
		"userinfo": "https://pusher:" + fmPassword + "@fossil.example.org/pub",
		"query":    "https://fossil.example.org/pub?x=1",
	} {
		code, out = g.doAs("alice", http.MethodPost, set, doorPassword, "",
			map[string]string{"remote": remote, "user": "pusher", "password": fmPassword})
		note("a refused "+label, out)
		if code != http.StatusBadRequest {
			t.Errorf("FAIL: %s remote: %d %s", label, code, out)
		}
	}
	code, out = g.doAs("alice", http.MethodPost, set, doorPassword, "", body)
	note("the owner's enable", out)
	if code != http.StatusOK || !strings.Contains(out, `"on":true`) || !strings.Contains(out, `"user":"pusher"`) {
		t.Fatalf("FAIL: the owner's enable: %d %s", code, out)
	}
	_, out = g.doAs("alice", http.MethodGet, set, doorPassword, "", nil)
	note("get", out)
	if !strings.Contains(out, `"remote":"https://fossil.example.org/pub"`) || strings.Contains(out, "password") {
		t.Errorf("FAIL: get: %s", out)
	}
	// Blank keeps the stored password; a new remote needs its own.
	if code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "",
		map[string]string{"remote": body["remote"], "user": "pusher"}); code != http.StatusOK {
		t.Errorf("FAIL: a blank password on an unchanged mirror: %d %s", code, out)
	}
	if code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "",
		map[string]string{"remote": "https://other.example.org/pub", "user": "pusher"}); code != http.StatusBadRequest {
		t.Errorf("FAIL: a new remote kept the old password: %d %s", code, out)
	}
	if code, out := g.doAs("alice", http.MethodPost, "/api/v1/r/secret/fossil-mirror", doorPassword, "", body); code != http.StatusBadRequest {
		t.Errorf("FAIL: a private repository: %d %s", code, out)
	}

	// The repository follows another Fossil: never pushed back to it.
	if err := g.db.SetMirror("granted", "https://up.example.org/granted", oltp.MinMirrorInterval); err != nil {
		t.Fatal(err)
	}
	if err := g.db.SetMirror("pub", "https://up.example.org/pub", oltp.MinMirrorInterval); err != nil {
		t.Fatal(err)
	}
	code, out = g.doAs("alice", http.MethodPost, set, doorPassword, "",
		map[string]string{"remote": "https://UP.example.org/pub/", "user": "pusher", "password": fmPassword})
	note("a set to its own upstream", out)
	if code != http.StatusBadRequest {
		t.Errorf("FAIL: mirroring back to the Fossil it follows: %d %s", code, out)
	}
	if err := g.db.DetachMirror("pub"); err != nil {
		t.Fatal(err)
	}

	// The settings page offers the form to its admin, and only to its
	// admin, and never carries the password.
	page := func(user, repo string) string {
		_, b := g.doAs(user, http.MethodGet, "/r/"+repo+"/settings", doorPassword, "", nil)
		note("the settings page", b)
		return b
	}
	if b := page("alice", "pub"); !strings.Contains(b, `data-hook="fossil-mirror-form"`) || !strings.Contains(b, `data-mirroring="true"`) ||
		!strings.Contains(b, `data-hook="fossil-mirror-keeps"`) {
		t.Error("FAIL: the settings page does not offer the mirror form")
	}
	if code, b := g.doAs("bob", http.MethodGet, "/r/pub/settings", doorPassword, "", nil); code != http.StatusOK || strings.Contains(b, `data-hook="fossil-mirror"`) {
		t.Errorf("FAIL: a reader without a: page %d, or the section is drawn", code)
	}
	if b := page("alice", "secret"); !strings.Contains(b, `data-hook="fossil-mirror-private"`) || strings.Contains(b, `data-hook="fossil-mirror-form"`) {
		t.Error("FAIL: a private repository offers a mirror form")
	}

	// An agent holding a on a public repository may set one up.
	if err := g.db.Grant("zeitgeist", "pub", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	v, _ := g.mint(t, oltp.ScopeAPI)
	if code, out := g.do(http.MethodPost, set, v, "", map[string]string{"remote": body["remote"], "user": "pusher"}); code != http.StatusOK {
		t.Errorf("FAIL: an agent holding a: %d %s", code, out)
	}

	// The form posts through the ops and redirects with the flash or
	// the refusal, never carrying the password in the address.
	post := func(form url.Values) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/r/pub/settings/fossil-mirror", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.SetBasicAuth("alice", doorPassword)
		w := httptest.NewRecorder()
		g.h.ServeHTTP(w, req)
		return w
	}
	// A refusal travels in the flash cookie: the redirect carries no
	// query, the page shows the words once, and nothing typed (a
	// password in the address included) is echoed or pre-filled.
	const secretUser = "uzer-9c2e"
	for label, remote := range map[string]string{
		"http":     "http://nope.example.org/x",
		"userinfo": "https://" + secretUser + ":" + fmPassword + "@nope.example.org/x",
	} {
		w := post(url.Values{"do": {"set"}, "remote": {remote}, "user": {"typed-user-1b7"}, "password": {fmPassword}})
		loc := w.Header().Get("Location")
		if w.Code != http.StatusSeeOther || strings.Contains(loc, "?") || strings.Contains(loc, fmPassword) || strings.Contains(loc, secretUser) {
			t.Errorf("FAIL: a refused %s post: %d %s", label, w.Code, loc)
		}
		req := httptest.NewRequest(http.MethodGet, "/r/pub/settings", nil)
		req.SetBasicAuth("alice", doorPassword)
		for _, c := range w.Result().Cookies() {
			req.AddCookie(c)
		}
		rec := httptest.NewRecorder()
		g.h.ServeHTTP(rec, req)
		page := rec.Body.String()
		if !strings.Contains(page, `data-hook="fossil-mirror-refused"`) {
			t.Errorf("FAIL: the refusal is not shown after a %s post", label)
		}
		for _, leak := range []string{fmPassword, secretUser, "nope.example.org", "typed-user-1b7"} {
			if strings.Contains(page, leak) || strings.Contains(strings.Join(w.Header().Values("Set-Cookie"), " "), leak) {
				t.Errorf("FAIL: a refused %s post echoed %q", label, leak)
			}
		}
		// Spent: the next render is silent.
		req = httptest.NewRequest(http.MethodGet, "/r/pub/settings", nil)
		req.SetBasicAuth("alice", doorPassword)
		rec = httptest.NewRecorder()
		g.h.ServeHTTP(rec, req)
		if strings.Contains(rec.Body.String(), `data-hook="fossil-mirror-refused"`) {
			t.Errorf("FAIL: the refusal showed without its flash")
		}
	}
	// A link cannot word a message or fill the form.
	_, crafted := g.doAs("alice", http.MethodGet, "/r/pub/settings?fme=Your+session+expired&fremote=https://evil.example/x&fuser=evil-user", doorPassword, "", nil)
	if strings.Contains(crafted, `data-hook="fossil-mirror-refused"`) || strings.Contains(crafted, "evil") || strings.Contains(crafted, "session expired") {
		t.Error("FAIL: the page rendered something from the query string")
	}
	w := post(url.Values{"do": {"set"}, "remote": {body["remote"]}, "user": {"pusher"}, "password": {""}})
	if w.Code != http.StatusSeeOther || strings.Contains(w.Header().Get("Location"), "?") {
		t.Errorf("FAIL: a form post keeping the password: %d %s", w.Code, w.Header().Get("Location"))
	}

	// Made private, the mirror says it is paused.
	priv := oltp.Private
	if _, err := g.db.PatchRepo("pub", oltp.RepoPatch{Visibility: &priv}, "alice", ""); err != nil {
		t.Fatal(err)
	}
	if _, out := g.doAs("alice", http.MethodGet, set, doorPassword, "", nil); !strings.Contains(out, `"paused":"the repository is private`) {
		t.Errorf("FAIL: a private mirror does not say it is paused: %s", out)
	}
	pub := oltp.Public
	if _, err := g.db.PatchRepo("pub", oltp.RepoPatch{Visibility: &pub}, "alice", ""); err != nil {
		t.Fatal(err)
	}
	g.rg.refreshPrivate()

	// Stop mirroring from the page: the row and the password go.
	w = post(url.Values{"do": {"off"}})
	if w.Code != http.StatusSeeOther || strings.Contains(w.Header().Get("Location"), "?") {
		t.Fatalf("FAIL: stop mirroring: %d %s", w.Code, w.Header().Get("Location"))
	}
	if _, on, _ := g.db.FossilMirrorOf("pub"); on {
		t.Error("FAIL: the mirror outlived Stop mirroring")
	}
	log, _ := g.db.SettingsLog("pub", 50)
	var fields []string
	for _, c := range log {
		if strings.HasPrefix(c.Field, "fossil-mirror") {
			fields = append(fields, c.Field)
		}
		if strings.Contains(c.Was+c.Now, fmPassword) {
			t.Errorf("FAIL: the password is in the settings log: %+v", c)
		}
	}
	if len(fields) == 0 || fields[0] != "fossil-mirror" {
		t.Errorf("FAIL: the settings log: %v", fields)
	}
	t.Log("ok")
}

// The About line names the Fossil mirror only while its copy is
// current: not before the first push, and not after a failed one.
func TestAboutSaysWhereARepositoryIsMirroredToFossil(t *testing.T) {
	g := newSiteRig(t)
	m, err := g.db.SetFossilMirror("pub", "https://fossil.example.org/pub", "pusher", fmPassword, "alice", "")
	if err != nil {
		t.Fatal(err)
	}
	about := func() string {
		_, b := g.doAs("", http.MethodGet, "/api/v1/r/pub/about", "", "", nil)
		return b
	}
	if b := about(); strings.Contains(b, "mirroredTo") {
		t.Errorf("FAIL: claimed before any push: %s", b)
	}
	if err := g.db.RecordFossilMirrorPush("pub", m.Gen, ""); err != nil {
		t.Fatal(err)
	}
	if b := about(); !strings.Contains(b, `"mirroredTo":"fossil.example.org/pub"`) ||
		!strings.Contains(b, `"mirroredToUrl":"https://fossil.example.org/pub"`) {
		t.Errorf("FAIL: a current mirror is not named: %s", b)
	}
	if err := g.db.RecordFossilMirrorFailure("pub", m.Gen, "the server could not be reached"); err != nil {
		t.Fatal(err)
	}
	if b := about(); strings.Contains(b, "mirroredTo") {
		t.Errorf("FAIL: claimed after a failed push: %s", b)
	}
	t.Log("ok")
}

// Triggers while a run is going make exactly one more run, for the
// Fossil kind as for the Git one: the machinery is shared.
func TestFossilMirrorTriggersCoalesce(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	release := make(chan struct{})
	runs := make(chan struct{}, 10)
	rg.fossilMirror.run = func(string, time.Time) {
		runs <- struct{}{}
		<-release
	}
	rg.fossilMirror.kick("r", rg.fossilMirrorRun)
	<-runs
	for i := 0; i < 3; i++ {
		rg.fossilMirror.kick("r", rg.fossilMirrorRun)
	}
	close(release)
	select {
	case <-runs:
	case <-time.After(5 * time.Second):
		t.Fatal("FAIL: a trigger during a run made no further run")
	}
	time.Sleep(100 * time.Millisecond)
	if n := len(runs); n != 0 {
		t.Errorf("FAIL: %d runs beyond the one more", n)
	}
	rg.fossilMirror.mu.Lock()
	idle := !rg.fossilMirror.running["r"]
	rg.fossilMirror.mu.Unlock()
	if !idle {
		t.Error("FAIL: the repository is still marked running")
	}
	// The two kinds do not share a slot: a Git run in flight does not
	// absorb a Fossil trigger.
	rg.gitMirror.mu.Lock()
	rg.gitMirror.running = map[string]bool{"r": true}
	rg.gitMirror.again = map[string]bool{}
	rg.gitMirror.mu.Unlock()
	got := make(chan struct{}, 1)
	rg.fossilMirror.run = func(string, time.Time) { got <- struct{}{} }
	rg.fossilMirror.kick("r", rg.fossilMirrorRun)
	select {
	case <-got:
	case <-time.After(5 * time.Second):
		t.Error("FAIL: a Git run in flight held back the Fossil mirror")
	}
	t.Log("ok")
}

// The retry tick runs a mirror that has never pushed, or whose last
// attempt failed once its backoff has passed; never one that is
// current, which waits for the next fold.
func TestFossilMirrorRetryRunsOnlyWhatIsDue(t *testing.T) {
	at := time.Date(2026, 11, 1, 12, 0, 0, 0, time.UTC)
	stamp := func(d time.Duration) string { return at.Add(d).Format(time.RFC3339) }
	for name, tc := range map[string]struct {
		pushedAt, errAt string
		fails           int
		due             bool
	}{
		"never pushed":              {"", "", 0, true},
		"current":                   {stamp(-time.Hour), "", 0, false},
		"failed inside its backoff": {"", stamp(-30 * time.Second), 1, false},
		"failed past its backoff":   {"", stamp(-2 * time.Minute), 1, true},
		"failed again, longer wait": {stamp(-time.Hour), stamp(-2 * time.Minute), 3, false},
		"failed, long since":        {stamp(-time.Hour), stamp(-5 * time.Minute), 3, true},
	} {
		if got := fossilMirrorDue(tc.pushedAt, tc.errAt, tc.fails, at); got != tc.due {
			t.Errorf("FAIL: %s: due %v, want %v", name, got, tc.due)
		}
	}
	t.Log("ok")
}

func TestFossilMirrorFailuresAreWords(t *testing.T) {
	for out, want := range map[string]string{
		"zit push: the peer refused: login failed":            "refused the login",
		"zit push: the peer refused: not authorized to write": "not allowed to push",
		"zit push: cannot connect to https://f.example.org/x": "could not be reached",
		"x509: certificate signed by unknown authority":       "certificate",
		"the push was refused: project code mismatch":         "project code",
		"something odd\nhint: ignore me":                      "the push failed: something odd",
		"":                                                    "the push failed",
	} {
		if got := fossilMirrorWords(out, "f.example.org"); !strings.Contains(got, want) {
			t.Errorf("FAIL: %q -> %q, want %q", out, got, want)
		}
	}
	t.Log("ok")
}

// Saving a mirror starts a push at once, so "Not pushed yet" does not
// wait for the next commit or tick; a save that is refused starts none.
func TestSavingAFossilMirrorKicksARun(t *testing.T) {
	g := newSiteRig(t)
	kicked := make(chan string, 4)
	g.rg.fossilMirror.run = func(name string, _ time.Time) { kicked <- name }
	body := map[string]string{"remote": "https://fossil.example.org/pub", "user": "pusher", "password": fmPassword}
	if code, out := g.doAs("alice", http.MethodPost, "/api/v1/r/pub/fossil-mirror", doorPassword, "", map[string]string{"remote": "http://x.example.org/pub", "user": "u", "password": "p"}); code != http.StatusBadRequest {
		t.Fatalf("FAIL: %d %s", code, out)
	}
	select {
	case name := <-kicked:
		t.Fatalf("FAIL: a refused save started a run for %s", name)
	case <-time.After(200 * time.Millisecond):
	}
	for _, change := range []map[string]string{body, {"remote": body["remote"], "user": "pusher", "password": "another-pw"}} {
		if code, out := g.doAs("alice", http.MethodPost, "/api/v1/r/pub/fossil-mirror", doorPassword, "", change); code != http.StatusOK {
			t.Fatalf("FAIL: %d %s", code, out)
		}
		select {
		case name := <-kicked:
			if name != "pub" {
				t.Errorf("FAIL: the run is for %q", name)
			}
		case <-time.After(5 * time.Second):
			t.Fatal("FAIL: saving the mirror started no run")
		}
	}
	t.Log("ok")
}