Skip to content
quarry Artifact [173e2b156c]
Login

Artifact [173e2b156c]

Artifact 173e2b156c51bb861828c7b5b884562c6b85fce3f118ef2f9f6d0da1787896d9:


package main

import (
	"bytes"
	"compress/zlib"
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

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

// zitShim is a zit on PATH that records its argument list and whether
// ZIT_PASSWORD was set (never its value) before running the real one,
// so a test can read what the forge handed zit.
const zitShim = `#!/bin/sh
{ printf 'argv:'; for a in "$@"; do printf ' [%s]' "$a"; done; printf ' pwenv=%s\n' "${ZIT_PASSWORD:+set}"; } >> "$SHIM_LOG"
exec "$REAL_ZIT" "$@"
`

// fossilRig is a real store, a Fossil server holding a clone of it (so
// the project codes match) that wants a login to push, and a registrar
// whose Fossil mirror points at it.
type fossilRig struct {
	*closeRig
	target, fossilHome, shimLog, remote string
	logged                              *bytes.Buffer
}

const fossilUser, fossilPw = "pusher", "e2e-secret-pw-7d3a"

func freePort(t *testing.T) int {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer l.Close()
	return l.Addr().(*net.TCPAddr).Port
}

func waitListening(t *testing.T, port int) {
	t.Helper()
	for i := 0; i < 100; i++ {
		if c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)); err == nil {
			c.Close()
			return
		}
		time.Sleep(50 * time.Millisecond)
	}
	t.Fatalf("nothing listens on %d", port)
}

func background(t *testing.T, dir string, env []string, name string, args ...string) {
	t.Helper()
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), env...)
	if err := cmd.Start(); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { cmd.Process.Kill(); cmd.Wait() })
}

