Skip to content
quarry Artifact [9c8c86d900]
Login

Artifact [9c8c86d900]

Artifact 9c8c86d9006c3e779d361eb0858ec2b255bb256a74b5504aadb915524ca0dafa:


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)
		}
	}
	if strings.Contains(r.logged.String(), fossilPw) {
		t.Errorf("FAIL: the password is in a log line: %s", r.logged.String())
	}

	// 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)
	}
	if strings.Contains(m.Err, "not-the-password") || strings.Contains(r.logged.String(), "not-the-password") {
		t.Error("FAIL: the wrong password was echoed")
	}
	// The retry tick waits out the backoff.
	if fossilMirrorDue(m.PushedAt, m.ErrAt, m.Fails, time.Now()) {
		t.Error("FAIL: the tick would retry inside the backoff")
	}
	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")
}