Skip to content
quarry Artifact [35f7abcf66]
Login

Artifact [35f7abcf66]

Artifact 35f7abcf66e13c79de6e5e2393440c045bc41f55e14c61cd7011def0971ec270:


package main

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

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

const mpPassword = "pw-unique-77d2b1e0"

type mirrorGet struct {
	On        bool   `json:"on"`
	Pushed    string `json:"pushed"`
	PushedAt  string `json:"pushed_at"`
	Error     string `json:"error"`
	ErrorAt   string `json:"error_at"`
	ErrorKind string `json:"error_kind"`
	Fails     int    `json:"fails"`
	NextTry   string `json:"next_try"`
	Created   string `json:"created"`
	Paused    string `json:"paused"`
	PausedBy  string `json:"paused_by"`
}

func (g siteRig) mirrorGet(t *testing.T, kind string) mirrorGet {
	t.Helper()
	code, out := g.doAs("alice", http.MethodGet, "/api/v1/r/pub/"+kind+"-mirror", doorPassword, "", nil)
	var m mirrorGet
	if code != http.StatusOK || json.Unmarshal([]byte(out), &m) != nil {
		t.Fatalf("FAIL: get %s mirror: %d %s", kind, code, out)
	}
	return m
}

func (g siteRig) settingsPage(t *testing.T, cookies []*http.Cookie, query string) string {
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, "/r/pub/settings"+query, nil)
	req.SetBasicAuth("alice", doorPassword)
	for _, c := range cookies {
		req.AddCookie(c)
	}
	rec := httptest.NewRecorder()
	g.h.ServeHTTP(rec, req)
	return rec.Body.String()
}

func mirrorState(page, kind string) string {
	i := strings.Index(page, `<section id="`+kind+`-mirror"`)
	if i < 0 {
		return ""
	}
	m := regexp.MustCompile(`data-hook="mirror-state" data-state="([a-z]+)"`).FindStringSubmatch(page[i:])
	if m == nil {
		return ""
	}
	return m[1]
}

// Both get ops report the failures in a row and the time of the next
// try, which is the runner's own schedule applied to the failure's
// time; the last good push survives a failure; a refused login is told
// as such; a paused mirror says by what.
func TestMirrorGetOpsReportFailuresAndTheRunnersOwnNextTry(t *testing.T) {
	g := newSiteRig(t)
	set := func(kind string, body map[string]string) {
		t.Helper()
		if code, out := g.doAs("alice", http.MethodPost, "/api/v1/r/pub/"+kind+"-mirror", doorPassword, "", body); code != http.StatusOK {
			t.Fatalf("FAIL: set %s: %d %s", kind, code, out)
		}
	}
	set("fossil", map[string]string{"remote": "https://fossil.example.org/pub", "user": "pusher", "password": mpPassword})
	set("git", map[string]string{"remote": "[email protected]:alice/pub.git"})

	if m := g.mirrorGet(t, "fossil"); m.Fails != 0 || m.NextTry != "" || m.Created == "" {
		t.Errorf("FAIL: a mirror that has not failed: %+v", m)
	}

	// A good push, then failures: the push is kept beside them.
	f, _, _ := g.db.FossilMirrorOf("pub")
	if err := g.db.RecordFossilMirrorPush("pub", f.Gen, "tipfossil"); err != nil {
		t.Fatal(err)
	}
	gm, _, _ := g.db.GitMirrorOf("pub")
	if err := g.db.RecordGitMirrorPush("pub", gm.PublicKey, gm.Remote, gm.Branch, "tipgit"); err != nil {
		t.Fatal(err)
	}
	for i := 0; i < 3; i++ {
		if err := g.db.RecordFossilMirrorFailure("pub", f.Gen, oltp.FossilLoginRefusedWords); err != nil {
			t.Fatal(err)
		}
	}
	if err := g.db.RecordGitMirrorFailure("pub", gm.PublicKey, gm.Remote, gm.Branch, "the host could not be reached"); err != nil {
		t.Fatal(err)
	}

	fm, gtm := g.mirrorGet(t, "fossil"), g.mirrorGet(t, "git")
	if fm.Fails != 3 || fm.Pushed != "tipfossil" || fm.PushedAt == "" || fm.Error == "" || fm.ErrorKind != "login" {
		t.Errorf("FAIL: fossil after three failures: %+v", fm)
	}
	if gtm.Fails != 1 || gtm.Pushed != "tipgit" || gtm.PushedAt == "" || gtm.ErrorKind != "" {
		t.Errorf("FAIL: git after a failure: %+v", gtm)
	}
	for kind, m := range map[string]mirrorGet{"fossil": fm, "git": gtm} {
		failed, err := time.Parse(time.RFC3339, m.ErrorAt)
		if err != nil {
			t.Fatalf("FAIL: %s error_at %q", kind, m.ErrorAt)
		}
		want := failed.Add(oltp.MirrorBackoff(m.Fails))
		next, err := time.Parse(time.RFC3339, m.NextTry)
		if err != nil || !next.Equal(want) {
			t.Errorf("FAIL: %s next try %q is not the failure's time plus the runner's backoff %v", kind, m.NextTry, want)
		}
		// The runner's own decision flips exactly there.
		if kind == "fossil" {
			if !fossilMirrorBackingOff(m.ErrorAt, m.Fails, next.Add(-time.Second)) || fossilMirrorBackingOff(m.ErrorAt, m.Fails, next) {
				t.Errorf("FAIL: the fossil runner does not retry when the page says")
			}
		}
	}
	// The words the runner stores for a refused login are the ones the
	// API classes as a login.
	if fossilMirrorWords("zit push: the peer refused: login failed", "f.example.org") != oltp.FossilLoginRefusedWords {
		t.Error("FAIL: the runner's login words are not the shared ones")
	}

	// The page reads them back: failing, and the last good push still there.
	page := g.settingsPage(t, nil, "")
	if mirrorState(page, "fossil") != "failing" || mirrorState(page, "git") != "failing" ||
		!strings.Contains(page, "/checkin/tipfossil") || !strings.Contains(page, "third failure in a row") ||
		!strings.Contains(page, `id="fossil-mirror-password"`) {
		t.Error("FAIL: the page does not show both mirrors failing with their last good push and the password row open")
	}
	if strings.Contains(page, mpPassword) {
		t.Error("FAIL: the password is on the page")
	}

	// Paused by a private repository.
	priv := oltp.Private
	if _, err := g.db.PatchRepo("pub", oltp.RepoPatch{Visibility: &priv}, "alice", ""); err != nil {
		t.Fatal(err)
	}
	if m := g.mirrorGet(t, "fossil"); m.PausedBy != "private" || m.NextTry != "" || m.Fails != 3 {
		t.Errorf("FAIL: a paused mirror reports a next try: %+v", m)
	}
	if m := g.mirrorGet(t, "git"); m.PausedBy != "private" || m.Paused == "" || m.NextTry != "" {
		t.Errorf("FAIL: paused by: %+v", m)
	}
	if page := g.settingsPage(t, nil, ""); mirrorState(page, "fossil") != "paused" || mirrorState(page, "git") != "paused" ||
		strings.Contains(page, `data-hook="mirror-edit"`) {
		t.Error("FAIL: a private repository's mirrors are not paused and read-only")
	}
	t.Log("ok")
}

