diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f34f07..051678d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## Unreleased (2026-10-19) — the new recovery code replaces the old one only after the household typed it back (R-1151, controller half) + +Ships in the next release. **No new MinAgent:** it works with every agent — the agent's claim answer says whether a +confirm is needed. + +- **The wizard's typed-back words now go to the box, not only to the browser.** The claim asks the agent to keep the + new package (`{"defer_upload": true}`); an agent ≥ 0.160.0 answers with `confirm_required` and the two word + positions (it draws them now; the page drew them before), and the page's „Szavak ellenőrzése" posts the two words to + the new `POST /api/escrow/confirm` (session + CSRF, like the other `/api/escrow/*` calls), which hands them to the + agent's `POST /escrow/ceremony/confirm`. Only a match stores the new package on the hub; until then the ceremony is not + complete (no „completed" stamp, no report push — both moved from the claim to the confirm). Answers: wrong words → + 422, the page says so and the wait goes on; the agent's window ran out (410) or nothing waits any more (404 — a new + start, an agent restart) → 410 with `restart`, the page clears the code and offers „Újraindítás"; a hub failure → + 502, try again. Every answer says the previous code still works where it does. New keys + `api.escrow.confirm_expired`, `confirm_lost`, `confirm_store_failed`, `confirm_words_mismatch` (hu + en). + **An agent older than 0.160.0** ignores the claim's body and stores the package at the claim, as before; its answer + has no `confirm_required`, so the page keeps its own check and the claim completes the ceremony (pinned). + Tests `TestR1151_ClaimThatWaitsIsNotCompleteUntilTheWordsComeBack`, `TestR1151_OldAgentClaimCompletesAtOnce`, + `TestR1151_ConfirmAnswersInTheBoxLanguage`, `TestEscrowClaimAndConfirm_R1151` (agentapi). Red-proved: the + `ConfirmRequired` branch removed → `claim: 200 {"data":{"recovery_code":…}}` with no confirm request. i18n fixtures + changed (named): `escrow_first`, `escrow_receremony` (the page's script and the hidden „Újraindítás" link); every + other fixture byte-identical. + ## v0.331.0 — OnlyOffice opens and SAVES from FileBrowser and Nextcloud; FileBrowser is back at once after a restart (R-1018); a removal deletes a volume the definition dropped (R-1117) (2026-10-18) **MinAgent: 0.158.1** — the floor's, carried (not above the vouched agent 0.159.2). Nothing here calls a new agent route. diff --git a/controller/internal/agentapi/escrow.go b/controller/internal/agentapi/escrow.go index 9b3b242..663d6a0 100644 --- a/controller/internal/agentapi/escrow.go +++ b/controller/internal/agentapi/escrow.go @@ -92,31 +92,63 @@ func (c *Client) EscrowCeremonyStatus(ctx context.Context) (EscrowCeremonyStatus return out, nil } +// EscrowClaim is a claimed recovery code. ConfirmRequired (agent ≥ 0.160.0, R-1151): the code's package reaches the +// hub only when EscrowCeremonyConfirm brings back the words at Positions (1-based); an older agent answers without +// it — its package is already on the hub, and no confirm is needed. +type EscrowClaim struct { + Code string + ConfirmRequired bool + Positions []int +} + // EscrowCeremonyClaim performs the ONE-SHOT R claim. Returns the recovery code + the agent's // HTTP status (410 = already claimed / expired — the wizard's void state). The code must never // be logged, persisted, or placed anywhere but the claim XHR response; the error path carries // the agent's reason text, never the code. -func (c *Client) EscrowCeremonyClaim(ctx context.Context) (string, int, error) { - env, status, err := c.postWithStatus(ctx, "/escrow/ceremony/claim", struct{}{}) +// +// R-1151: the claim asks the agent to keep the new package until the household typed the code back +// (`defer_upload`). An agent older than 0.160.0 ignores the body and uploads as before. +func (c *Client) EscrowCeremonyClaim(ctx context.Context) (EscrowClaim, int, error) { + env, status, err := c.postWithStatus(ctx, "/escrow/ceremony/claim", map[string]bool{"defer_upload": true}) if err != nil { - return "", status, err + return EscrowClaim{}, status, err } if status == http.StatusGone { - return "", status, fmt.Errorf("agentapi: POST /escrow/ceremony/claim: gone (claimed or expired)") + return EscrowClaim{}, status, fmt.Errorf("agentapi: POST /escrow/ceremony/claim: gone (claimed or expired)") } if err := refusalError("/escrow/ceremony/claim", status, env); err != nil { - return "", status, err + return EscrowClaim{}, status, err } var out struct { - RecoveryCode string `json:"recovery_code"` + RecoveryCode string `json:"recovery_code"` + ConfirmRequired bool `json:"confirm_required"` + VerifyPositions []int `json:"verify_positions"` } if err := json.Unmarshal(env.Data, &out); err != nil { - return "", status, fmt.Errorf("agentapi: decode /escrow/ceremony/claim: %w", err) + return EscrowClaim{}, status, fmt.Errorf("agentapi: decode /escrow/ceremony/claim: %w", err) } if out.RecoveryCode == "" { - return "", status, fmt.Errorf("agentapi: /escrow/ceremony/claim returned no code") + return EscrowClaim{}, status, fmt.Errorf("agentapi: /escrow/ceremony/claim returned no code") } - return out.RecoveryCode, status, nil + if out.ConfirmRequired && len(out.VerifyPositions) != 2 { + return EscrowClaim{}, status, fmt.Errorf("agentapi: /escrow/ceremony/claim asks for a confirmation without two positions") + } + return EscrowClaim{Code: out.RecoveryCode, ConfirmRequired: out.ConfirmRequired, Positions: out.VerifyPositions}, status, nil +} + +// EscrowCeremonyConfirm sends the two words the household typed back (R-1151, agent ≥ 0.160.0); on success the agent +// has stored the new package on the hub. The status is returned for the caller to classify: 422 the words do not +// match (still waiting), 410 the window ran out, 404 nothing waits (superseded, restarted), 502 the hub failed (still +// waiting). The words are part of the recovery code — never logged. +func (c *Client) EscrowCeremonyConfirm(ctx context.Context, words []string) (int, error) { + env, status, err := c.postWithStatus(ctx, "/escrow/ceremony/confirm", map[string][]string{"words": words}) + if err != nil { + return status, err + } + if err := refusalError("/escrow/ceremony/confirm", status, env); err != nil { + return status, err + } + return status, nil } // RecoverOffsiteRepoPassword asks the agent to open this host's hub-held sealed bundle with the diff --git a/controller/internal/agentapi/escrow_r1151_test.go b/controller/internal/agentapi/escrow_r1151_test.go new file mode 100644 index 0000000..c28d185 --- /dev/null +++ b/controller/internal/agentapi/escrow_r1151_test.go @@ -0,0 +1,63 @@ +package agentapi + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// R-1151 — the claim asks the agent to keep the package until the words come back, and reads the agent's answer; +// the confirm carries the two words. An agent older than 0.160.0 answers without confirm_required. +func TestEscrowClaimAndConfirm_R1151(t *testing.T) { + var claimBody map[string]any + var confirmBody map[string][]string + newAgent := true + mux := http.NewServeMux() + mux.HandleFunc("POST /escrow/ceremony/claim", func(w http.ResponseWriter, r *http.Request) { + claimBody = nil + _ = json.NewDecoder(r.Body).Decode(&claimBody) + if newAgent { + _, _ = w.Write([]byte(`{"ok":true,"data":{"recovery_code":"a-b-c","confirm_required":true,"verify_positions":[1,3]}}`)) + return + } + _, _ = w.Write([]byte(`{"ok":true,"data":{"recovery_code":"a-b-c"}}`)) + }) + mux.HandleFunc("POST /escrow/ceremony/confirm", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewDecoder(r.Body).Decode(&confirmBody) + if confirmBody["words"][0] != "a" { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"ok":false,"error":"the words do not match the recovery code"}`)) + return + } + _, _ = w.Write([]byte(`{"ok":true,"data":{"uploaded":true}}`)) + }) + s := httptest.NewTLSServer(mux) + defer s.Close() + c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://")) + + claim, status, err := c.EscrowCeremonyClaim(context.Background()) + if err != nil || status != http.StatusOK || claim.Code != "a-b-c" || !claim.ConfirmRequired || len(claim.Positions) != 2 || claim.Positions[1] != 3 { + t.Fatalf("claim: %+v %d %v", claim, status, err) + } + if claimBody["defer_upload"] != true { + t.Fatalf("the claim did not ask the agent to wait for the words: body %v", claimBody) + } + if st, err := c.EscrowCeremonyConfirm(context.Background(), []string{"a", "c"}); err != nil || st != http.StatusOK { + t.Fatalf("confirm: %d %v", st, err) + } + if strings.Join(confirmBody["words"], ",") != "a,c" { + t.Fatalf("confirm sent %v", confirmBody) + } + if st, err := c.EscrowCeremonyConfirm(context.Background(), []string{"x", "c"}); err == nil || st != http.StatusUnprocessableEntity { + t.Fatalf("a wrong word must come back as a 422 error: %d %v", st, err) + } + + newAgent = false // agent 0.159.2: no confirm request + claim, _, err = c.EscrowCeremonyClaim(context.Background()) + if err != nil || claim.ConfirmRequired || claim.Code != "a-b-c" { + t.Fatalf("claim on an old agent: %+v %v", claim, err) + } +} diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index 7b09e81..6e38082 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -2634,6 +2634,10 @@ "api.escrow.already_running": "A code is already being created — wait until it finishes.", "api.escrow.code_not_shown": "The code was created but not shown — for security it cannot be fetched again. Start the process again: the new code replaces the old one.", "api.escrow.no_active_job": "There is no active recovery process — start creating the code first.", + "api.escrow.confirm_expired": "The time to type the new code back has run out — your previous code still works. Start the process again.", + "api.escrow.confirm_lost": "The new code no longer waits for confirmation — your previous code still works. Start the process again.", + "api.escrow.confirm_store_failed": "The new code could not be saved — your previous code still works. Try again.", + "api.escrow.confirm_words_mismatch": "The words do not match — check the code you wrote down.", "api.share.form_invalid": "Invalid form — reload the page.", "api.share.too_many_attempts": "Too many failed attempts, try again in 1 minute", "api.export.upload_size_mismatch": "The uploaded size does not match — try again.", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index b7c509d..10ac74c 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -2622,6 +2622,10 @@ "api.escrow.already_running": "Egy kódkészítés már folyamatban van — várd meg, míg befejeződik.", "api.escrow.code_not_shown": "A kód létrejött, de nem lett megjelenítve — biztonsági okból újra nem kérhető le. Indítsd újra a folyamatot: az új kód a régit érvényteleníti.", "api.escrow.no_active_job": "Nincs aktív helyreállítási folyamat — előbb indítsd el a kódkészítést.", + "api.escrow.confirm_expired": "Lejárt az idő az új kód visszaírására — a régi kódod továbbra is érvényes. Indítsd újra a folyamatot.", + "api.escrow.confirm_lost": "Az új kód már nem vár megerősítésre — a régi kódod továbbra is érvényes. Indítsd újra a folyamatot.", + "api.escrow.confirm_store_failed": "Az új kódot nem sikerült elmenteni — a régi kódod továbbra is érvényes. Próbáld újra.", + "api.escrow.confirm_words_mismatch": "A megadott szavak nem egyeznek — ellenőrizd a felírt kódot.", "api.share.form_invalid": "Érvénytelen űrlap — töltsd újra az oldalt.", "api.share.too_many_attempts": "Túl sok sikertelen próbálkozás, próbáld újra 1 perc múlva", "api.export.upload_size_mismatch": "A feltöltött méret nem egyezik — próbáld újra.", diff --git a/controller/internal/web/escrow_handlers.go b/controller/internal/web/escrow_handlers.go index 535b5be..c9460d9 100644 --- a/controller/internal/web/escrow_handlers.go +++ b/controller/internal/web/escrow_handlers.go @@ -32,7 +32,8 @@ type escrowAgent interface { EscrowPreflight(ctx context.Context) (agentapi.EscrowPreflightResponse, error) EscrowCeremonyStart(ctx context.Context) (agentapi.EscrowCeremonyStartResponse, int, error) EscrowCeremonyStatus(ctx context.Context) (agentapi.EscrowCeremonyStatusResponse, error) - EscrowCeremonyClaim(ctx context.Context) (string, int, error) + EscrowCeremonyClaim(ctx context.Context) (agentapi.EscrowClaim, int, error) + EscrowCeremonyConfirm(ctx context.Context, words []string) (int, error) AgentVersion() string } @@ -102,6 +103,8 @@ func (s *Server) ServeEscrowAPI(w http.ResponseWriter, r *http.Request) { s.escrowStatusAPIHandler(w, r) case r.URL.Path == "/api/escrow/claim" && r.Method == http.MethodPost: s.escrowClaimAPIHandler(w, r) + case r.URL.Path == "/api/escrow/confirm" && r.Method == http.MethodPost: + s.escrowConfirmAPIHandler(w, r) default: escrowJSON(w, http.StatusNotFound, nil, "ismeretlen végpont") } @@ -275,7 +278,8 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) { escrowJSON(w, http.StatusBadGateway, nil, "Az ügynök nem elérhető.") return } - code, status, err := agent.EscrowCeremonyClaim(r.Context()) + claim, status, err := agent.EscrowCeremonyClaim(r.Context()) + code := claim.Code if err != nil { switch status { case http.StatusGone: // 410 — the code was minted but never shown; permanently void. @@ -293,7 +297,63 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) { } return } + if claim.ConfirmRequired { + // R-1151: the agent keeps the new package until the household types the two words back (/api/escrow/confirm); + // the ceremony is not complete yet, so nothing is stamped and no report is pushed here. + s.logger.Printf("[INFO] [web] escrow recovery code claimed (one-shot; not logged) — its package waits for the typed-back words") + escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code, "confirm_required": true, "verify_positions": claim.Positions}, "") + code, claim = "", agentapi.EscrowClaim{} + _, _ = code, claim + return + } s.logger.Printf("[INFO] [web] escrow recovery code claimed (one-shot; not logged)") + s.escrowCompleted() + escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code}, "") + code = "" // drop the reference promptly (GC caveat: best-effort) + _ = code +} + +// escrowConfirmAPIHandler (R-1151) takes the two words the household typed back and hands them to the agent, which +// stores the new package on the hub only when they match. Body: JSON {"words": [w1, w2]}. The words are part of the +// code: never logged. On success the ceremony is complete (escrowCompleted). +func (s *Server) escrowConfirmAPIHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + var req struct { + Words []string `json:"words"` + } + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil || len(req.Words) != 2 { + escrowJSON(w, http.StatusBadRequest, nil, s.msg(r, "api.escrow.confirm_words_mismatch")) + return + } + agent, err := s.escrowAgentConn() + if err != nil { + escrowJSON(w, http.StatusBadGateway, nil, "Az ügynök nem elérhető.") + return + } + status, err := agent.EscrowCeremonyConfirm(r.Context(), req.Words) + req.Words = nil + if err != nil { + switch status { + case http.StatusUnprocessableEntity, http.StatusBadRequest: // the words do not match — the wait goes on + escrowJSON(w, http.StatusUnprocessableEntity, nil, s.msg(r, "api.escrow.confirm_words_mismatch")) + case http.StatusGone: + escrowJSON(w, http.StatusGone, map[string]any{"restart": true}, s.msg(r, "api.escrow.confirm_expired")) + case http.StatusNotFound: + escrowJSON(w, http.StatusGone, map[string]any{"restart": true}, s.msg(r, "api.escrow.confirm_lost")) + default: + s.logger.Printf("[WARN] [web] escrow confirm failed (status %d)", status) + escrowJSON(w, http.StatusBadGateway, nil, s.msg(r, "api.escrow.confirm_store_failed")) + } + return + } + s.logger.Printf("[INFO] [web] escrow recovery code typed back — the new package is on the hub (R-1151)") + s.escrowCompleted() + escrowJSON(w, http.StatusOK, map[string]any{"uploaded": true}, "") +} + +// escrowCompleted runs when the new package is on the hub: at the claim for an agent older than 0.160.0, at the +// typed-back confirm since (R-1151). +func (s *Server) escrowCompleted() { // v0.138.0: stamp the ceremony-completed time so /backups/remote shows the "awaiting hub // confirmation" card during the report-cycle gap before the auto-confirmer flips to escrowed // (Phase-0 verdict A: the yellow "szükséges" card during that ~15-min wait was the real gap). @@ -310,9 +370,6 @@ func (s *Server) escrowClaimAPIHandler(w http.ResponseWriter, r *http.Request) { // hub's ACK hash-match flip pending→escrowed in seconds (EscrowAutoConfirmer, unchanged) // instead of after the next ~15-min cycle. Non-blocking; a failed push degrades to the cycle. s.reportTriggerNow() - escrowJSON(w, http.StatusOK, map[string]any{"recovery_code": code}, "") - code = "" // drop the reference promptly (GC caveat: best-effort) - _ = code } // clearAuthFailures clears the login counter after a right re-auth password — the counter reserveLoginAttempt counts diff --git a/controller/internal/web/escrow_wizard_test.go b/controller/internal/web/escrow_wizard_test.go index f481c30..86712e9 100644 --- a/controller/internal/web/escrow_wizard_test.go +++ b/controller/internal/web/escrow_wizard_test.go @@ -35,7 +35,13 @@ type fakeEscrowAgent struct { claimCode string claimStatus int claimErr error - pf agentapi.EscrowPreflightResponse + // R-1151: an agent ≥ 0.160.0 answers the claim with a confirm request; confirmStatus/confirmErr answer the confirm. + claimConfirm bool + claimPos []int + confirmWords []string + confirmStatus int + confirmErr error + pf agentapi.EscrowPreflightResponse } func (f *fakeEscrowAgent) EscrowPreflight(context.Context) (agentapi.EscrowPreflightResponse, error) { @@ -49,9 +55,14 @@ func (f *fakeEscrowAgent) EscrowCeremonyStart(context.Context) (agentapi.EscrowC func (f *fakeEscrowAgent) EscrowCeremonyStatus(context.Context) (agentapi.EscrowCeremonyStatusResponse, error) { return f.status, nil } -func (f *fakeEscrowAgent) EscrowCeremonyClaim(context.Context) (string, int, error) { +func (f *fakeEscrowAgent) EscrowCeremonyClaim(context.Context) (agentapi.EscrowClaim, int, error) { *f.order = append(*f.order, "claim") - return f.claimCode, f.claimStatus, f.claimErr + return agentapi.EscrowClaim{Code: f.claimCode, ConfirmRequired: f.claimConfirm, Positions: f.claimPos}, f.claimStatus, f.claimErr +} +func (f *fakeEscrowAgent) EscrowCeremonyConfirm(_ context.Context, words []string) (int, error) { + *f.order = append(*f.order, "confirm") + f.confirmWords = append([]string(nil), words...) + return f.confirmStatus, f.confirmErr } func (f *fakeEscrowAgent) AgentVersion() string { return f.version } diff --git a/controller/internal/web/r1151_escrow_confirm_test.go b/controller/internal/web/r1151_escrow_confirm_test.go new file mode 100644 index 0000000..2c023bd --- /dev/null +++ b/controller/internal/web/r1151_escrow_confirm_test.go @@ -0,0 +1,123 @@ +package web + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// R-1151 — the wizard's typed-back words reach the agent, which stores the new package only when they match. Until +// then the ceremony is NOT complete: no „completed" stamp, no report push — the hub still holds the previous package. + +func postConfirm(t *testing.T, s *Server, lang, body string) *httptest.ResponseRecorder { + t.Helper() + r := httptest.NewRequest("POST", "/api/escrow/confirm?lang="+lang, strings.NewReader(body)) + r.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + s.escrowConfirmAPIHandler(w, r) + return w +} + +func ceremonyStamped(t *testing.T, h *escrowWizardHarness) bool { + t.Helper() + o := h.sett.GetOffboxTarget() + return o != nil && o.CeremonyCompletedAt != "" +} + +// COMPANION RED-PROOF (observed 2026-10-19): drop the `if claim.ConfirmRequired { … return }` block in +// escrowClaimAPIHandler → this fails with `claim: 200 {"data":{"recovery_code":"egy-ketto-harom-negy"},…}` — no +// confirm request reaches the page, and the claim stamps the ceremony completed. Restored. +func TestR1151_ClaimThatWaitsIsNotCompleteUntilTheWordsComeBack(t *testing.T) { + h := newEscrowWizardHarness(t) + h.configureOffbox(t, "pending") + h.agent.claimCode, h.agent.claimStatus = "egy-ketto-harom-negy", http.StatusOK + h.agent.claimConfirm, h.agent.claimPos = true, []int{2, 4} + + w := httptest.NewRecorder() + h.s.escrowClaimAPIHandler(w, httptest.NewRequest("POST", "/api/escrow/claim", nil)) + if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), `"confirm_required":true`) || !strings.Contains(w.Body.String(), `"verify_positions":[2,4]`) { + t.Fatalf("claim: %d %s", w.Code, w.Body.String()) + } + if ceremonyStamped(t, h) { + t.Fatal("a claim that waits for the typed-back words stamped the ceremony completed") + } + + h.agent.confirmStatus = http.StatusOK + if w := postConfirm(t, h.s, "hu", `{"words":["ketto","negy"]}`); w.Code != http.StatusOK { + t.Fatalf("confirm: %d %s", w.Code, w.Body.String()) + } + if got := strings.Join(h.agent.confirmWords, ","); got != "ketto,negy" { + t.Fatalf("the agent got the words %q, want ketto,negy", got) + } + if !ceremonyStamped(t, h) { + t.Fatal("the confirmed ceremony was not stamped completed") + } + if c := w.Header().Get("Cache-Control"); c != "no-store" { + t.Fatalf("confirm Cache-Control = %q", c) + } +} + +// An agent older than 0.160.0 answers the claim without a confirm request: its package is on the hub, and the claim +// completes the ceremony as before. +func TestR1151_OldAgentClaimCompletesAtOnce(t *testing.T) { + h := newEscrowWizardHarness(t) + h.configureOffbox(t, "pending") + h.agent.claimCode, h.agent.claimStatus = "egy-ketto-harom-negy", 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(), "confirm_required") { + t.Fatalf("claim on an old agent: %d %s", w.Code, w.Body.String()) + } + if !ceremonyStamped(t, h) { + t.Fatal("an old agent's claim must complete the ceremony") + } + for _, o := range h.order { + if o == "confirm" { + t.Fatal("a confirm was sent to an old agent") + } + } +} + +// Every refusal of the confirm answers in the box's language, says the previous code still works where it does, +// and tells the page to start again only when the new code is void. +func TestR1151_ConfirmAnswersInTheBoxLanguage(t *testing.T) { + cases := []struct { + agent int + want int + restart bool + hu, en string + }{ + {http.StatusUnprocessableEntity, http.StatusUnprocessableEntity, false, "A megadott szavak nem egyeznek", "The words do not match"}, + {http.StatusGone, http.StatusGone, true, "Lejárt az idő", "has run out"}, + {http.StatusNotFound, http.StatusGone, true, "már nem vár megerősítésre", "no longer waits"}, + {http.StatusBadGateway, http.StatusBadGateway, false, "nem sikerült elmenteni", "could not be saved"}, + {0, http.StatusBadGateway, false, "nem sikerült elmenteni", "could not be saved"}, + } + for _, c := range cases { + for _, lang := range []string{"hu", "en"} { + h := newEscrowWizardHarness(t) + h.agent.confirmStatus, h.agent.confirmErr = c.agent, fmt.Errorf("agent said %d", c.agent) + w := postConfirm(t, h.s, lang, `{"words":["a","b"]}`) + want := map[string]string{"hu": c.hu, "en": c.en}[lang] + if w.Code != c.want || !strings.Contains(w.Body.String(), want) { + t.Errorf("agent %d, %s: got %d %s — want %d with %q", c.agent, lang, w.Code, w.Body.String(), c.want, want) + } + if got := strings.Contains(w.Body.String(), `"restart":true`); got != c.restart { + t.Errorf("agent %d: restart=%v, want %v", c.agent, got, c.restart) + } + if ceremonyStamped(t, h) { + t.Errorf("agent %d: a refused confirm stamped the ceremony", c.agent) + } + } + } + // A body without two words never reaches the agent. + h := newEscrowWizardHarness(t) + if w := postConfirm(t, h.s, "hu", `{"words":["a"]}`); w.Code != http.StatusBadRequest { + t.Fatalf("one word: got %d", w.Code) + } + if len(h.order) != 0 { + t.Fatalf("a malformed confirm reached the agent: %v", h.order) + } +} diff --git a/controller/internal/web/templates/backups_escrow.html b/controller/internal/web/templates/backups_escrow.html index ee7ccda..a8595a6 100644 --- a/controller/internal/web/templates/backups_escrow.html +++ b/controller/internal/web/templates/backups_escrow.html @@ -98,6 +98,7 @@ + + +