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 := gitMirrorBackoff(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)
}
}