//go:build !windows // Tagged !windows. It always WAS unix-only — it calls faTestUser, // which lives in freeagent_drop_test.go behind `//go:build !windows` — so this // file has never compiled for Windows and `GOOS=windows go vet ./...` has been // red on it the whole time. The tag does not remove any coverage; it states the // dependency that was already there and lets vet run clean on the target, which // is what makes a REAL Windows regression visible instead of being lost in a // failure everyone has learned to scroll past. package main // The managed lane, proven on this box: the artifact fetch with hash // verification, the identity, the manifest rendered from a grant, and — // when a real freeagent .pyz and python3 are available (FREEAGENT_TEST_PYZ) // — the whole thing end to end: a signed grant with 'search' brings up a // local agent that answers /health and /catalog over mTLS to this box. import ( "crypto/ed25519" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" "io" "net/http" "net/http/httptest" "os" "os/user" "path/filepath" "strconv" "strings" "testing" "time" ) func stubPublish(t *testing.T, pyz []byte, sha string) *httptest.Server { t.Helper() mux := http.NewServeMux() var srv *httptest.Server mux.HandleFunc("/bastion-agent/version.json", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{"version": "9.9.9", "files": map[string]any{ "bastion-agent-9.9.9.pyz": map[string]any{"url": srv.URL + "/bastion-agent/dl/bastion-agent-9.9.9.pyz", "sha256": sha}}}) }) mux.HandleFunc("/bastion-agent/dl/bastion-agent-9.9.9.pyz", func(w http.ResponseWriter, r *http.Request) { w.Write(pyz) }) srv = httptest.NewServer(signedStub(releaseKindAgent, mux)) t.Cleanup(srv.Close) return srv } func withConfigDir(t *testing.T) string { t.Helper() d := t.TempDir() switch { case os.Getenv("XDG_CONFIG_HOME") != "" || true: t.Setenv("XDG_CONFIG_HOME", d) // UserConfigDir on linux t.Setenv("HOME", d) t.Setenv("AppData", d) } // The environment alone does NOT isolate faBaseDir. On a ROOT install it // prefers faStateBase() — "/var/lib/privatae/freeagent" // (freeagent_drop.go:318-326) — which no environment variable reaches. So // `go test` as root wrote a fake runtime into the LIVE bastion's state, and // a later case could return that cached copy instead of exercising the code // under test. Measured: a run left a 27-byte freeagent.pyz and version // 2.2.2 under that base, on a machine with an active bastion. Isolation // is the override the drop tests already use. // The machine marker is machine-scoped, and claim-anchor now consults it // (singleton.go). Without this, an anchor test on a box that HAS a marker // gets the singleton refusal instead of the refusal it is asserting — which // is exactly what happened. oldMarker := markerPathOverride markerPathOverride = filepath.Join(d, "machine.json") t.Cleanup(func() { markerPathOverride = oldMarker }) oldBase := faStateBaseOverride faStateBaseOverride = filepath.Join(d, "state") if err := os.MkdirAll(faStateBaseOverride, 0o755); err != nil { t.Fatal(err) } t.Cleanup(func() { faStateBaseOverride = oldBase }) return d } func TestFaEnsurePyzVerifiesHash(t *testing.T) { withConfigDir(t) body := []byte("#!/usr/bin/env python3\nfake") sum := sha256.Sum256(body) good := hex.EncodeToString(sum[:]) srv := stubPublish(t, body, good) old := faManifestURL faManifestURL = srv.URL + "/bastion-agent/version.json" t.Cleanup(func() { faManifestURL = old }) p, v, err := faEnsurePyz() if err != nil || v != "9.9.9" { t.Fatalf("good hash must install: %v %q", err, v) } got, _ := os.ReadFile(p) if string(got) != string(body) { t.Fatal("installed bytes differ") } // same version again: no re-download needed (still returns the copy) if _, v2, err := faEnsurePyz(); err != nil || v2 != "9.9.9" { t.Fatalf("cached copy: %v", err) } // a tampered file is refused and the old copy stays bad := stubPublish(t, append(body, 'x'), good) faManifestURL = bad.URL + "/bastion-agent/version.json" _ = os.Remove(filepath.Join(filepath.Dir(p), "freeagent.version")) if _, _, err := faEnsurePyz(); err == nil || !strings.Contains(err.Error(), "sha256") { t.Fatalf("hash mismatch must refuse, got %v", err) } // Refused means the installed agent is the one that was verified, byte for // byte: a refusal after the tampered bytes were written would still run them. if got, err := os.ReadFile(p); err != nil || string(got) != string(body) { t.Fatalf("after a refused download the installed agent must be unchanged: %v %q", err, got) } // offline with an installed copy: still served faManifestURL = "http://127.0.0.1:9/nope" _ = atomicWrite(filepath.Join(filepath.Dir(p), "freeagent.version"), []byte("9.9.9\n")) if p2, v3, err := faEnsurePyz(); err != nil || p2 != p || v3 != "9.9.9" { t.Fatalf("offline must fall back to the installed copy: %v", err) } } func TestFaEnsureIdentity(t *testing.T) { dir := t.TempDir() cert, key, _, err := faEnsureIdentity(dir, "store-7") if err != nil { t.Fatal(err) } raw, _ := os.ReadFile(cert) blk, _ := pem.Decode(raw) c, err := x509.ParseCertificate(blk.Bytes) if err != nil || c.Subject.CommonName != "store-7" || !c.IsCA { t.Fatalf("cert: %v cn=%q ca=%v", err, c.Subject.CommonName, c.IsCA) } pool := x509.NewCertPool() pool.AddCert(c) if _, err := c.Verify(x509.VerifyOptions{Roots: pool}); err != nil { t.Fatalf("must be its own root: %v", err) } st, _ := os.Stat(key) if st.Mode().Perm()&0o077 != 0 { t.Fatalf("key must be private, mode %v", st.Mode()) } // idempotent: same files on the second call c2, k2, _, _ := faEnsureIdentity(dir, "store-7") if c2 != cert || k2 != key { t.Fatal("identity must be minted once") } } func TestFaManifestText(t *testing.T) { terms := &dcnTerms{Class: "machine", Root: `C:\Users\user\Documents\context`, Mask: []string{"read", "search", "index"}, Excludes: []string{".ssh", "*.key"}} m := faManifestText("store-1", "store-1", terms, "daily", "age1abc", "qwen3-embedding:4b", 2560) for _, want := range []string{ `roots: ["C:\\Users\\user\\Documents\\context"]`, `exclude: [".ssh", "*.key"]`, "mask: [read, search, index]", "stages: [walk, extract, sanitize, chunk, embed, catalog, seal]", "sweep: daily", "catalog: host", "plaintext: never", "dim: 2560", `model: "qwen3-embedding:4b"`} { if !strings.Contains(m, want) { t.Fatalf("manifest missing %q:\n%s", want, m) } } // no embedding model on the box → no chunk/embed; no recipient → no seal, local catalog m = faManifestText("store-2", "store-2", terms, "", "", "", 0) if !strings.Contains(m, "stages: [walk, extract, sanitize, catalog]") || !strings.Contains(m, "catalog: local") || !strings.Contains(m, "sweep: hourly") { t.Fatalf("degraded manifest wrong:\n%s", m) } // read-only grant: no index → catalog needs index; only walk/extract/sanitize ro := &dcnTerms{Root: "/data", Mask: []string{"read"}} m = faManifestText("s", "s", ro, "15m", "", "m", 4) if !strings.Contains(m, "stages: [walk, extract, sanitize]") || !strings.Contains(m, "mask: [read]") { t.Fatalf("read-only manifest wrong:\n%s", m) } } func TestFaManagedConfigFromPortFile(t *testing.T) { dir := t.TempDir() faMgr.Lock() faMgr.inst[faKey{"dcn-test", 4242}] = &faInstance{StoreID: 4242, Dir: dir, Cert: "c", Key: "k"} faMgr.Unlock() t.Cleanup(func() { faStop("dcn-test", 4242) }) if _, ok := faManagedConfig("dcn-test", 4242); ok { t.Fatal("no port file → not configured") } os.WriteFile(filepath.Join(dir, "port"), []byte("18473\n"), 0o600) c, ok := faManagedConfig("dcn-test", 4242) if !ok || c.Addr != "127.0.0.1:18473" || c.CA != "c" { t.Fatalf("port file → config, got %+v %v", c, ok) } if _, ok := faConfigFor("dcn-test", 4242); !ok { t.Fatal("faConfigFor must prefer the managed instance") } t.Setenv("PRIVATAE_BASTION_AGENT_ADDR", "") if _, ok := faConfigFor("dcn-test", 1); ok { t.Fatal("unknown store with no env → absent") } if !strings.Contains(faAbsentReason("dcn-test", 1), "no local agent") { t.Fatal("absent reason for an unknown store") } } // A beat's freeagent capability and freeagent_ok are its own pairing's agents // only. Probed across every pairing, Host B's beat carried Host A's agent's // health, and with B's freeagent_stores now empty the Host reads the // capability alone (the Host's store-card check: `"freeagent" // in caps and not stores`), so A's serving agent marked every one of B's // stores on this machine as served. func TestHeartbeatProbesOnlyItsOwnPairingsAgents(t *testing.T) { t.Setenv("PRIVATAE_BASTION_AGENT_ADDR", "") // no hand-run agent dcnA, dcnB := t.TempDir(), t.TempDir() faMgr.Lock() // no port file: A's agent is managed and not serving faMgr.inst[faKey{dcnA, 7}] = &faInstance{StoreID: 7, Dir: t.TempDir(), Cert: "a", Key: "a"} faMgr.Unlock() t.Cleanup(func() { faStop(dcnA, 7) }) beat := func(dir string) map[string]any { var body map[string]any raw := buildHeartbeatBodyOn(beatLanes{dcnDir: dir}, "", "box", "linux", "test", "pub", "fp", "", "", false, nil, nil, nil) if err := json.Unmarshal([]byte(raw), &body); err != nil { t.Fatal(err) } return body } // MUTATION: probe every pairing's instances (no k.Dir filter in // faManagedProbe). RESULT: red, B's beat carries freeagent_ok false. if b, ok := beat(dcnB)["freeagent_ok"]; ok { t.Errorf("Host B holds no agent here, yet its beat says freeagent_ok %v", b) } if b, ok := beat(dcnA)["freeagent_ok"]; !ok || b != false { t.Errorf("Host A's beat must carry its own agent's health: %v %v", b, ok) } if b, ok := beat("")["freeagent_ok"]; ok { t.Errorf("a beat with no DCN state probes no managed agent: %v", b) } } // End to end with the real artifact: FREEAGENT_TEST_PYZ=/path/to/freeagent-x.pyz func TestFaManagedEndToEnd(t *testing.T) { pyzPath := os.Getenv("FREEAGENT_TEST_PYZ") if pyzPath == "" { t.Skip("set FREEAGENT_TEST_PYZ to the built freeagent .pyz") } if _, _, err := faPython(); err != nil { t.Skip(err.Error()) } withConfigDir(t) pyz, err := os.ReadFile(pyzPath) if err != nil { t.Fatal(err) } sum := sha256.Sum256(pyz) srv := stubPublish(t, pyz, hex.EncodeToString(sum[:])) old := faManifestURL faManifestURL = srv.URL + "/bastion-agent/version.json" t.Cleanup(func() { faManifestURL = old }) // a corpus and a signed grant with 'search' corpus := t.TempDir() os.WriteFile(filepath.Join(corpus, "note.txt"), []byte("routers and firewalls, mail m@example.com\n"), 0o600) dcnDir := t.TempDir() // As root the agent is DROPPED to the folder's owner (freeagent_drop.go): // the corpus must belong to a real user, and the state base must be a // traversable chain like /var/lib/privatae/freeagent is. var owner *user.User if processIsRoot() { owner = faTestUser(t) if owner == nil { t.Skip("root, and no ordinary user account on this box to drop to") } uid, _ := strconv.Atoi(owner.Uid) gid, _ := strconv.Atoi(owner.Gid) makeTraversable(t, corpus) for _, p := range []string{corpus, filepath.Join(corpus, "note.txt")} { if err := os.Chown(p, uid, gid); err != nil { t.Fatal(err) } } base := t.TempDir() makeTraversable(t, base) oldBase := faStateBaseOverride faStateBaseOverride = base t.Cleanup(func() { faStateBaseOverride = oldBase }) } pub, priv, _ := ed25519.GenerateKey(nil) terms := map[string]any{"kind": "dcn_store_grant", "class": "machine", "server": "box", "root": corpus, "mask": []string{"read", "search", "index"}, "excludes": []string{".git"}, "sweep": "manual", "expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339)} rec := signedRec(t, priv, terms) f := &dcnGrantFile{VerifyPubkey: base64.StdEncoding.EncodeToString(pub), Stores: map[string]dcnGrantRec{"1": rec}} if err := dcnSaveFile(dcnDir, f); err != nil { t.Fatal(err) } faReconcile(dcnDir) // what the daemon does at start t.Cleanup(func() { faStop(dcnDir, 1) }) stateDir, err := faStoreDir(dcnDir, 1) if err != nil { t.Fatal(err) } deadline := time.Now().Add(120 * time.Second) var c freeagentConfig for time.Now().Before(deadline) { faMgr.Lock() inst := faMgr.inst[faKey{dcnDir, 1}] state, detail := "", "" if inst != nil { state, detail = inst.State, inst.Detail } faMgr.Unlock() if state == "failed" || state == "missing-python" { t.Fatalf("managed install %s: %s", state, detail) } if cfg, ok := faManagedConfig(dcnDir, 1); ok { c = cfg break } time.Sleep(500 * time.Millisecond) } if c.Addr == "" { st, _ := os.ReadFile(filepath.Join(stateDir, "serve.log")) t.Fatalf("serve never wrote its port; serve.log:\n%s", st) } // health over mTLS with the box's identity var body []byte var status int for time.Now().Before(deadline) { status, body, err = freeagentCall(c, http.MethodGet, "/health", nil) if err == nil && status == 200 { break } time.Sleep(500 * time.Millisecond) } if err != nil || status != 200 || !strings.Contains(string(body), `"serving": true`) { t.Fatalf("health: %d %v %s", status, err, body) } // the sweep ran: the catalog answers, sanitized for time.Now().Before(deadline) { status, body, err = freeagentCall(c, http.MethodGet, "/catalog", nil) if err == nil && status == 200 && strings.Contains(string(body), "note.txt") { break } time.Sleep(time.Second) } if status != 200 || !strings.Contains(string(body), "[PII_EMAIL_1]") || strings.Contains(string(body), "m@example.com") { sl, _ := os.ReadFile(filepath.Join(stateDir, "sweep.log")) t.Fatalf("catalog: %d %s\nsweep.log:\n%s", status, body, sl) } // files-and-folders: the agent lives in /.agent, its output too, and // the walk never indexed it; secrets stayed in the bastion's state dir agentDir := filepath.Join(corpus, ".agent") for _, f := range []string{"manifest.yaml", "catalog.jsonl", "walk.jsonl"} { if _, err := os.Stat(filepath.Join(agentDir, f)); err != nil { t.Fatalf("%s must live in /.agent: %v", f, err) } } if _, err := os.Stat(filepath.Join(agentDir, "sanitize", "vault.db")); err == nil { t.Fatal("the vault must NOT be inside the folder") } if _, err := os.Stat(filepath.Join(stateDir, "vault.db")); err != nil { t.Fatal("the vault must be in the bastion's state dir") } if _, err := os.Stat(filepath.Join(agentDir, "host.key")); err == nil { t.Fatal("the identity key must NOT be inside the folder") } wm, _ := os.ReadFile(filepath.Join(agentDir, "walk.jsonl")) if strings.Contains(string(wm), ".agent") { t.Fatal("the walk must not index .agent") } gi, _ := os.ReadFile(filepath.Join(agentDir, ".gitignore")) if !strings.Contains(string(gi), "!manifest.yaml") { t.Fatal(".agent/.gitignore must keep the stores out of a member's repo") } if _, err := os.Stat(filepath.Join(stateDir, "out")); err == nil { t.Fatal("pre-0.13.3 residue dir/out must be removed") } // the reply maps like any local agent reply m, _, ok := freeagentMap("search", status, body, err) if !ok || m["state"] != "ok" { t.Fatal("mapping") } // the drop held: every child this daemon runs is the folder's owner, and // the state it was handed belongs to that owner if owner != nil { faOwnChildren.Lock() pids := []int{} for pid := range faOwnChildren.pid { pids = append(pids, pid) } faOwnChildren.Unlock() if len(pids) == 0 { t.Fatal("serve must be running as a tracked child") } for _, pid := range pids { st, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "status")) if err != nil { continue // exited between the snapshot and the read } for _, line := range strings.Split(string(st), "\n") { if strings.HasPrefix(line, "Uid:") && !strings.Contains(line, "\t"+owner.Uid+"\t") { t.Fatalf("child %d is not running as %s: %s", pid, owner.Username, line) } } } for _, p := range []string{stateDir, filepath.Join(stateDir, "host.key"), filepath.Join(stateDir, "vault.db"), filepath.Join(agentDir, "manifest.yaml"), filepath.Join(agentDir, "catalog.jsonl")} { if u, _, err := faPathOwner(p); err != nil || strconv.Itoa(int(u)) != owner.Uid { t.Fatalf("%s must belong to %s (uid %d, %v)", p, owner.Username, u, err) } } } // the signature names a cert the child can read but not replace: on a // root install that is the root-owned public copy if owner != nil { mf, _ := os.ReadFile(filepath.Join(agentDir, "manifest.yaml")) _, _, pubDir, _ := faStoreDirs(dcnDir, 1) want := "cert: " + filepath.Join(pubDir, "host.crt") + "\n" if !strings.Contains(string(mf), want) { t.Fatalf("the signature must name the public cert, want %q in:\n%s", want, mf) } if u, _, _ := faPathOwner(filepath.Join(pubDir, "host.crt")); u != 0 { t.Fatal("the public cert must stay root-owned") } } // heartbeat sees it adv, allOK := faManagedProbe(dcnDir) if !adv || !allOK { t.Fatalf("managed probe: %v %v", adv, allOK) } st := faManagedStatus(dcnDir)["1"].(map[string]any) if st["state"] != "running" || st["serving"] != true || st["version"] != "9.9.9" { t.Fatalf("status: %v", st) } // a `manual` sweep runs ONCE — with a 1s backoff, // a supervised sweep would have several runs on record by now oldBackoff := faRestartBackoff faRestartBackoff = time.Second t.Cleanup(func() { faRestartBackoff = oldBackoff }) time.Sleep(3 * time.Second) runs, _ := os.ReadFile(filepath.Join(corpus, ".agent", "run.jsonl")) if n := strings.Count(string(runs), "\n"); n != 1 { t.Fatalf("manual sweep must run exactly once, run.jsonl has %d lines", n) } // a re-approved grant with narrower terms restarts // the instance under the new manifest (new excludes, new serve port) before, _ := faSnapshot(dcnDir, 1) terms2 := map[string]any{} for k, v := range terms { terms2[k] = v } terms2["excludes"] = []string{".git", ".ssh", "*.key"} rec2 := signedRec(t, priv, terms2) f.Stores["1"] = rec2 if err := dcnSaveFile(dcnDir, f); err != nil { t.Fatal(err) } faReconcile(dcnDir) deadline = time.Now().Add(60 * time.Second) var after faInstance for time.Now().Before(deadline) { after, _ = faSnapshot(dcnDir, 1) if after.State == "running" && after.Text != before.Text { if c2, ok := faManagedConfig(dcnDir, 1); ok && c2.Addr != c.Addr { break } } time.Sleep(300 * time.Millisecond) } if after.Text == before.Text || !strings.Contains(after.Text, `"*.key"`) { t.Fatalf("instance must restart under the re-approved terms; text unchanged: %q", after.Detail) } man, _ := os.ReadFile(after.Manifest) if !strings.Contains(string(man), `".ssh"`) || !strings.Contains(string(man), "signature:") { t.Fatal("re-rendered manifest must carry the new excludes and be re-signed") } // unchanged terms on the next reconcile: no restart (same port) c3, _ := faManagedConfig(dcnDir, 1) faReconcile(dcnDir) time.Sleep(time.Second) c4, _ := faManagedConfig(dcnDir, 1) if c3.Addr != c4.Addr { t.Fatal("identical terms must not restart the instance") } // A fresh identity with a surviving manifest (new layout, wiped base, a // re-pair): the manifest must be re-signed under the new cert, or the // gate refuses it and serve answers 503 forever. faStop(dcnDir, 1) _, privDir, _, _ := faStoreDirs(dcnDir, 1) if err := os.RemoveAll(filepath.Join(privDir, "host.key")); err != nil { t.Fatal(err) } _ = os.Remove(filepath.Join(privDir, "host.crt")) before3 := after faReconcile(dcnDir) deadline = time.Now().Add(60 * time.Second) var c9 freeagentConfig for time.Now().Before(deadline) { if cfg, ok := faManagedConfig(dcnDir, 1); ok && cfg.Addr != "" { if st, _ := faSnapshot(dcnDir, 1); st.State == "running" && st.Cert != "" { if _, body, err := freeagentCall(cfg, http.MethodGet, "/health", nil); err == nil && strings.Contains(string(body), `"serving": true`) { c9 = cfg break } } } if st, _ := faSnapshot(dcnDir, 1); st.State == "failed" { t.Fatalf("re-identity: %s", st.Detail) } time.Sleep(500 * time.Millisecond) } if c9.Addr == "" { sl, _ := os.ReadFile(filepath.Join(stateDir, "serve.log")) t.Fatalf("after a fresh identity the manifest must be re-signed and served (was %q); serve.log:\n%s", before3.State, sl) } } func TestFaChildEnvIsAnAllowlist(t *testing.T) { t.Setenv("PRIVATAE_SECRET_LIKE_THING", "nope") t.Setenv("PATH", "/usr/bin") env := strings.Join(faChildEnv(&faInstance{Dir: t.TempDir(), Cert: "c", Key: "k"}), "\n") if strings.Contains(env, "PRIVATAE_SECRET_LIKE_THING") { t.Fatal("daemon environment must not leak to the child") } for _, want := range []string{"PATH=/usr/bin", "FREEAGENT_CA_ROOT=c", "FREEAGENT_TLS_KEY=k", "EMBED_URL=http://127.0.0.1:11434", "FREEAGENT_VAULT_DIR="} { if !strings.Contains(env, want) { t.Fatalf("child env missing %s", want) } } } // The legacy name is retired on this side (0.42.0): one manifest address, one // artifact name. Older bastions still read the legacy ones, which is the // publish script's concern, not this binary's. // recordingTransport answers every request 404 and records its URL, so a test // can see every address a fetch tried, including ones it was never pointed at. type recordingTransport struct{ urls []string } func (r *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { r.urls = append(r.urls, req.URL.String()) return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil } func TestFaFetchManifestTriesOnlyTheNewAddress(t *testing.T) { if faManifestURL != "https://privatae.ai/bastion-agent/version.json" { t.Fatalf("manifest address = %q", faManifestURL) } // The production address, with nothing leaving the machine: every request // is answered here. A failed fetch must not go on to any other address. rec := &recordingTransport{} oldTransport := http.DefaultTransport http.DefaultTransport = rec t.Cleanup(func() { http.DefaultTransport = oldTransport }) if _, _, _, err := faFetchManifest(); err == nil { t.Fatal("a 404 must fail the fetch") } if len(rec.urls) != 1 || rec.urls[0] != faManifestURL { t.Fatalf("requested %v; want only %s", rec.urls, faManifestURL) } } func TestFaFetchManifestErrors(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/bad/version.json", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"version":"1.0.0","files":["not","a","map"]}`)) }) srv := httptest.NewServer(signedStub(releaseKindAgent, mux)) t.Cleanup(srv.Close) old := faManifestURL t.Cleanup(func() { faManifestURL = old }) // Signed and versioned, but not a manifest this reads: refused, not read // as an empty one. faManifestURL = srv.URL + "/bad/version.json" if _, _, _, err := faFetchManifest(); err == nil || !strings.Contains(err.Error(), "malformed") { t.Fatalf("a malformed manifest must be refused, got %v", err) } // Not served: the error names the address that failed. faManifestURL = srv.URL + "/nope/version.json" if _, _, _, err := faFetchManifest(); err == nil || !strings.Contains(err.Error(), "/nope/") { t.Fatalf("want the failing address in the error, got %v", err) } } func TestFaEnsurePyzReadsOnlyTheNewArtifactName(t *testing.T) { body := []byte("#!/usr/bin/env python3\nfake") sum := sha256.Sum256(body) good := hex.EncodeToString(sum[:]) old := faManifestURL t.Cleanup(func() { faManifestURL = old }) // A DISTINCT version per case, deliberately. faEnsurePyz returns the cached // copy early when the installed version matches the manifest's, so reusing // one version would let a later case pass without reaching the name lookup. serve := func(files map[string]string, version string) (*httptest.Server, chan string) { mux := http.NewServeMux() var srv *httptest.Server served := make(chan string, 4) mux.HandleFunc("/m.json", func(w http.ResponseWriter, r *http.Request) { m := map[string]any{} for name, path := range files { m[name] = map[string]any{"url": srv.URL + path, "sha256": good} } json.NewEncoder(w).Encode(map[string]any{"version": version, "files": m}) }) for _, path := range files { path := path mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { served <- path; w.Write(body) }) } srv = httptest.NewServer(signedStub(releaseKindAgent, mux)) t.Cleanup(srv.Close) return srv, served } withConfigDir(t) srv, _ := serve(map[string]string{"bastion-agent-1.1.1.pyz": "/new"}, "1.1.1") faManifestURL = srv.URL + "/m.json" if _, v, err := faEnsurePyz(); err != nil || v != "1.1.1" { t.Fatalf("the artifact name must install: %v %q", err, v) } // Both names, as the publish script writes while older bastions need the // legacy one. The URL proves which entry was downloaded. withConfigDir(t) srv, served := serve(map[string]string{"bastion-agent-4.4.4.pyz": "/new", "freeagent-4.4.4.pyz": "/legacy"}, "4.4.4") faManifestURL = srv.URL + "/m.json" if _, v, err := faEnsurePyz(); err != nil || v != "4.4.4" { t.Fatalf("both names present must install: %v %q", err, v) } select { case which := <-served: if which != "/new" { t.Fatalf("downloaded %s; only the new name is read", which) } default: t.Fatal("nothing was downloaded") } // Only the legacy name: refused, and the error names what was sought. withConfigDir(t) srv, served = serve(map[string]string{"freeagent-2.2.2.pyz": "/legacy"}, "2.2.2") faManifestURL = srv.URL + "/m.json" _, _, err := faEnsurePyz() if err == nil || !strings.Contains(err.Error(), "bastion-agent-2.2.2.pyz") { t.Fatalf("a manifest with only the legacy name must be refused, got %v", err) } select { case which := <-served: t.Fatalf("downloaded %s from a manifest it should have refused", which) default: } }