Skip to content
quarry Artifact [7076d2dbc3]
Login

Artifact [7076d2dbc3]

Artifact 7076d2dbc38ea89f15082ec35abbdaceac4847c60737c102f3d8570eb8ddfb3a:


package main

import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// sshStub serves a push from bare repositories under base, the way a
// Git host would over ssh: git hands it the host and the command last.
// Each call appends what it was given to the log: the -F file, the key
// file's mode, and the key path, so a test can check the key existed
// only for the push.
const sshStub = `#!/bin/sh
key=
while [ $# -gt 2 ]; do
  case "$1" in
    -i) key=$2; shift ;;
    -F) echo "F=$2" >> "$STUB_LOG"; shift ;;
  esac
  shift
done
echo "key=$key mode=$(ls -l "$key" | cut -c1-10)" >> "$STUB_LOG"
[ -n "$STUB_DENY" ] && { echo "git@$1: Permission denied (publickey)." >&2; exit 255; }
cd "$STUB_BASE" && eval "$2"
`

type pushRig struct {
	*closeRig
	remote, log string
}

func newPushRig(t *testing.T) pushRig {
	t.Helper()
	for _, bin := range []string{"git", "zit"} {
		if _, err := exec.LookPath(bin); err != nil {
			t.Skip(bin + " not on PATH")
		}
	}
	if err := exec.Command("zit", "git", "export", "--help").Run(); err != nil {
		t.Skip("this zit has no git export")
	}
	r := newCloseRig(t)
	if err := r.db.AddRepo("test", r.dir, "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if _, err := r.db.SetGitMirror("test", "[email protected]:o/r.git", "", "dan", ""); err != nil {
		t.Fatal(err)
	}
	base := t.TempDir()
	remote := filepath.Join(base, "o", "r.git")
	if out, err := exec.Command("git", "init", "-q", "--bare", remote).CombinedOutput(); err != nil {
		t.Fatalf("%v: %s", err, out)
	}
	stub := filepath.Join(t.TempDir(), "ssh")
	if err := os.WriteFile(stub, []byte(sshStub), 0o755); err != nil {
		t.Fatal(err)
	}
	log := filepath.Join(t.TempDir(), "ssh.log")
	t.Setenv("STUB_BASE", base)
	t.Setenv("STUB_LOG", log)
	r.rg.dir = t.TempDir()
	r.rg.gitMirror.ssh = stub
	return pushRig{closeRig: r, remote: remote, log: log}
}

// remoteLog is the remote main's subjects, newest first.
func (r pushRig) remoteLog() []string {
	out, err := exec.Command("git", "-C", r.remote, "log", "--format=%s", "main").Output()
	if err != nil {
		return nil
	}
	return strings.Fields(strings.TrimSpace(string(out)))
}

func (r pushRig) calls() []string {
	b, _ := os.ReadFile(r.log)
	var keys []string
	for _, l := range strings.Split(string(b), "\n") {
		if strings.HasPrefix(l, "key=") {
			keys = append(keys, l)
		}
	}
	return keys
}

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

