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")
}