Skip to content
quarry Artifact [1e87d3d83e]
Login

Artifact [1e87d3d83e]

Artifact 1e87d3d83e0be6d522b6c864bccd4092696637b7d975090604cf39a8d2b7a979:


package main

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

	"craftdesign.group/quarry/internal/chat"
	"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", "127.0.0.1") || 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 with any private address among its answers is refused before
// zit runs: nothing is dialled, and the failure is in words.
func TestAFossilPushToAPrivateAddressIsRefusedBeforeZitRuns(t *testing.T) {
	r := newFossilRig(t)
	r.rg.fossilMirror.resolve = chat.PublicAddrs
	for _, host := range []string{"10.0.0.5", "127.0.0.1", "169.254.169.254", "100.64.0.9"} {
		if _, err := r.db.SetFossilMirror("test", "https://"+host+"/x", fossilUser, fossilPw, "dan", ""); err != nil {
			t.Fatal(err)
		}
		r.rg.fossilMirrorRun("test", time.Now())
		if n := len(r.pushes()); n != 0 {
			t.Errorf("FAIL: %s: zit was run for a private address (%d)", host, n)
		}
		if m := r.mirror(); m.Fails != 1 || m.Err == "" {
			t.Errorf("FAIL: %s: a refused host is not recorded: %+v", host, 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)
	}
	// The probe connects to the address it is given and to nothing else:
	// the name in the URL is never looked up.
	if !certificateRefused(context.Background(), srv.URL+"/x", "127.0.0.1") || certificateRefused(context.Background(), r.remote, "127.0.0.1") ||
		certificateRefused(context.Background(), "https://127.0.0.1:1/x", "127.0.0.1") {
		t.Error("FAIL: the certificate probe tells the cases wrong")
	}
	if certificateRefused(context.Background(), "https://no-such-name.invalid:"+strings.TrimPrefix(srv.URL, "https://127.0.0.1:")+"/x", "") {
		t.Error("FAIL: the probe dialled with no address vetted")
	}
	t.Log("ok")
}

// scriptedResolver answers each lookup from the next list and counts
// them.
type scriptedResolver struct {
	mu      sync.Mutex
	answers [][]string
	calls   int
}

func (s *scriptedResolver) resolve(_ context.Context, _ string) ([]net.IPAddr, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	a := s.answers[min(s.calls, len(s.answers)-1)]
	s.calls++
	var out []net.IPAddr
	for _, ip := range a {
		out = append(out, net.IPAddr{IP: net.ParseIP(ip)})
	}
	return out, nil
}

func (s *scriptedResolver) count() int {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.calls
}

// named is the rig's server under a name, so a push has something to
// pin: localhost is the one name the forge's outbound rule lets reach
// this machine over http.
func (r fossilRig) named() string {
	return strings.Replace(r.remote, "127.0.0.1", "localhost", 1)
}

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

// The push is pinned to the address the forge vetted: the name is
// resolved once, whatever it would answer next, and zit is handed
// --resolve with that address so it never looks the name up; the
// password stays out of argv.
func TestAFossilPushIsPinnedToTheAddressItVetted(t *testing.T) {
	r := newFossilRig(t)
	res := &scriptedResolver{answers: [][]string{{"127.0.0.1"}, {"10.0.0.5"}}}
	r.rg.fossilMirror.resolve = res.resolve
	named := r.named()
	r.enableAt(named)
	r.commit("b.txt", "second", "")
	r.sync()
	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.Fatalf("FAIL: the pinned push: %+v", m)
	}
	if n := res.count(); n != 1 {
		t.Errorf("FAIL: the host was resolved %d times, want once", n)
	}
	pushes := r.pushes()
	u, _ := url.Parse(named)
	pin := "[--resolve] [localhost:" + u.Port() + ":127.0.0.1]"
	if len(pushes) != 1 || !strings.Contains(pushes[0], pin) {
		t.Errorf("FAIL: argv lacks the vetted pin %s: %v", pin, pushes)
	}
	for _, l := range pushes {
		if strings.Contains(l, fossilPw) || strings.Contains(l, "--password") || !strings.Contains(l, "pwenv=set") {
			t.Errorf("FAIL: the password and argv: %q", l)
		}
	}
	t.Log("ok")
}

