package xfer
import (
"bytes"
"crypto/md5"
"crypto/sha3"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
)
// needHorizonServe skips unless the zit on PATH serves horizons with a
// history URL (zit 0.7.1): the flag and the seed refusal came together.
func needHorizonServe(t *testing.T) {
t.Helper()
out, err := exec.Command("zit", "serve", "--help").CombinedOutput()
if err != nil && len(out) == 0 {
t.Skip("zit not on PATH")
}
if !bytes.Contains(out, []byte("--horizon-url")) {
t.Skip("zit serve has no --horizon-url (zit 0.7.1)")
}
}
// A repository with three check-ins of one file, so a horizon at the
// tip leaves history below it.
func horizonOrigin(t *testing.T) string {
t.Helper()
origin := t.TempDir()
zrun(t, origin, "init")
for i := 1; i <= 3; i++ {
if err := os.WriteFile(filepath.Join(origin, "f"), []byte(fmt.Sprintf("%d\n", i)), 0o644); err != nil {
t.Fatal(err)
}
if i == 1 {
zrun(t, origin, "add", "f")
}
zrun(t, origin, "commit", "-m", fmt.Sprintf("c%d", i))
}
return origin
}
// A horizon clone through the front is told where the rest of the
// history answers (RFC 0024 rung 3): the child is started with the
// repository's history URL, and its horizon-served pragma carries it.
func TestAHorizonCloneLearnsTheHistoryURL(t *testing.T) {
needHorizonServe(t)
origin := horizonOrigin(t)
const want = "https://forge.test/api/v1/r/alpha/history"
f := New()
defer f.Close()
f.Register("alpha", origin)
f.SetHorizonURL(func(name string) string {
if name != "alpha" {
return ""
}
return want
})
var mu sync.Mutex
var replies [][]byte
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := httptest.NewRecorder()
f.ServeHTTP(rec, r)
mu.Lock()
replies = append(replies, decodeContainer(rec.Body.Bytes()))
mu.Unlock()
for k, v := range rec.Header() {
w.Header()[k] = v
}
w.WriteHeader(rec.Code)
w.Write(rec.Body.Bytes())
}))
srv := httptest.NewServer(mux)
defer srv.Close()
work := t.TempDir()
zrun(t, work, "clone", "--horizon=tip", srv.URL+"/r/alpha", filepath.Join(work, "c"))
mu.Lock()
defer mu.Unlock()
var served []string
for _, r := range replies {
for _, line := range strings.Split(string(r), "\n") {
if strings.HasPrefix(line, "pragma horizon-served ") {
served = append(served, line)
}
}
}
if len(served) == 0 {
t.Fatalf("FAIL: no reply said which horizon it served")
}
for _, line := range served {
if f := strings.Fields(line); len(f) != 4 || f[3] != want {
t.Errorf("FAIL: %q does not name the history URL", line)
}
}
if !t.Failed() {
t.Logf("ok: %d replies named the history URL", len(served))
}
}
// A seed is private to the clone that holds it. A push carrying one
// through the front gets the child's refusal, and nothing from that
// request is stored, the ordinary file beside it included (rung 5).
//
// A real clone never offers one: zit keeps a seed out of the artifact
// store, and only a structural horizon has one. So the test builds the
// control artifact a forged or broken client would send.
func TestAPushedSeedIsRefusedAndNothingIsStored(t *testing.T) {
needHorizonServe(t)
origin := horizonOrigin(t)
code, err := ProjectCode(origin)
if err != nil {
t.Fatal(err)
}
tipHash := strings.Fields(zrun(t, origin, "info", "trunk"))[0]
if len(tipHash) != 64 {
t.Fatalf("no full trunk hash in zit info: %q", tipHash)
}
f := New()
defer f.Close()
f.Register("alpha", origin)
mux := http.NewServeMux()
mux.Handle("POST /r/{repo}", f)
srv := httptest.NewServer(mux)
defer srv.Close()
blob := []byte("pushed beside a seed\n")
seed := controlArtifact("D 2026-10-24T00:00:00.000\nT +horizon-seed " + tipHash + "\nU zit-seed\n")
body := "push " + code + " " + code + "\n" +
pushedFile(blob) + pushedFile(seed)
reply, err := postSync(srv.URL+"/r/alpha", []byte(body), "")
if err != nil {
t.Fatal(err)
}
if !bytes.HasPrefix(reply, []byte(`error seed\srefused`)) {
t.Errorf("FAIL: a pushed seed was answered %q", reply)
}
for _, art := range [][]byte{blob, seed} {
if err := zrunErr(origin, "artifact", sha3hex(art)); err == nil {
t.Errorf("FAIL: %s was stored from a refused request", sha3hex(art)[:12])
}
}
// The same file pushed alone is stored: the check above can fail.
if _, err := postSync(srv.URL+"/r/alpha", []byte("push "+code+" "+code+"\n"+pushedFile(blob)), ""); err != nil {
t.Fatal(err)
}
if err := zrunErr(origin, "artifact", sha3hex(blob)); err != nil {
t.Errorf("FAIL: the file pushed alone was not stored either; the test proves nothing")
}
if !t.Failed() {
t.Log("ok: the seed was refused and nothing from its request was stored")
}
}
func sha3hex(b []byte) string {
h := sha3.Sum256(b)
return hex.EncodeToString(h[:])
}
// controlArtifact closes cards with their Z checksum.
func controlArtifact(cards string) []byte {
sum := md5.Sum([]byte(cards))
return []byte(cards + "Z " + hex.EncodeToString(sum[:]) + "\n")
}
func pushedFile(content []byte) string {
return fmt.Sprintf("file %s %d\n%s", sha3hex(content), len(content), content)
}