// A refused save through the real form names the field it is about, for
// each field of each mirror, and the typed remote and user come back
// while the password does not.
func TestRefusedMirrorSaveNamesItsFieldThroughTheForm(t *testing.T) {
	g := newSiteRig(t)
	post := func(kind string, form url.Values) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/r/pub/settings/"+kind+"-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
	}
	for name, tc := range map[string]struct {
		kind  string
		form  url.Values
		field string
		typed []string
	}{
		"fossil remote": {"fossil", url.Values{"remote": {"http://nope.example.org/x"}, "user": {"typed-user-31"}, "password": {mpPassword}}, "remote", []string{"typed-user-31"}},
		"fossil user":   {"fossil", url.Values{"remote": {"https://fossil.example.org/x"}, "user": {"bad user"}, "password": {mpPassword}}, "user", []string{"fossil.example.org/x"}},
		"fossil login":  {"fossil", url.Values{"remote": {"https://fossil.example.org/x"}, "user": {"typed-user-31"}}, "password", []string{"typed-user-31", "fossil.example.org/x"}},
		"git remote":    {"git", url.Values{"remote": {"https://github.com/o/r.git"}, "branch": {"typed-branch"}}, "remote", []string{"typed-branch"}},
		"git branch":    {"git", url.Values{"remote": {"[email protected]:o/r.git"}, "branch": {"-bad"}}, "branch", []string{"[email protected]:o/r.git"}},
	} {
		w := post(tc.kind, tc.form)
		loc := w.Header().Get("Location")
		if w.Code != http.StatusSeeOther || strings.Contains(loc, "?") {
			t.Errorf("FAIL: %s: %d %s", name, w.Code, loc)
		}
		page := g.settingsPage(t, w.Result().Cookies(), "")
		sec := page[strings.Index(page, `<section id="`+tc.kind+`-mirror"`):]
		sec = sec[:strings.Index(sec, "</section>")]
		if !strings.Contains(sec, `data-hook="mirror-field-error" data-field="`+tc.field+`"`) ||
			!strings.Contains(sec, `id="`+tc.kind+`-mirror-`+tc.field+`-err"`) || strings.Count(sec, `aria-invalid="true"`) != 1 {
			t.Errorf("FAIL: %s: the field %s is not the one marked", name, tc.field)
		}
		for _, typed := range tc.typed {
			if !strings.Contains(sec, typed) {
				t.Errorf("FAIL: %s: %q did not survive the refusal", name, typed)
			}
		}
		if strings.Contains(page, mpPassword) || strings.Contains(strings.Join(w.Header().Values("Set-Cookie"), " "), mpPassword) {
			t.Errorf("FAIL: %s: the password came back", name)
		}
	}
	t.Log("ok")
}

