Skip to content
quarry Artifact [fa08117449]
Login

Artifact [fa08117449]

Artifact fa081174495cd0993d004fb2807f3c1219ec41c2829db63cddadf4500ca40b03:


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