package xfer
import (
"bytes"
"log"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
"sync"
"testing"
)
// THE PUSH LISTENER HEARS A PUSH THAT LANDED, AND NOTHING ELSE.
//
// A tracking store that takes a push has diverged from its upstream,
// and the only place quarry knows a push was ADMITTED — not refused by
// the posture gate, not refused by the store — is the fronter, after
// the child's reply came back clean (5d40f151). A clone and a pull are
// not pushes; a refused push is not a push that landed.
func TestThePushListenerHearsOnlyAPushThatLanded(t *testing.T) {
if _, err := exec.LookPath("zit"); err != nil {
t.Skip("zit not on PATH")
}
origin := t.TempDir()
zrun(t, origin, "init")
if err := os.WriteFile(filepath.Join(origin, "README.md"), []byte("# pushed\n"), 0o644); err != nil {
t.Fatal(err)
}
zrun(t, origin, "add", "README.md")
zrun(t, origin, "commit", "-m", "first")
var mu sync.Mutex
var heard []string
f := New()
defer f.Close()
f.Register("alpha", origin)
f.SetPushed(func(repo string) {
mu.Lock()
defer mu.Unlock()
heard = append(heard, repo)
})
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", f)
srv := httptest.NewServer(mux)
defer srv.Close()
work := t.TempDir()
clone := filepath.Join(work, "clone")
zrun(t, work, "clone", srv.URL+"/r/alpha", clone)
zrun(t, clone, "pull")
mu.Lock()
n := len(heard)
mu.Unlock()
if n != 0 {
t.Fatalf("FAIL: a clone and a pull were heard as pushes: %v", heard)
}
// Logged from here on: commit autosyncs, so its push is the one
// that carries the artifacts.
logged := &lockedBuffer{}
log.SetOutput(logged)
defer log.SetOutput(os.Stderr)
if err := os.WriteFile(filepath.Join(clone, "NOTES.md"), []byte("more\n"), 0o644); err != nil {
t.Fatal(err)
}
zrun(t, clone, "add", "NOTES.md")
zrun(t, clone, "commit", "-m", "second")
zrun(t, clone, "push")
mu.Lock()
got := append([]string(nil), heard...)
mu.Unlock()
// A push is several rounds and each admitted round carrying a push
// is heard: at least one, all on alpha. The listener counts
// rounds, and its consumer treats them as "pushed at least once".
if len(got) == 0 {
t.Fatalf("FAIL: a push that landed was not heard")
}
for _, r := range got {
if r != "alpha" {
t.Fatalf("FAIL: heard %v, want only alpha", got)
}
}
before := len(got)
// A push the posture gate refuses is not heard.
f.SetWriteGate(func(string) (bool, string) { return false, "https://up.example/r/alpha" })
if err := os.WriteFile(filepath.Join(clone, "NOTES.md"), []byte("more still\n"), 0o644); err != nil {
t.Fatal(err)
}
zrun(t, clone, "commit", "-m", "third")
_ = zrunErr(clone, "push")
mu.Lock()
n = len(heard)
mu.Unlock()
if n != before {
t.Fatalf("FAIL: a refused push was heard: %v", heard)
}
// And the log names the push that landed, with the identity the gate
// verified: the record #256 had no way to consult. Checked last, as
// a round is logged after its reply has gone back to the client.
if !regexp.MustCompile(`xfer: alpha: push admitted \(user ""\): [1-9]`).MatchString(logged.String()) {
t.Errorf("FAIL: an admitted push left no log line:\n%s", logged.String())
}
t.Log("ok: one push landed, one push heard")
}
// lockedBuffer is a log destination the server's goroutines may write
// while the test reads it.
type lockedBuffer struct {
mu sync.Mutex
b bytes.Buffer
}
func (l *lockedBuffer) Write(p []byte) (int, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.Write(p)
}
func (l *lockedBuffer) String() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.String()
}
// zrunErr runs zit and returns its error instead of failing the test,
// for the push that is meant to be refused.
func zrunErr(dir string, args ...string) error {
cmd := exec.Command("zit", args...)
cmd.Dir = dir
return cmd.Run()
}