// RFC 0022 rung 2: a mirror pushes trunk after it moves, with the key
// on disk only for the push and no ssh config read; a remote with
// history the export did not write is refused, never forced, and
// retried after a backoff; a private repository is paused.
func TestAMirrorPushesTrunkAndNeverForces(t *testing.T) {
	r := newPushRig(t)
	now := time.Now()
	stale := filepath.Join(r.rg.dir, "gitmirror", "keys", "run-crashed", "key")
	oldExport := filepath.Join(r.rg.dir, "gitmirror", "exports", "test", "0000.git")
	for _, d := range []string{filepath.Dir(stale), oldExport} {
		if err := os.MkdirAll(d, 0o700); err != nil {
			t.Fatal(err)
		}
	}
	if err := os.WriteFile(stale, []byte("old key"), 0o600); err != nil {
		t.Fatal(err)
	}

	r.rg.gitMirrorSweep() // what a start does
	r.rg.gitMirrorRun("test", now)
	for _, p := range []string{stale, oldExport} {
		if _, err := os.Stat(p); !os.IsNotExist(err) {
			t.Errorf("FAIL: %s outlived the first push: %v", p, err)
		}
	}
	if got := r.remoteLog(); len(got) != 1 || got[0] != "first" {
		t.Fatalf("FAIL: the first push left the remote at %v (err %q)", got, r.mirror().Err)
	}
	tip, _, _ := r.ix.TipOf("trunk")
	if m := r.mirror(); m.Pushed != tip.Tip || m.Err != "" {
		t.Errorf("FAIL: after the push: pushed %q err %q", m.Pushed, m.Err)
	}
	calls := r.calls()
	if len(calls) == 0 || !strings.Contains(calls[0], "mode=-rw-------") {
		t.Fatalf("FAIL: the key file during the push: %v", calls)
	}
	keyPath := strings.Fields(strings.TrimPrefix(calls[0], "key="))[0]
	if _, err := os.Stat(keyPath); !os.IsNotExist(err) {
		t.Errorf("FAIL: the key file outlived the push: %v", err)
	}
	if b, _ := os.ReadFile(r.log); !strings.Contains(string(b), "F=/dev/null") {
		t.Error("FAIL: ssh was not told to read no config")
	}

	// Nothing new: no push at all.
	r.rg.gitMirrorRun("test", now)
	if n := len(r.calls()); n != 1 {
		t.Errorf("FAIL: an unmoved trunk pushed again (%d calls)", n)
	}

	r.commit("b.txt", "second", "")
	r.sync()
	r.rg.gitMirrorRun("test", now)
	if got := r.remoteLog(); len(got) != 2 || got[0] != "second" {
		t.Fatalf("FAIL: after trunk moved the remote is %v", got)
	}

	// Someone pushes to the mirror directly.
	work := t.TempDir()
	for _, args := range [][]string{
		{"clone", "-q", "-b", "main", r.remote, work},
		{"-C", work, "-c", "user.email=x@y", "-c", "user.name=x", "commit", "-q", "--allow-empty", "-m", "foreign"},
		{"-C", work, "push", "-q", "origin", "main"},
	} {
		if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
			t.Fatalf("git %v: %v %s", args, err, out)
		}
	}
	r.commit("c.txt", "third", "")
	r.sync()
	r.rg.gitMirrorRun("test", now)
	m := r.mirror()
	if !strings.Contains(m.Err, "has history this repository does not") || m.Fails != 1 {
		t.Fatalf("FAIL: a diverged remote: err %q fails %d", m.Err, m.Fails)
	}
	if got := r.remoteLog(); got[0] != "foreign" {
		t.Fatalf("FAIL: the remote was forced: %v", got)
	}
	before := len(r.calls())
	r.rg.gitMirrorRun("test", now.Add(30*time.Second))
	if len(r.calls()) != before {
		t.Error("FAIL: a failure was retried inside its backoff")
	}
	r.rg.gitMirrorRun("test", now.Add(2*time.Minute))
	if len(r.calls()) != before+1 || r.mirror().Fails != 2 {
		t.Errorf("FAIL: the retry after the backoff: %d calls, %d fails", len(r.calls())-before, r.mirror().Fails)
	}

	// Private: paused, and nothing is attempted.
	vis := oltp.Private
	if _, err := r.db.PatchRepo("test", oltp.RepoPatch{Visibility: &vis}, "dan", ""); err != nil {
		t.Fatal(err)
	}
	// No cache refresh: the push reads visibility from the database.
	before = len(r.calls())
	r.rg.gitMirrorRun("test", now.Add(3*time.Hour))
	if len(r.calls()) != before {
		t.Error("FAIL: a private repository was pushed")
	}

	// Taken down: paused the same way.
	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.gitMirrorRun("test", now.Add(6*time.Hour))
	if len(r.calls()) != before {
		t.Error("FAIL: a taken-down repository was pushed")
	}
}

// A refused key is said as such.
func TestARefusedDeployKeyIsSaidInWords(t *testing.T) {
	r := newPushRig(t)
	t.Setenv("STUB_DENY", "1")
	r.rg.gitMirrorRun("test", time.Now())
	if m := r.mirror(); !strings.Contains(m.Err, "refused the deploy key") || m.Pushed != "" {
		t.Errorf("FAIL: a refused key: err %q pushed %q", m.Err, m.Pushed)
	}
}

// Triggers while a run is going make exactly one more run.
func TestMirrorTriggersCoalesce(t *testing.T) {
	rg := newTestRegistrar(t, 25)
	release := make(chan struct{})
	runs := make(chan struct{}, 10)
	rg.gitMirror.run = func(string, time.Time) {
		runs <- struct{}{}
		<-release
	}
	rg.gitMirrorKick("r")
	<-runs // the first run is under way
	rg.gitMirrorKick("r")
	rg.gitMirrorKick("r")
	rg.gitMirrorKick("r")
	close(release)
	select {
	case <-runs: // the one more run
	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.gitMirror.mu.Lock()
	idle := !rg.gitMirror.running["r"]
	rg.gitMirror.mu.Unlock()
	if !idle {
		t.Error("FAIL: the repository is still marked running")
	}
}

func TestMirrorBackoffDoublesToAnHour(t *testing.T) {
	for fails, want := range map[int]time.Duration{1: time.Minute, 2: 2 * time.Minute, 3: 4 * time.Minute, 7: time.Hour, 40: time.Hour} {
		if got := oltp.MirrorBackoff(fails); got != want {
			t.Errorf("FAIL: backoff(%d) = %v, want %v", fails, got, want)
		}
	}
}

func TestMirrorFailuresAreWords(t *testing.T) {
	for out, want := range map[string]string{
		"ssh: Could not resolve hostname h.example: nodename nor servname": "could not be reached",
		" ! [rejected]        trunk -> main (fetch first)":                 "has history",
		"Host key verification failed.":                                    "not the one it showed",
		"ERROR: Repository not found.":                                     "no such repository",
		"something odd\nhint: ignore me":                                   "the push failed: something odd",
	} {
		if got := gitMirrorWords(out, "main"); !strings.Contains(got, want) {
			t.Errorf("FAIL: %q -> %q, want %q", out, got, want)
		}
	}
}

// A push that fails while the forge is stopping records nothing: the
// stop ended it, and the next start should push without a backoff.
func TestAPushEndedByAStopRecordsNothing(t *testing.T) {
	r := newPushRig(t)
	t.Setenv("STUB_DENY", "1")
	children.Default.Stopping()
	t.Cleanup(children.Default.Reset)
	r.rg.gitMirrorRun("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)
	}
}