package web import ( "bytes" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // ── R-295 — ONE NAME PER SECRET ───────────────────────────────────────────────────────────────── // // Two different secrets were both called „Visszaállító kód": // // - the THREE-word code that gives a person control of the dashboard (the claim/reset code), and // - the TEN-word code that opens the sealed off-site backups (the escrow recovery code). // // The names are near-homographs of each other and of „Helyreállítási kód", and the collision cost a // real code. The ruling: the dashboard code is „Beállító kód" everywhere — the name the box already // showed on the page where it is typed — the escrow code is „Helyreállítási kód", and „Visszaállító // kód" is retired. Where one secret serves two situations the NAME stays constant and the SENTENCE // changes. // // THIS IS NAMING, NOT FUNCTION. TestResetCode_StillAcceptedOnTheSetupPage below is the pin that says // so: the code kept working throughout, and a rename that quietly broke acceptance would be a far // worse outcome than the collision it fixed. // claimPageHTML renders the claim page in one of its two branches through the PRODUCTION template // tree. IsReset is the only field varied — in production it is `s.authEnabled()` (a set password // means this is the reset flow), and it is the branch that used to rename the secret. func claimPageHTML(t *testing.T, isReset bool) string { t.Helper() s := testServer(t) s.loadTemplates() var buf bytes.Buffer data := map[string]interface{}{ "Title": "A szerver beállítása", "CustomerName": "Teszt Ügyfél", "Domain": "pelda.hu", "ClaimCSRF": "t", "IsReset": isReset, "HasCode": true, "MinPassword": 12, } if err := s.tmpl.ExecuteTemplate(&buf, "claim", data); err != nil { t.Fatalf("render claim (IsReset=%v): %v", isReset, err) } return buf.String() } // RED-PROOF: restore `{{if .IsReset}}Visszaállító kód{{else}}Beállító kód{{end}}` on the label in // claim.html and the reset branch fails here, with the retired name quoted back. func TestClaimPage_BothBranchesNameTheSameSecretTheSameWay(t *testing.T) { for _, isReset := range []bool{false, true} { branch := "first-time" if isReset { branch = "reset" } html := claimPageHTML(t, isReset) if strings.Contains(html, "isszaállító kód") { t.Errorf("[%s branch] the retired name „Visszaállító kód" + "\" is still on the page — it collides with the escrow „Helyreállítási kód", branch) } if !strings.Contains(html, "eállító kód") { t.Errorf("[%s branch] the page no longer names the secret „Beállító kód" + "\" at all", branch) } } } // The escrow code's name must NOT appear on the dashboard-claim page — that confusion is the whole // finding. (Substring is ASCII-safe on purpose: an accented pattern that fails to match reads exactly // like the string being absent.) func TestClaimPage_DoesNotMentionTheEscrowCodeName(t *testing.T) { for _, isReset := range []bool{false, true} { if html := claimPageHTML(t, isReset); strings.Contains(html, "elyreállítási kód") { t.Error("the claim page names the ESCROW code — the two secrets are different, and " + "naming one on the other's page is how a customer types the wrong one") } } } // ── The pin the ruling explicitly asks for: acceptance did not move ───────────────────────────── // A reset-issued code is still accepted on the setup page and still sets the password. The rename // touched copy only; if this ever fails, a naming change has broken a recovery path. func TestResetCode_StillAcceptedOnTheSetupPage(t *testing.T) { s, code, sett := claimTestServer(t) form := url.Values{ "_csrf": {s.claimCSRFToken()}, "code": {code}, "new_password": {"a-strong-passphrase-12"}, "confirm_password": {"a-strong-passphrase-12"}, } req := httptest.NewRequest(http.MethodPost, "/claim", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(&http.Cookie{Name: claimCSRFCookie, Value: s.claimCSRFToken()}) rr := httptest.NewRecorder() s.handleClaimSubmit(rr, req) if rr.Code != http.StatusFound { t.Fatalf("a valid code was REFUSED after the rename: got %d, body=%q — the ruling was that "+ "this is naming, not function", rr.Code, claimFirstLine(rr.Body.String())) } if !sett.GetClaimed() { t.Error("the box was not marked claimed — acceptance logic moved with the copy") } if !s.authEnabled() { t.Error("the password was not set — acceptance logic moved with the copy") } }