// A save that fails on the edit form of a mirror that is on reopens that
// edit with the typed values, and Edit and Replace are chosen by the
// address alone.
func TestEditAndReplaceAreChosenByTheAddress(t *testing.T) {
	g := newSiteRig(t)
	if code, out := g.doAs("alice", http.MethodPost, "/api/v1/r/pub/fossil-mirror", doorPassword, "", map[string]string{
		"remote": "https://fossil.example.org/pub", "user": "pusher", "password": mpPassword}); code != http.StatusOK {
		t.Fatalf("FAIL: %d %s", code, out)
	}
	rest := g.settingsPage(t, nil, "")
	if mirrorState(rest, "fossil") != "pushing" || !strings.Contains(rest, `data-hook="mirror-edit"`) || strings.Contains(rest, `id="fossil-mirror-remote"`) {
		t.Error("FAIL: the saved, never pushed mirror at rest")
	}
	if edit := g.settingsPage(t, nil, "?edit=fossil-mirror"); !strings.Contains(edit, `id="fossil-mirror-remote"`) || !strings.Contains(edit, `id="fossil-mirror-user"`) {
		t.Error("FAIL: ?edit= did not open the remote and user")
	}
	if rep := g.settingsPage(t, nil, "?edit=fossil-mirror-password"); strings.Contains(rep, `id="fossil-mirror-remote"`) || !strings.Contains(rep, `data-hook="mirror-save-password"`) {
		t.Error("FAIL: ?edit= did not open the password row alone")
	}
	if junk := g.settingsPage(t, nil, "?edit=anything"); strings.Contains(junk, `id="fossil-mirror-remote"`) {
		t.Error("FAIL: an unknown edit opened something")
	}

	// A new remote with no password is refused on the password, and the
	// edit it came from is open again with the typed remote.
	req := httptest.NewRequest(http.MethodPost, "/r/pub/settings/fossil-mirror",
		strings.NewReader(url.Values{"do": {"set"}, "edit": {"all"}, "remote": {"https://other.example.org/pub"}, "user": {"pusher"}}.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.SetBasicAuth("alice", doorPassword)
	w := httptest.NewRecorder()
	g.h.ServeHTTP(w, req)
	page := g.settingsPage(t, w.Result().Cookies(), "")
	if !strings.Contains(page, `value="https://other.example.org/pub"`) || !strings.Contains(page, `data-field="password"`) {
		t.Error("FAIL: the refused edit did not reopen with the typed remote and the password marked")
	}
	if m := g.mirrorGet(t, "fossil"); !strings.Contains(m.Created, "20") {
		t.Errorf("FAIL: created is not reported: %+v", m)
	}
	t.Log("ok")
}

// Through the API door a password alone replaces the password of a mirror
// that is on and leaves its remote and user, and is refused when the
// mirror is off or the repository is private.
func TestPasswordOnlyThroughTheDoors(t *testing.T) {
	g := newSiteRig(t)
	set := "/api/v1/r/pub/fossil-mirror"
	if code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "", map[string]string{"password": mpPassword}); code != http.StatusBadRequest {
		t.Errorf("FAIL: a password alone on a mirror that is off: %d %s", code, out)
	}
	if code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "", map[string]string{
		"remote": "https://fossil.example.org/pub", "user": "pusher", "password": "first-pw"}); code != http.StatusOK {
		t.Fatalf("FAIL: %d %s", code, out)
	}
	before, _, _ := g.db.FossilMirrorOf("pub")
	code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "", map[string]string{"password": mpPassword})
	if code != http.StatusOK || strings.Contains(out, mpPassword) {
		t.Fatalf("FAIL: password only: %d %s", code, out)
	}
	after, _, _ := g.db.FossilMirrorOf("pub")
	if after.Remote != before.Remote || after.User != before.User || after.Gen == before.Gen {
		t.Errorf("FAIL: remote, user or generation: %+v -> %+v", before, after)
	}
	if got, _ := g.db.FossilMirrorPassword("pub", after.Gen); got != mpPassword {
		t.Error("FAIL: the password was not replaced")
	}
	priv := oltp.Private
	if _, err := g.db.PatchRepo("pub", oltp.RepoPatch{Visibility: &priv}, "alice", ""); err != nil {
		t.Fatal(err)
	}
	if code, out := g.doAs("alice", http.MethodPost, set, doorPassword, "", map[string]string{"password": "x-other"}); code == http.StatusOK {
		t.Errorf("FAIL: password only on a private repository: %s", out)
	}
	t.Log("ok")
}