7465713a2f
v0.129.0: CAMPAIGN-4 fixes — rate-limiter key (F-B) + volume-blind estimate (F-A) + no-op claim status (F-C) F-B (MED, security): shared clientIP(r) helper (XFF first-hop, else SplitHostPort host, else raw) replaces requestIP + the duplicated inline derivation in handleLogin, so login AND escrow re-auth key on the port-stripped host IP — distinct direct connections no longer evade the failed-attempt counter. XFF-trust out of scope (commented). F-A (MED, honesty): volumeSizer seam reads volume size from a container view (docker run --rm -v vol:/vol:ro alpine du -sb /vol), replacing the host-path du that returned 0 inside the containerized controller. Failed read -> size_unknown + fits_on_dest forced false (never "fits"). Export pre-flight hard-aborts only on a KNOWN doesn.t-fit. HDD branch unchanged. F-C (LOW-MED): escrowClaimAPIHandler relays agent 404 -> clean 404 and 409 -> 409; 410 and genuine-unreachable 502 unchanged (was: 404 fell through to 502). Tests + red-proofs: ratelimit_ip_test.go (F-B x6), estimate_volsize_test.go (F-A x3), TestEscrowClaim_ProxySemantics +3 (F-C). Alpine busybox du -sb verified prod-valid. Claude-Session: https://claude.ai/code/session_01LbMm4T7Ayzs1unB9pN6Uqd @
480 lines
18 KiB
Go
480 lines
18 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Escrow wizard handler tests (v0.127.0). The agent + staging seams record CALL ORDER — the
|
|
// load-bearing Scenario A/B assertion is "re-stage happened BEFORE the agent trigger", and every
|
|
// Scenario E gate must exit with the agent NEVER called.
|
|
|
|
const wizardPassword = "titkos-jelszo"
|
|
|
|
type fakeEscrowAgent struct {
|
|
order *[]string // shared call-order log (harness-owned)
|
|
version string
|
|
startResp agentapi.EscrowCeremonyStartResponse
|
|
startStatus int
|
|
startErr error
|
|
status agentapi.EscrowCeremonyStatusResponse
|
|
claimCode string
|
|
claimStatus int
|
|
claimErr error
|
|
pf agentapi.EscrowPreflightResponse
|
|
}
|
|
|
|
func (f *fakeEscrowAgent) EscrowPreflight(context.Context) (agentapi.EscrowPreflightResponse, error) {
|
|
*f.order = append(*f.order, "preflight")
|
|
return f.pf, nil
|
|
}
|
|
func (f *fakeEscrowAgent) EscrowCeremonyStart(context.Context) (agentapi.EscrowCeremonyStartResponse, int, error) {
|
|
*f.order = append(*f.order, "start")
|
|
return f.startResp, f.startStatus, f.startErr
|
|
}
|
|
func (f *fakeEscrowAgent) EscrowCeremonyStatus(context.Context) (agentapi.EscrowCeremonyStatusResponse, error) {
|
|
return f.status, nil
|
|
}
|
|
func (f *fakeEscrowAgent) EscrowCeremonyClaim(context.Context) (string, int, error) {
|
|
*f.order = append(*f.order, "claim")
|
|
return f.claimCode, f.claimStatus, f.claimErr
|
|
}
|
|
func (f *fakeEscrowAgent) AgentVersion() string { return f.version }
|
|
|
|
type escrowWizardHarness struct {
|
|
s *Server
|
|
sett *settings.Settings
|
|
m *backup.Manager
|
|
agent *fakeEscrowAgent
|
|
order []string
|
|
}
|
|
|
|
func newEscrowWizardHarness(t *testing.T) *escrowWizardHarness {
|
|
t.Helper()
|
|
tmp := t.TempDir()
|
|
lg := log.New(io.Discard, "", 0)
|
|
sett, err := settings.Load(filepath.Join(tmp, "settings.json"), lg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := &config.Config{}
|
|
cfg.Paths.DataDir = tmp
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(wizardPassword), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.Web.PasswordHash = string(hash)
|
|
h := &escrowWizardHarness{
|
|
sett: sett,
|
|
m: backup.NewManager(cfg, sett, lg),
|
|
}
|
|
h.agent = &fakeEscrowAgent{
|
|
order: &h.order,
|
|
version: "0.88.0",
|
|
startResp: agentapi.EscrowCeremonyStartResponse{JobID: "escrow-1", Phase: "running"},
|
|
startStatus: http.StatusAccepted,
|
|
}
|
|
h.s = &Server{cfg: cfg, backupMgr: h.m, settings: sett, logger: lg}
|
|
h.s.escrowAgentFn = func() (escrowAgent, error) { return h.agent, nil }
|
|
h.s.escrowStageFn = func(context.Context) error { h.order = append(h.order, "stage"); return nil }
|
|
return h
|
|
}
|
|
|
|
// configureOffbox makes OffboxConfigured() true with the given escrow state.
|
|
func (h *escrowWizardHarness) configureOffbox(t *testing.T, state string) {
|
|
t.Helper()
|
|
if err := h.m.WriteOffboxSecrets("KEYMATERIAL", "nas.local ssh-ed25519 HOSTKEY"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.sett.SetOffboxTarget(&settings.OffboxTarget{
|
|
Enabled: true, Host: "nas.local", Port: 22, User: "felhom", RepoPath: "/srv/repo",
|
|
Schedule: "daily", EscrowState: state,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !h.m.OffboxConfigured() {
|
|
t.Fatal("setup: offbox should be configured")
|
|
}
|
|
}
|
|
|
|
func postStart(t *testing.T, s *Server, password string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
form := url.Values{"password": {password}}
|
|
r := httptest.NewRequest("POST", "/api/escrow/start", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
s.escrowStartAPIHandler(w, r)
|
|
return w
|
|
}
|
|
|
|
// Scenario A — pending offsite happy path: correct password → re-stage FIRST, then the agent
|
|
// trigger; 200 with the job id.
|
|
func TestEscrowStart_StagesBeforeTrigger(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.configureOffbox(t, "pending")
|
|
|
|
w := postStart(t, h.s, wizardPassword)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("start: got %d (%s)", w.Code, w.Body.String())
|
|
}
|
|
if got := strings.Join(h.order, ","); got != "stage,start" {
|
|
t.Fatalf("call order = %q, want stage BEFORE start (a ceremony without the staged secret mints a hash-less blob)", got)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "escrow-1") {
|
|
t.Fatalf("response lacks the job id: %s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Scenario B — re-ceremony from ESCROWED state stages too (the same order assertion).
|
|
func TestEscrowStart_ReceremonyStagesToo(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.configureOffbox(t, "escrowed")
|
|
|
|
if w := postStart(t, h.s, wizardPassword); w.Code != http.StatusOK {
|
|
t.Fatalf("re-ceremony start: got %d", w.Code)
|
|
}
|
|
if got := strings.Join(h.order, ","); got != "stage,start" {
|
|
t.Fatalf("re-ceremony call order = %q, want stage,start", got)
|
|
}
|
|
}
|
|
|
|
// Scenario C — no offbox configured: NO staging attempted; the ceremony still runs.
|
|
func TestEscrowStart_NoOffboxSkipsStaging(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
|
|
if w := postStart(t, h.s, wizardPassword); w.Code != http.StatusOK {
|
|
t.Fatalf("start: got %d", w.Code)
|
|
}
|
|
if got := strings.Join(h.order, ","); got != "start" {
|
|
t.Fatalf("call order = %q, want start only (no staging without offbox)", got)
|
|
}
|
|
}
|
|
|
|
// Scenario E — every gate refuses BEFORE the agent (and staging) is touched.
|
|
func TestEscrowStart_SecurityGates(t *testing.T) {
|
|
t.Run("wrong password", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.configureOffbox(t, "pending")
|
|
w := postStart(t, h.s, "rossz-jelszo")
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Fatalf("wrong password: got %d, want 401", w.Code)
|
|
}
|
|
if len(h.order) != 0 {
|
|
t.Fatalf("agent/staging touched despite failed re-auth: %v", h.order)
|
|
}
|
|
// The failure rides the LOGIN rate limiter.
|
|
h.s.loginAttemptMu.Lock()
|
|
var count int
|
|
for _, a := range h.s.loginAttempts {
|
|
count += a.count
|
|
}
|
|
h.s.loginAttemptMu.Unlock()
|
|
if count != 1 {
|
|
t.Fatalf("wrong password must increment the login rate-limit counter, got %d", count)
|
|
}
|
|
})
|
|
t.Run("rate limited after max attempts", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
for i := 0; i < loginMaxAttempts; i++ {
|
|
postStart(t, h.s, "rossz-jelszo")
|
|
}
|
|
w := postStart(t, h.s, wizardPassword) // even the CORRECT password is refused inside the window
|
|
if w.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("rate limit: got %d, want 429", w.Code)
|
|
}
|
|
if len(h.order) != 0 {
|
|
t.Fatalf("agent touched while rate-limited: %v", h.order)
|
|
}
|
|
})
|
|
t.Run("passwordless box refused", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.s.cfg.Web.PasswordHash = ""
|
|
w := postStart(t, h.s, "")
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("passwordless: got %d, want 403", w.Code)
|
|
}
|
|
if len(h.order) != 0 {
|
|
t.Fatal("agent touched on a passwordless box")
|
|
}
|
|
})
|
|
t.Run("stage failure aborts before trigger", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.configureOffbox(t, "pending")
|
|
h.s.escrowStageFn = func(context.Context) error { return fmt.Errorf("agent down") }
|
|
w := postStart(t, h.s, wizardPassword)
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Fatalf("stage failure: got %d, want 502", w.Code)
|
|
}
|
|
for _, c := range h.order {
|
|
if c == "start" {
|
|
t.Fatal("the ceremony started despite the failed staging (would mint a hash-less blob)")
|
|
}
|
|
}
|
|
})
|
|
t.Run("agent too old", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.version = "0.87.0"
|
|
w := postStart(t, h.s, wizardPassword)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("old agent: got %d, want 409", w.Code)
|
|
}
|
|
for _, c := range h.order {
|
|
if c == "start" {
|
|
t.Fatal("an old agent must not be triggered")
|
|
}
|
|
}
|
|
})
|
|
t.Run("header-less agent counts as older", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.version = ""
|
|
if w := postStart(t, h.s, wizardPassword); w.Code != http.StatusConflict {
|
|
t.Fatalf("version-less agent: got %d, want 409", w.Code)
|
|
}
|
|
})
|
|
t.Run("ceremony already running", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.startStatus = http.StatusConflict
|
|
h.agent.startErr = fmt.Errorf("agentapi: POST /escrow/ceremony: HTTP 409: already running")
|
|
w := postStart(t, h.s, wizardPassword)
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("busy agent: got %d, want 409", w.Code)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "folyamatban") {
|
|
t.Fatalf("busy refusal must speak Hungarian: %s", w.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
// Scenario D (controller half) — the claim proxy: no-store on 200, the 410 void message, and the
|
|
// code appears ONLY in the claim response.
|
|
func TestEscrowClaim_ProxySemantics(t *testing.T) {
|
|
const code = "proba-kod-tiz-szo"
|
|
t.Run("success", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.claimCode = code
|
|
h.agent.claimStatus = http.StatusOK
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil))
|
|
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), code) {
|
|
t.Fatalf("claim: got %d %s", w.Code, w.Body.String())
|
|
}
|
|
if cc := w.Header().Get("Cache-Control"); cc != "no-store" {
|
|
t.Fatalf("claim Cache-Control = %q, want no-store", cc)
|
|
}
|
|
})
|
|
t.Run("gone", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.claimStatus = http.StatusGone
|
|
h.agent.claimErr = fmt.Errorf("gone")
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil))
|
|
if w.Code != http.StatusGone || !strings.Contains(w.Body.String(), "újra nem kérhető le") {
|
|
t.Fatalf("gone: got %d %s", w.Code, w.Body.String())
|
|
}
|
|
})
|
|
// Scenario I (F-C fix): agent 404 "no ceremony has run" → a clean 404, NOT 502.
|
|
t.Run("no_active_ceremony_404_not_502", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.claimStatus = http.StatusNotFound
|
|
h.agent.claimErr = fmt.Errorf("no ceremony has run")
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Fatalf("no-ceremony claim must be 404, got %d %s", w.Code, w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), "Nincs aktív helyreállítási folyamat") {
|
|
t.Fatalf("expected honest Hungarian no-ceremony message, got %s", w.Body.String())
|
|
}
|
|
})
|
|
// Scenario J (regression): agent 409 → passed through as 409, not 502.
|
|
t.Run("conflict_409", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.claimStatus = http.StatusConflict
|
|
h.agent.claimErr = fmt.Errorf("conflict")
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil))
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("409 must pass through, got %d", w.Code)
|
|
}
|
|
})
|
|
// Scenario K (regression): a genuinely-unreachable agent (status 0) stays 502 — that IS a real
|
|
// bad gateway; the fix must NOT over-correct it to a 4xx.
|
|
t.Run("unreachable_stays_502", func(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.claimStatus = 0
|
|
h.agent.claimErr = fmt.Errorf("dial tcp: connection refused")
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil))
|
|
if w.Code != http.StatusBadGateway {
|
|
t.Fatalf("unreachable agent must stay 502, got %d", w.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// The status proxy relays the agent's non-secret job view verbatim.
|
|
func TestEscrowStatus_Proxy(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.agent.status = agentapi.EscrowCeremonyStatusResponse{
|
|
Phase: "done", JobID: "escrow-1", ResticPwSealed: true, Uploaded: true,
|
|
Claimable: true, ClaimExpiresInSec: 599,
|
|
}
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowStatusAPIHandler(w, httptest.NewRequest("GET", "/api/escrow/status", nil))
|
|
body := w.Body.String()
|
|
for _, want := range []string{`"phase":"done"`, `"claimable":true`, `"restic_pw_sealed":true`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("status proxy missing %s: %s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The wizard page + card render states (Hungarian copy, no server-side R anywhere by
|
|
// construction — the template has no code variable to leak).
|
|
func TestEscrowTemplates_Render(t *testing.T) {
|
|
base := func() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"Title": "Helyreállítási kód", "Domain": "example.hu",
|
|
"AgentSupported": true, "Receremony": false, "OffboxConfigured": true,
|
|
}
|
|
}
|
|
t.Run("wizard fresh", func(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_escrow", base())
|
|
for _, want := range []string{
|
|
"Előfeltételek ellenőrzése",
|
|
"a mentései utolsó kulcsa",
|
|
"A folytatáshoz adja meg a bejelentkezési jelszavát",
|
|
"Kód létrehozása",
|
|
"Kód megjelenítése",
|
|
// v0.127.3 (Viktor): the reveal states EXPLICITLY that the shown code is already the
|
|
// live one (the supersede happened at upload, before display).
|
|
"Ez mostantól az élő helyreállítási kód — a korábbi kód érvényét vesztette. Mentse el most: a kód többé nem jeleníthető meg.",
|
|
// The pre-generation cancel must stay next to the start button (nothing runs until then).
|
|
`>Mégsem</a>`,
|
|
"nem ezen a szerveren",
|
|
"biztonsági okból újra nem kérhető le",
|
|
// v0.127.1 polish: the Hungarian preflight detail map + the hide/show toggle
|
|
// (v0.127.2: manual-only — initial label "Elrejtés", code visible by default).
|
|
"nincs előkészített jelszó — a varázsló indításkor automatikusan előkészíti",
|
|
"a rendszerjogosultság hiányzik",
|
|
"Megjelenítés",
|
|
">Elrejtés</button>",
|
|
} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("wizard missing %q", want)
|
|
}
|
|
}
|
|
// v0.127.2 (Viktor's live finding): the code must NOT auto-blur when a verification input
|
|
// gets focus — the customer types from the screen. The hide toggle is manual-only.
|
|
if strings.Contains(html, "armCodeBlur") || strings.Contains(html, "onfocus=") {
|
|
t.Error("auto-blur-on-focus must be gone (manual toggle only)")
|
|
}
|
|
if strings.Contains(html, "érvényét veszti") {
|
|
t.Error("fresh wizard must not show the re-ceremony supersede warning")
|
|
}
|
|
// v0.127.1: the agent's operator-English detail strings must never be hardcoded into the
|
|
// page — they may only arrive at runtime, inside the muted failure-diagnostic span.
|
|
for _, raw := range []string{"DR tier applied", "hub upload target configured", "sudo grant listed"} {
|
|
if strings.Contains(html, raw) {
|
|
t.Errorf("raw English agent detail %q leaked into the wizard page source", raw)
|
|
}
|
|
}
|
|
})
|
|
t.Run("wizard re-ceremony variant", func(t *testing.T) {
|
|
d := base()
|
|
d["Receremony"] = true
|
|
if html := renderBackupPage(t, "backups_escrow", d); !strings.Contains(html, "érvényét veszti") {
|
|
t.Error("re-ceremony wizard must show the supersede warning")
|
|
}
|
|
})
|
|
t.Run("wizard agent too old", func(t *testing.T) {
|
|
d := base()
|
|
d["AgentSupported"] = false
|
|
html := renderBackupPage(t, "backups_escrow", d)
|
|
if !strings.Contains(html, "az ügynök frissítése szükséges") {
|
|
t.Error("old-agent wizard must show the version note")
|
|
}
|
|
if strings.Contains(html, "Kód létrehozása") {
|
|
t.Error("old-agent wizard must be inert (no start form)")
|
|
}
|
|
})
|
|
t.Run("remote card states", func(t *testing.T) {
|
|
data := splitTestData()
|
|
data["EscrowAgentOK"] = true
|
|
data["EscrowStale"] = false
|
|
html := renderBackupPage(t, "backups_remote", data) // escrowed clean
|
|
if !strings.Contains(html, "Új helyreállítási kód készítése") {
|
|
t.Error("escrowed card must offer the secondary re-ceremony link")
|
|
}
|
|
// v0.127.1: the CTA is a real secondary BUTTON (outline — available-not-urgent), no longer
|
|
// an inline link buried in the muted hint.
|
|
if !strings.Contains(html, `class="btn btn-sm btn-outline">Új helyreállítási kód készítése`) {
|
|
t.Error("escrowed-card CTA must render as a btn-outline button")
|
|
}
|
|
if strings.Contains(html, "Letét megerősítése") {
|
|
t.Error("the deprecated manual-confirm button must be GONE from the card")
|
|
}
|
|
|
|
data["EscrowStale"] = true
|
|
html = renderBackupPage(t, "backups_remote", data)
|
|
if !strings.Contains(html, "nem fedi a jelenlegi távoli mentési jelszót") {
|
|
t.Error("stale card must show the exact stale warning")
|
|
}
|
|
|
|
pending := splitTestData()
|
|
pending["EscrowAgentOK"] = true
|
|
pending["EscrowStale"] = false
|
|
pending["Offbox"].(*settings.OffboxTarget).EscrowState = "pending"
|
|
html = renderBackupPage(t, "backups_remote", pending)
|
|
if !strings.Contains(html, "Helyreállítási kód szükséges") || !strings.Contains(html, "Helyreállítási kód létrehozása") {
|
|
t.Error("pending card must show the CTA state")
|
|
}
|
|
|
|
old := splitTestData()
|
|
old["EscrowAgentOK"] = false
|
|
old["EscrowStale"] = false
|
|
old["Offbox"].(*settings.OffboxTarget).EscrowState = "pending"
|
|
html = renderBackupPage(t, "backups_remote", old)
|
|
if !strings.Contains(html, "az ügynök frissítése szükséges") || strings.Contains(html, `href="/backup/escrow"`) {
|
|
t.Error("old-agent card must show the version note with NO CTA")
|
|
}
|
|
})
|
|
}
|
|
|
|
// The preflight proxy augments the agent checklist with the version gate + offbox facts.
|
|
func TestEscrowPreflight_Augments(t *testing.T) {
|
|
h := newEscrowWizardHarness(t)
|
|
h.configureOffbox(t, "escrowed")
|
|
h.agent.pf = agentapi.EscrowPreflightResponse{OK: true, Items: []agentapi.EscrowPreflightItem{
|
|
{ID: "pbs_storage_id", OK: true, Detail: "felhom-pbs"},
|
|
}}
|
|
w := httptest.NewRecorder()
|
|
h.s.escrowPreflightAPIHandler(w, httptest.NewRequest("GET", "/api/escrow/preflight", nil))
|
|
body := w.Body.String()
|
|
for _, want := range []string{`"agent_supported":true`, `"escrow_state":"escrowed"`, `"offbox_configured":true`, `"pbs_storage_id"`} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("preflight missing %s: %s", want, body)
|
|
}
|
|
}
|
|
// An old agent flips both the flag and the aggregate ok.
|
|
h.agent.version = "0.87.0"
|
|
w2 := httptest.NewRecorder()
|
|
h.s.escrowPreflightAPIHandler(w2, httptest.NewRequest("GET", "/api/escrow/preflight", nil))
|
|
if !strings.Contains(w2.Body.String(), `"agent_supported":false`) || !strings.Contains(w2.Body.String(), `"ok":false`) {
|
|
t.Fatalf("old agent must flip agent_supported + ok: %s", w2.Body.String())
|
|
}
|
|
}
|