package api
import (
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
"craftdesign.group/quarry/internal/zitcli"
)
// /api/v1/version says which zit the forge needs and whether the one it
// runs falls short, so an ordering slip in a deploy is visible there.
func TestTheVersionReportNamesTheZitGap(t *testing.T) {
for _, c := range []struct {
found string
short bool
}{{"0.3.30", true}, {"0.3.31", false}, {"0.4.0", false}} {
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", ZitVersion: c.found}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(as("", ""), "meta.version", versionIn{})
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(out)
var body map[string]any
if err := json.Unmarshal(b, &body); err != nil {
t.Fatal(err)
}
if body["zitMin"] != zitcli.MinVersion || body["zitTooOld"] != c.short {
t.Errorf("FAIL: zit %s: body %s; want zitMin %s, zitTooOld %v", c.found, b, zitcli.MinVersion, c.short)
}
}
}
func assertZitTooOld(t *testing.T, err error, found string) {
t.Helper()
if !errors.Is(err, ops.ErrUnavailable) {
t.Fatalf("FAIL: accept on an old zit = %v; want unavailable (503)", err)
}
var ref *ops.Refusal
if !errors.As(err, &ref) || ref.Code != RefusalZitTooOld {
t.Fatalf("FAIL: no %q refusal: %#v", RefusalZitTooOld, err)
}
if ref.Fields["found"] != found || ref.Fields["required"] != zitcli.MinVersion {
t.Fatalf("FAIL: fields %v; want found %s, required %s", ref.Fields, found, zitcli.MinVersion)
}
if !strings.Contains(err.Error(), found) || !strings.Contains(err.Error(), zitcli.MinVersion) {
t.Fatalf("FAIL: the message does not name both versions: %v", err)
}
}
// Known at startup: the accept is refused before anything is written.
func TestAnAcceptOnAKnownOldZitIsUnavailableNamingBothVersions(t *testing.T) {
dir, id, ix, pipeSync := acceptStore(t)
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", ZitVersion: "0.3.30", Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}); err != nil {
t.Fatal(err)
}
_, err := r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id})
assertZitTooOld(t, err, "0.3.30")
if out := zrun(t, dir, "landing", "ls"); !strings.Contains(out, id[:10]) {
t.Fatalf("FAIL: the landing is not open after a refused accept:\n%s", out)
}
t.Logf("ok: %v", err)
}
// Found only when zit refuses the flag: the same refusal, with the
// startup probe's answer unknown.
func TestAnAcceptOnAZitThatRefusesTheFlagIsUnavailable(t *testing.T) {
dir, id, ix, pipeSync := acceptStore(t)
real, err := exec.LookPath("zit")
if err != nil {
t.Skip("zit not on PATH")
}
bin := t.TempDir()
script := "#!/bin/sh\nif [ \"$1\" = landing ] && [ \"$2\" = accept ]; then\n" +
" echo \"zit landing: unknown option '--expect-tip'\" >&2; exit 2\nfi\nexec '" + real + "' \"$@\"\n"
if err := os.WriteFile(filepath.Join(bin, "zit"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}); err != nil {
t.Fatal(err)
}
_, err = r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id})
assertZitTooOld(t, err, "unknown")
t.Logf("ok: %v", err)
}
// The startup probe read nothing. That is not a zit known to be new
// enough, so the accept is refused before anything is written; each
// accept probes again, and the first that reads a good version goes on.
func TestAnAcceptOnAnUnknownZitIsRefusedUntilAProbeSucceeds(t *testing.T) {
dir, id, ix, pipeSync := acceptStore(t)
probed := ""
orig := probeZit
probeZit = func() string { return probed }
t.Cleanup(func() { probeZit = orig })
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{
"test": {Index: ix, Zit: zitcli.New(dir), Sync: pipeSync}})}); err != nil {
t.Fatal(err)
}
_, err := r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id})
assertZitTooOld(t, err, "unknown")
if out := zrun(t, dir, "landing", "ls"); !strings.Contains(out, id[:10]) {
t.Fatalf("FAIL: the landing is not open after a refused accept:\n%s", out)
}
probed = zitcli.MinVersion
if _, err := r.Invoke(as("dan", "i"), "landing.accept", landingSayIn{Repo: "test", ID: id}); err != nil {
t.Fatalf("FAIL: the accept after a good probe was refused: %v", err)
}
t.Log("ok")
}