Skip to content
quarry Artifact [8988a53c34]
Login

Artifact [8988a53c34]

Artifact 8988a53c34cdcbaa6a60d02a90b4618fbc203c6de29f481dbb6709a4504962db:


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
	}
	w := post(url.Values{"do": {"set"}, "remote": {"http://nope.example.org/x"}, "user": {"pusher"}, "password": {fmPassword}})
	loc := w.Header().Get("Location")
	if w.Code != http.StatusSeeOther || !strings.Contains(loc, "fme=") || strings.Contains(loc, fmPassword) || !strings.Contains(loc, "fuser=pusher") {
		t.Errorf("FAIL: a refused form post: %d %s", w.Code, loc)
	}
	w = post(url.Values{"do": {"set"}, "remote": {body["remote"]}, "user": {"pusher"}, "password": {""}})
	if w.Code != http.StatusSeeOther || strings.Contains(w.Header().Get("Location"), "fme=") {
		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"), "fme=") {
		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.Remote, m.User, m.Created, ""); 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.Remote, m.User, m.Created, "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); !strings.Contains(got, want) {
			t.Errorf("FAIL: %q -> %q, want %q", out, got, want)
		}
	}
	t.Log("ok")
}