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="mirror-replace"`) {
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 under the field they are
// about, the typed remote and user come back, and a password, or a
// credential typed into the address, is echoed nowhere.
const secretUser = "uzer-9c2e"
for label, tc := range map[string]struct {
remote, field string
kept bool
}{
"http": {"http://nope.example.org/x", "remote", false},
"userinfo": {"https://" + secretUser + ":" + fmPassword + "@nope.example.org/x", "remote", false},
} {
w := post(url.Values{"do": {"set"}, "remote": {tc.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="mirror-field-error" data-field="`+tc.field+`"`) {
t.Errorf("FAIL: the refusal is not shown under its field after a %s post", label)
}
if !strings.Contains(page, `value="typed-user-1b7"`) || strings.Contains(page, "nope.example.org") != tc.kept {
t.Errorf("FAIL: after a refused %s post the typed user must survive and the remote only if it held no credential (kept %v)", label, tc.kept)
}
for _, leak := range []string{fmPassword, secretUser} {
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="mirror-field-error"`) {
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="mirror-field-error"`) || 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")
}
// What zit prints for each failure maps to words of its own. The
// certificate wording is not here: zit says "cannot connect" for it, and
// the forge tells it apart by asking the host, which
// TestAFossilMirrorToAnUntrustedCertificateSaysSo exercises.
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",
// zit's own text for a peer that is another project.
"zit push: the peer refused: wrong project (this repository and the peer's are different projects; clone the peer to work on it)": "clone of this repository",
"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")
}
// Whatever changes a mirror's settings leaves it due for the retry tick,
// on whichever node holds the lease, with no commit and no kick.
func TestAChangedFossilMirrorIsDueForTheRetryTick(t *testing.T) {
rg := newTestRegistrar(t, 25)
if err := rg.odb.AddRepo("r", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
m, err := rg.odb.SetFossilMirror("r", "https://fossil.example.org/r", "u", "pw", "dan", "")
if err != nil {
t.Fatal(err)
}
if err := rg.odb.RecordFossilMirrorPush("r", m.Gen, "abc"); err != nil {
t.Fatal(err)
}
cur, _, _ := rg.odb.FossilMirrorOf("r")
if fossilMirrorDue(cur.PushedAt, cur.ErrAt, cur.Fails, time.Now()) {
t.Fatal("FAIL: a current mirror is due")
}
if _, err := rg.odb.SetFossilMirror("r", "https://fossil.example.org/r", "u", "corrected", "dan", ""); err != nil {
t.Fatal(err)
}
cur, _, _ = rg.odb.FossilMirrorOf("r")
if !fossilMirrorDue(cur.PushedAt, cur.ErrAt, cur.Fails, time.Now()) {
t.Errorf("FAIL: a corrected password left the mirror not due: %+v", cur)
}
t.Log("ok")
}