package console import ( "encoding/json" "net/http" "testing" "time" ) // TestAPIBuildProbeSetsAndClearsOnAnExistingApp is the whole point of the // door. Until it existed, SetAppBuildProbe had exactly one caller — the claim // door — so an app created without the flag could never acquire a probe, and // the fix for "deploy reported live over the old binary" was unreachable by // every app that already had the problem (discussion #13). func TestAPIBuildProbeSetsAndClearsOnAnExistingApp(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-buildprobe") if _, err := fx.st.CreateAppIn(OpsAccountID, "docs", "paul@keymail.dev", time.Now()); err != nil { t.Fatalf("create app: %v", err) } probeOf := func() string { t.Helper() _, probe, err := fx.st.AppFlags(OpsAccountID, "docs") if err != nil { t.Fatalf("AppFlags: %v", err) } return probe } if got := probeOf(); got != "" { t.Fatalf("a fresh app already carries a probe: %q", got) } u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/docs/build-probe" status, _, body := rawReq(t, http.MethodPost, u, token, `{"build_probe":"/api/version"}`) if status != http.StatusOK { t.Fatalf("set = %d %q", status, body) } var out struct { BuildProbe string `json:"build_probe"` } if err := json.Unmarshal([]byte(body), &out); err != nil { t.Fatalf("unreadable: %v: %s", err, body) } // The echo is the contract: this is the field the deploy watch trusts // instead of the header, so a write that silently failed has to be a loud // error rather than a quiet lie. Response and store both. if out.BuildProbe != "/api/version" { t.Errorf("echo = %q, want /api/version", out.BuildProbe) } if got := probeOf(); got != "/api/version" { t.Errorf("stored = %q, want /api/version", got) } // "" clears back to header-watching — a legal value of this field, not a // missing one. status, _, body = rawReq(t, http.MethodPost, u, token, `{"build_probe":""}`) if status != http.StatusOK { t.Fatalf("clear = %d %q", status, body) } if got := probeOf(); got != "" { t.Errorf("after clear, stored = %q, want empty", got) } } // TestAPIBuildProbeRefusesABadShape: the watch builds "https://" + host + // probe, so a probe not starting with "/" turns the poll into a request to a // machine of somebody else's choosing, which would then report ITS build as // this deploy's. Refused at the write door, not only where it is read. func TestAPIBuildProbeRefusesABadShape(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-buildprobebad") if _, err := fx.st.CreateAppIn(OpsAccountID, "docs", "paul@keymail.dev", time.Now()); err != nil { t.Fatalf("create app: %v", err) } u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/docs/build-probe" for _, bad := range []string{"@evil.example/x", "api/version", "/x?y=1", "https://evil.example/x"} { payload, _ := json.Marshal(map[string]string{"build_probe": bad}) status, _, body := rawReq(t, http.MethodPost, u, token, string(payload)) if status != http.StatusBadRequest { t.Errorf("probe %q = %d %q, want 400", bad, status, body) } } if _, probe, _ := fx.st.AppFlags(OpsAccountID, "docs"); probe != "" { t.Errorf("a refused probe still landed: %q", probe) } }