// An address that cannot be connected to falls through to the next one
// the resolver gave, each already vetted, and the argv of each attempt
// pins its own. The server listens on 127.0.0.1 only, so ::1 is refused
// at once; the second attempt reaching it is what is asserted.
func TestAFossilPushTriesTheNextVettedAddress(t *testing.T) {
	r := newFossilRig(t)
	var hits atomic.Int32
	l, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	srv := &httptest.Server{Listener: l, Config: &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		hits.Add(1)
		http.Error(w, "no", http.StatusInternalServerError)
	})}}
	srv.Start()
	t.Cleanup(srv.Close)
	res := &scriptedResolver{answers: [][]string{{"::1", "127.0.0.1"}}}
	r.rg.fossilMirror.resolve = res.resolve
	u, _ := url.Parse(srv.URL)
	r.enableAt("http://localhost:" + u.Port() + "/")
	r.rg.fossilMirrorRun("test", time.Now())
	pushes := r.pushes()
	if len(pushes) != 2 || !strings.Contains(pushes[0], ":::1]") || !strings.Contains(pushes[1], ":127.0.0.1]") {
		t.Errorf("FAIL: the attempts: %v", pushes)
	}
	if hits.Load() == 0 {
		t.Error("FAIL: the second address was never reached")
	}
	if n := res.count(); n != 1 {
		t.Errorf("FAIL: resolved %d times, want once", n)
	}
	t.Log("ok")
}

// A server that redirects the push elsewhere is not followed: zit says
// so, the failure is in words that name only the remote's host, and the
// redirect target is never contacted.
func TestAFossilPushIsNotFollowedToARedirect(t *testing.T) {
	r := newFossilRig(t)
	var hits atomic.Int32
	target := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { hits.Add(1) }))
	t.Cleanup(target.Close)
	redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		http.Redirect(w, req, "http://redirect-user:redirect-secret@"+strings.TrimPrefix(target.URL, "http://")+"/x", http.StatusFound)
	}))
	t.Cleanup(redirector.Close)
	r.enableAt(redirector.URL + "/")
	r.rg.fossilMirrorRun("test", time.Now())
	m := r.mirror()
	if m.Fails != 1 || m.Err != fossilMirrorWords("zit push: the peer redirected to http://x/y, another host or port; not followed", "127.0.0.1") {
		t.Fatalf("FAIL: a redirect: %+v", m)
	}
	for _, leak := range []string{"redirect-secret", "redirect-user", strings.TrimPrefix(target.URL, "http://")} {
		if strings.Contains(m.Err, leak) {
			t.Errorf("FAIL: the failure names the redirect's %q", leak)
		}
	}
	if n := hits.Load(); n != 0 {
		t.Errorf("FAIL: the redirect target received %d requests", n)
	}
	t.Log("ok")
}

// The two refusals zit 0.14.5 makes have their own words.
func TestFossilMirrorWordsForRedirectAndMissingPin(t *testing.T) {
	redirect := fossilMirrorWords("zit push: the peer redirected to http://u:[email protected]/x, another host or port; not followed", "fossil.example.org")
	if !strings.Contains(redirect, "fossil.example.org") || strings.Contains(redirect, "evil") || strings.Contains(redirect, "u:p") {
		t.Errorf("FAIL: the redirect words: %q", redirect)
	}
	pin := fossilMirrorWords("zit push: no --resolve entry for fossil.example.org:443; not connecting", "fossil.example.org")
	if pin == fossilMirrorWords("", "fossil.example.org") || pin == redirect {
		t.Errorf("FAIL: the missing pin has no words of its own: %q", pin)
	}
	t.Log("ok")
}

func TestFossilPinsNameTheEffectivePort(t *testing.T) {
	for raw, want := range map[string][]string{
		"https://f.example.org/x":      {"f.example.org:443:93.184.216.34"},
		"https://f.example.org:8443/x": {"f.example.org:8443:93.184.216.34"},
		"http://localhost/x":           {"localhost:80:93.184.216.34", "localhost:443:93.184.216.34"},
		"http://localhost:8080/x":      {"localhost:8080:93.184.216.34"},
		"http://127.0.0.1:8080/x":      nil,
		"https://[::1]:8443/x":         nil,
	} {
		u, _ := url.Parse(raw)
		got := fossilPins(u, net.ParseIP("93.184.216.34"))
		if strings.Join(got, " ") != strings.Join(want, " ") {
			t.Errorf("FAIL: %s pins %v, want %v", raw, got, want)
		}
	}
	t.Log("ok")
}