func newFossilRig(t *testing.T) fossilRig {
	t.Helper()
	for _, bin := range []string{"fossil", "zit"} {
		if _, err := exec.LookPath(bin); err != nil {
			t.Skip(bin + " not on PATH")
		}
	}
	realZit, _ := exec.LookPath("zit")
	shimDir, shimLog := t.TempDir(), filepath.Join(t.TempDir(), "shim.log")
	if err := os.WriteFile(filepath.Join(shimDir, "zit"), []byte(zitShim), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("REAL_ZIT", realZit)
	t.Setenv("SHIM_LOG", shimLog)
	t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
	fossilHome := t.TempDir()
	t.Setenv("FOSSIL_HOME", fossilHome)

	r := newCloseRig(t)
	if err := r.db.AddRepo("test", r.dir, "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	// The target is a clone of the store, so its project code matches.
	src := freePort(t)
	srv := exec.Command("zit", "serve", "--port", fmt.Sprint(src))
	srv.Dir = r.dir
	if err := srv.Start(); err != nil {
		t.Fatal(err)
	}
	waitListening(t, src)
	target := filepath.Join(t.TempDir(), "target.fossil")
	if out, err := exec.Command("fossil", "clone", fmt.Sprintf("http://127.0.0.1:%d/", src), target).CombinedOutput(); err != nil {
		t.Fatalf("fossil clone: %v\n%s", err, out)
	}
	srv.Process.Kill()
	srv.Wait()
	for _, args := range [][]string{
		{"user", "new", fossilUser, "", fossilPw, "-R", target},
		{"user", "capabilities", fossilUser, "oi", "-R", target},
	} {
		if out, err := exec.Command("fossil", args...).CombinedOutput(); err != nil {
			t.Fatalf("fossil %v: %v\n%s", args, err, out)
		}
	}
	port := freePort(t)
	background(t, filepath.Dir(target), nil, "fossil", "server", target, "--port", fmt.Sprint(port), "--localhost")
	waitListening(t, port)

	logged := &bytes.Buffer{}
	log.SetOutput(logged)
	t.Cleanup(func() { log.SetOutput(os.Stderr) })
	return fossilRig{closeRig: r, target: target, fossilHome: fossilHome, shimLog: shimLog,
		remote: fmt.Sprintf("http://127.0.0.1:%d/", port), logged: logged}
}

func (r fossilRig) enable(password string) oltp.FossilMirror {
	r.t.Helper()
	m, err := r.db.SetFossilMirror("test", r.remote, fossilUser, password, "dan", "")
	if err != nil {
		r.t.Fatal(err)
	}
	return m
}

func (r fossilRig) mirror() oltp.FossilMirror {
	m, _, err := r.db.FossilMirrorOf("test")
	if err != nil {
		r.t.Fatal(err)
	}
	return m
}

// pushes is the argument lines of every `zit push` the forge ran.
func (r fossilRig) pushes() []string {
	b, _ := os.ReadFile(r.shimLog)
	var out []string
	for _, l := range strings.Split(string(b), "\n") {
		if strings.HasPrefix(l, "argv: [push]") {
			out = append(out, l)
		}
	}
	return out
}

func (r fossilRig) fossil(args ...string) string {
	r.t.Helper()
	out, err := exec.Command("fossil", args...).CombinedOutput()
	if err != nil {
		r.t.Fatalf("fossil %v: %v\n%s", args, err, out)
	}
	return strings.TrimSpace(string(out))
}

func (r fossilRig) blobs() string {
	return r.fossil("sql", "-R", r.target, "SELECT count(*) FROM blob")
}

func (r fossilRig) has(hash string) bool {
	return exec.Command("fossil", "artifact", hash, "-R", r.target).Run() == nil
}

// loginCards counts the login cards a sink was sent: a Fossil sync
// body is a zlib stream behind a four-byte length, and a login card is
// a line starting "login ".
type loginCards struct {
	mu    sync.Mutex
	cards int
	posts int
}

func (l *loginCards) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	body, _ := io.ReadAll(req.Body)
	text := body
	if len(body) > 4 {
		if zr, err := zlib.NewReader(bytes.NewReader(body[4:])); err == nil {
			if b, err := io.ReadAll(zr); err == nil {
				text = b
			}
		}
	}
	l.mu.Lock()
	l.posts++
	for _, line := range bytes.Split(text, []byte("\n")) {
		if bytes.HasPrefix(line, []byte("login ")) {
			l.cards++
		}
	}
	l.mu.Unlock()
	http.Error(w, "no", http.StatusInternalServerError)
}

func (l *loginCards) count() (cards, posts int) {
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.cards, l.posts
}

// The whole path against a real Fossil server: a run pushes what the
// store holds (check-ins and tickets, not only trunk), a second run with
// nothing new still runs, the password reaches zit only in its
// environment and appears in no argument and no log line, and zit saves
// no login: a push or pull to another address afterwards sends none.
func TestAFossilMirrorPushesEverythingWithoutLeavingALoginBehind(t *testing.T) {
	r := newFossilRig(t)
	r.enable(fossilPw)
	before := r.blobs()

	r.commit("b.txt", "second", "")
	if _, _, err := r.cli.TicketNew(lease.Unguarded("test: scratch store, no coordinator"), "dan", map[string]string{"title": "after the clone", "status": "Open"}); err != nil {
		t.Fatal(err)
	}
	r.sync()
	tip, _, _ := r.ix.TipOf("trunk")
	if r.has(tip.Tip) {
		t.Fatal("FAIL: the target already holds the new tip")
	}

	r.rg.fossilMirrorRun("test", time.Now())
	m := r.mirror()
	if m.Err != "" || m.PushedAt == "" || m.Pushed != tip.Tip || m.Fails != 0 {
		t.Fatalf("FAIL: after the push: %+v", m)
	}
	if !r.has(tip.Tip) {
		t.Error("FAIL: the target lacks trunk's tip after the push")
	}
	if after := r.blobs(); after == before {
		t.Errorf("FAIL: the target holds %s artifacts before and after", after)
	}

	// Each run pushes, even when nothing is new: a push carries more
	// than trunk, so an unchanged tip proves nothing.
	r.rg.fossilMirrorRun("test", time.Now())
	pushes := r.pushes()
	if len(pushes) != 2 {
		t.Fatalf("FAIL: %d pushes after two runs: %v", len(pushes), pushes)
	}
	for _, l := range pushes {
		for _, want := range []string{"[push] [" + r.remote + "]", "[--user] [" + fossilUser + "]", "[--once]", "pwenv=set"} {
			if !strings.Contains(l, want) {
				t.Errorf("FAIL: argv line %q lacks %q", l, want)
			}
		}
		if strings.Contains(l, fossilPw) || strings.Contains(l, "--password") {
			t.Errorf("FAIL: the password is in argv: %q", l)
		}
	}

	// Nothing was saved: a sink sees no login card from a push or a
	// pull with no --user, though a push with one is seen (so the sink
	// can tell).
	sink := &loginCards{}
	srv := httptest.NewServer(sink)
	t.Cleanup(srv.Close)
	probe := func(env []string, args ...string) {
		cmd := exec.Command("zit", args...)
		cmd.Dir = r.dir
		cmd.Env = append(os.Environ(), env...)
		cmd.CombinedOutput() // the sink refuses everything; only what it was sent matters
	}
	probe([]string{"ZIT_PASSWORD=" + fossilPw}, "push", srv.URL+"/", "--user", fossilUser, "--once")
	if cards, _ := sink.count(); cards == 0 {
		t.Fatal("FAIL: the sink cannot see a login card, so it proves nothing")
	}
	base, _ := sink.count()
	probe(nil, "push", srv.URL+"/")
	probe(nil, "pull", srv.URL+"/")
	if cards, posts := sink.count(); cards != base || posts < 3 {
		t.Errorf("FAIL: zit sent a saved login (%d cards, was %d; %d posts)", cards, base, posts)
	}
	// Nor to the same server: no remote is saved, and a push there with
	// no --user is anonymous, which the server refuses.
	if out := zitIn(t, r.dir, "remote"); !strings.Contains(out, "no remote set") {
		t.Errorf("FAIL: zit saved a remote: %q", out)
	}
	anon := exec.Command("zit", "push", r.remote)
	anon.Dir = r.dir
	if out, err := anon.CombinedOutput(); err == nil {
		t.Errorf("FAIL: an anonymous push to the server was accepted, so a login was saved: %s", out)
	}
	if out := r.fossil("sql", "-R", r.target, "SELECT count(*) FROM user WHERE login='"+fossilUser+"'"); out != "1" {
		t.Errorf("FAIL: the target's user: %s", out)
	}
	t.Log("ok")
}

// A wrong password is said in words and counted; giving the right one
// clears it and the next run pushes.
func TestAFossilMirrorWithAWrongPasswordSaysSoAndRecovers(t *testing.T) {
	r := newFossilRig(t)
	r.enable("not-the-password")
	r.commit("b.txt", "second", "")
	r.sync()
	r.rg.fossilMirrorRun("test", time.Now())
	m := r.mirror()
	if m.Fails != 1 || m.Err != fossilMirrorWords("the peer refused: login failed") || m.Pushed != "" {
		t.Fatalf("FAIL: a wrong password: %+v", m)
	}
	// The retry tick and a kick both wait out the backoff: a wrong
	// password is not tried again after every fold.
	if fossilMirrorDue(m.PushedAt, m.ErrAt, m.Fails, time.Now()) {
		t.Error("FAIL: the tick would retry inside the backoff")
	}
	r.rg.fossilMirrorRun("test", time.Now())
	if n := len(r.pushes()); n != 1 || r.mirror().Fails != 1 {
		t.Errorf("FAIL: a kick inside the backoff pushed again (%d pushes, %d fails)", n, r.mirror().Fails)
	}
	r.rg.fossilMirrorRun("test", time.Now().Add(2*time.Minute))
	if n := len(r.pushes()); n != 2 {
		t.Errorf("FAIL: a kick after the backoff did not push (%d pushes)", n)
	}
	// A change of settings clears the wait: the corrected password is
	// tried at once.
	r.enable(fossilPw)
	if m := r.mirror(); m.Err != "" || m.Fails != 0 {
		t.Errorf("FAIL: a new password did not clear the failure: %+v", m)
	}
	r.rg.fossilMirrorRun("test", time.Now())
	tip, _, _ := r.ix.TipOf("trunk")
	if m := r.mirror(); m.Err != "" || m.Pushed != tip.Tip || !r.has(tip.Tip) {
		t.Errorf("FAIL: after the right password: %+v", m)
	}
	t.Log("ok")
}

// Private and taken down: paused, nothing attempted, and the lifted
// pause resumes by itself.
func TestAFossilMirrorOfAPrivateOrTakenDownRepositoryIsNotPushed(t *testing.T) {
	r := newFossilRig(t)
	r.enable(fossilPw)
	vis := oltp.Private
	if _, err := r.db.PatchRepo("test", oltp.RepoPatch{Visibility: &vis}, "dan", ""); err != nil {
		t.Fatal(err)
	}
	r.rg.fossilMirrorRun("test", time.Now())
	if n := len(r.pushes()); n != 0 {
		t.Errorf("FAIL: a private repository was pushed (%d)", n)
	}
	pub := oltp.Public
	if _, err := r.db.PatchRepo("test", oltp.RepoPatch{Visibility: &pub}, "dan", ""); err != nil {
		t.Fatal(err)
	}
	r.rg.refreshPrivate()
	if err := r.db.SetTakedown(oltp.Takedown{Repo: "test", Strength: oltp.Disabled, Category: "copyright", Actor: "dmestas"}); err != nil {
		t.Fatal(err)
	}
	r.rg.fossilMirrorRun("test", time.Now())
	if n := len(r.pushes()); n != 0 {
		t.Errorf("FAIL: a taken-down repository was pushed (%d)", n)
	}
	t.Log("ok")
}

// A push that ends because the forge is stopping records nothing: the
// stop ended it, and the next start should push without a backoff.
func TestAFossilPushEndedByAStopRecordsNothing(t *testing.T) {
	r := newFossilRig(t)
	r.enable("not-the-password")
	children.Default.Stopping()
	t.Cleanup(children.Default.Reset)
	r.rg.fossilMirrorRun("test", time.Now())
	if m := r.mirror(); m.Err != "" || m.Fails != 0 {
		t.Errorf("FAIL: a stop was recorded as a failure: %q, %d", m.Err, m.Fails)
	}
	t.Log("ok")
}

// A host the forge may not reach is refused at each push, not only at
// the setting: a name can come to resolve elsewhere.
func TestAFossilPushChecksItsHostAtEachPush(t *testing.T) {
	r := newFossilRig(t)
	r.enable(fossilPw)
	r.rg.fossilMirror.host = func(_ context.Context, host string) error { return fmt.Errorf("private address %s", host) }
	r.rg.fossilMirrorRun("test", time.Now())
	if n := len(r.pushes()); n != 0 {
		t.Errorf("FAIL: pushed to a refused host (%d)", n)
	}
	if m := r.mirror(); m.Fails != 1 || m.Err == "" {
		t.Errorf("FAIL: a refused host is not recorded: %+v", m)
	}
	t.Log("ok")
}

// A password that is wrong, to a server that refuses it or one that is
// not there, appears in no recorded error, no log line and no settings
// log row: a failure is the one time zit's own words are logged.
func TestAFailedFossilPushNeverEchoesThePassword(t *testing.T) {
	const distinctive = "distinct-pw-5e8a41c7"
	r := newFossilRig(t)
	closed := freePort(t) // nothing listens there once the probe closed it
	for label, remote := range map[string]string{
		"a wrong password":    r.remote,
		"an unreachable port": fmt.Sprintf("http://127.0.0.1:%d/", closed),
	} {
		if _, err := r.db.SetFossilMirror("test", remote, fossilUser, distinctive, "dan", ""); err != nil {
			t.Fatal(err)
		}
		r.rg.fossilMirrorRun("test", time.Now())
		m := r.mirror()
		if m.Fails != 1 || m.Err == "" {
			t.Fatalf("FAIL: %s was not recorded as a failure: %+v", label, m)
		}
		if strings.Contains(m.Err, distinctive) {
			t.Errorf("FAIL: %s: the password is in the recorded error", label)
		}
		if strings.Contains(r.logged.String(), distinctive) {
			t.Errorf("FAIL: %s: the password is in the log: %s", label, r.logged.String())
		}
		if !strings.Contains(r.logged.String(), "fossil mirror: test:") {
			t.Errorf("FAIL: %s: the failure was not logged, so the log check proves nothing", label)
		}
		r.logged.Reset()
	}
	for _, c := range func() []oltp.SettingsChange { l, _ := r.db.SettingsLog("test", 50); return l }() {
		if strings.Contains(c.Was+c.Now, distinctive) {
			t.Errorf("FAIL: the password is in the settings log: %+v", c)
		}
	}
	for _, l := range r.pushes() {
		if strings.Contains(l, distinctive) {
			t.Errorf("FAIL: the password is in argv: %q", l)
		}
	}
	t.Log("ok")
}

// A push that read the mirror before it was changed is never handed the
// new password, so a password entered for a new remote cannot reach the
// old one, and it records nothing.
func TestAFossilPushNeverSendsANewPasswordToTheOldRemote(t *testing.T) {
	r := newFossilRig(t)
	for label, change := range map[string]func() error{
		"a remote and password change": func() error {
			_, err := r.db.SetFossilMirror("test", "https://new.example.org/x", "other", "new-pw-for-new-remote", "dan", "")
			return err
		},
		"a password-only change": func() error {
			_, err := r.db.SetFossilMirror("test", "https://new.example.org/x", "other", "newer-pw", "dan", "")
			return err
		},
	} {
		snap := r.enable("old-pw")
		if label == "a password-only change" {
			// The snapshot is of the remote the change keeps.
			var err error
			if snap, err = r.db.SetFossilMirror("test", "https://new.example.org/x", "other", "old-pw", "dan", ""); err != nil {
				t.Fatal(err)
			}
		}
		if err := change(); err != nil {
			t.Fatal(err)
		}
		called := false
		why, decided := r.rg.fossilMirrorPush("test", snap.Remote, snap.User, snap.Gen, func(_ context.Context, _, _ string, env []string) (string, error) {
			called = true
			return "", nil
		})
		if called || decided || why != "" {
			t.Errorf("FAIL: %s: a stale run pushed (called %v, decided %v, %q)", label, called, decided, why)
		}
	}
	t.Log("ok")
}

// A server whose certificate cannot be verified is not "could not be
// reached": zit says the same for both, so the forge asks the host.
func TestAFossilMirrorToAnUntrustedCertificateSaysSo(t *testing.T) {
	r := newFossilRig(t)
	srv := httptest.NewTLSServer(http.NotFoundHandler()) // a self-signed certificate
	t.Cleanup(srv.Close)
	if _, err := r.db.SetFossilMirror("test", srv.URL+"/x", fossilUser, fossilPw, "dan", ""); err != nil {
		t.Fatal(err)
	}
	r.rg.fossilMirrorRun("test", time.Now())
	if m := r.mirror(); m.Fails != 1 || m.Err != fossilCertificateWords {
		t.Errorf("FAIL: an untrusted certificate: %+v", m)
	}
	if !certificateRefused(context.Background(), srv.URL+"/x", false) || certificateRefused(context.Background(), r.remote, false) ||
		certificateRefused(context.Background(), "https://127.0.0.1:1/x", false) {
		t.Error("FAIL: the certificate probe tells the cases wrong")
	}
	// With the address guard on, the probe never connects to a private
	// address, so the local server's bad certificate is not even seen.
	if certificateRefused(context.Background(), srv.URL+"/x", true) {
		t.Error("FAIL: the guarded certificate probe connected to a loopback address")
	}
	t.Log("ok")
}