4d6ec7c7bb
gates / gates (push) Successful in 14s
Four paper debts and one fact given a reader. Hub-only — nothing to bake. A4 — the entry about "the tester's machine" named a risk correctly and labelled it in a way that invited deleting it. Established from the hub's own store: `peti-felhom` is a REAL machine (482 reports, 2026-02-27 → 2026-07-15, a named person's own box) and the 3.6 GB with no key and no backup is real. `david` → `tester-1` is a DIFFERENT record with no host, no escrow and no report, ever — deleted 07:55:49 and re-created 07:56:47 this morning. The prompt's premise conflated the two; the register now says which is which. A1 — R-312/R-313/R-303 recorded as DECIDED with their re-open triggers, and moved out of STATUS's "Waiting on you", which is now empty. A3 — day0-install §C.1 said pushing the installer publishes it. It has not since R-110. Corrected, with the two manifest pins named and an outside-verification command; the one copy that repeated it (a dated audit, true when written) carries a superseded note. A5 — standing rule 5: evidence comes off the machine at the end of the phase that produced it, before any revert. Earned twice in three days on the same box at the same point (R-320). Four homes, plus what to do when it is already gone. R-295 hub half — „Beállító kód" everywhere; „Visszaállító kód" retired. New `reenroll` mail kind so the mail names the page a REBUILT box actually shows („A szerver beállítása"), not the „Elfelejtett jelszó" page it has no login screen to reach. Naming only; the acceptance pin proves the secret is untouched. R-319 — the hub models `guest_net` after 23 days of receiving and discarding it. The signal is `heals_last_hour`, not `state`: a guest the watchdog keeps repairing reads healthy between repairs. `heal_succeeded` decoded too (R-260's lesson). Unknown is never drawn as healthy — three absences, three sentences. No alarm, deliberately. Three red-proofs, mutations asserted applied. Wire-gate checked tags 182 → 190. B1 — the operator's 2026-08-12 dispositions were NOT in the register; they are now. Third allowlist kind for the five ruled "no reader wanted"; `reporting_disabled` reclassified redundant. 8 read · 5 deliberately unread · 1 redundant · 6 still owed. Also filed: R-321 (a deliberately-silent box still alarms stale/down — the checker is age-only, and decoding the flag would not have fixed it), R-322 (the claim guard has never scanned the hub; a hand scan returns zero, so it is a scope gap, not a defect).
125 lines
4.3 KiB
Go
125 lines
4.3 KiB
Go
package web
|
|
|
|
// Customer-claim arc (v0.50.0) — the Setup-tab claim card + the operator resend route.
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/claim"
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type uiFakeMailer struct{ sends []string }
|
|
|
|
func (f *uiFakeMailer) SendClaimEmail(kind, customerID, email, domain, code string) error {
|
|
f.sends = append(f.sends, kind)
|
|
return nil
|
|
}
|
|
|
|
func withUIClaim(t *testing.T, s *Server, st *store.Store) (*claim.Engine, *uiFakeMailer) {
|
|
t.Helper()
|
|
m := &uiFakeMailer{}
|
|
e := &claim.Engine{Store: st, Mailer: m, Logger: log.New(io.Discard, "", 0)}
|
|
s.SetClaimEngine(e)
|
|
return e, m
|
|
}
|
|
|
|
// The Setup tab renders the claim card in each state, and never any plaintext code (there is
|
|
// none to render — the assertion pins that no template regression invents one).
|
|
func TestCustomerPage_ClaimCardStates(t *testing.T) {
|
|
s, st := newTestServer(t)
|
|
e, _ := withUIClaim(t, s, st)
|
|
cc := &store.CustomerConfig{
|
|
CustomerID: "acme", CustomerName: "Acme", Domain: "acme.hu",
|
|
RetrievalPassword: "pw", APIKey: "k", Status: "active", Email: "owner@acme.hu",
|
|
}
|
|
if err := st.SaveCustomerConfig(cc); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// No claim row yet → neutral chip, no resend form.
|
|
html := renderCustomerPage(t, s, "acme")
|
|
if !strings.Contains(html, "no code issued yet") {
|
|
t.Error("missing the no-code-issued chip")
|
|
}
|
|
if strings.Contains(html, "/configs/acme/claim-resend") {
|
|
t.Error("resend form must not render before a code exists")
|
|
}
|
|
|
|
// Issued (unclaimed) → Nyitott chip + resend form; the bcrypt hash must NOT leak into HTML.
|
|
cs, err := e.EnsureIssued(cc)
|
|
if err != nil {
|
|
t.Fatalf("EnsureIssued: %v", err)
|
|
}
|
|
html = renderCustomerPage(t, s, "acme")
|
|
if !strings.Contains(html, "Nyitott — kód kiküldve") {
|
|
t.Error("missing the unclaimed-emailed chip")
|
|
}
|
|
if !strings.Contains(html, "/configs/acme/claim-resend") || !strings.Contains(html, "Kód újraküldése") {
|
|
t.Error("missing the resend form/button")
|
|
}
|
|
if strings.Contains(html, cs.CodeHash) {
|
|
t.Error("the code HASH leaked into the page (nothing claim-secret may render)")
|
|
}
|
|
|
|
// Claimed → Claimed chip + the reset-flavored button label.
|
|
if err := e.MarkClaimed(cc); err != nil {
|
|
t.Fatalf("MarkClaimed: %v", err)
|
|
}
|
|
html = renderCustomerPage(t, s, "acme")
|
|
if !strings.Contains(html, ">Claimed ") {
|
|
t.Error("missing the Claimed chip")
|
|
}
|
|
// R-295: the button says „Beállító kód küldése" — ONE name per secret. It used to read
|
|
// „Visszaállító kód küldése", a near-homograph of the escrow „Helyreállítási kód".
|
|
if !strings.Contains(html, "Beállító kód küldése") {
|
|
t.Error("claimed state should offer the reset-code button, named „Beállító kód küldése”")
|
|
}
|
|
if strings.Contains(html, "Visszaállító kód") {
|
|
t.Error("the retired name „Visszaállító kód” is back on the customer page")
|
|
}
|
|
}
|
|
|
|
// The resend route rotates the code (generation++) and redirects with a flash.
|
|
func TestClaimResendRoute_RotatesAndRedirects(t *testing.T) {
|
|
s, st := newTestServer(t)
|
|
e, m := withUIClaim(t, s, st)
|
|
cc := &store.CustomerConfig{
|
|
CustomerID: "acme", CustomerName: "Acme", Domain: "acme.hu",
|
|
RetrievalPassword: "pw", APIKey: "k", Status: "active", Email: "owner@acme.hu",
|
|
}
|
|
st.SaveCustomerConfig(cc)
|
|
if _, err := e.EnsureIssued(cc); err != nil {
|
|
t.Fatalf("EnsureIssued: %v", err)
|
|
}
|
|
before, _ := st.GetClaim("acme")
|
|
|
|
rr := httptest.NewRecorder()
|
|
s.handleClaimResend(rr, httptest.NewRequest("POST", "/configs/acme/claim-resend", nil), "acme")
|
|
if rr.Code != 303 {
|
|
t.Fatalf("resend status = %d, want 303", rr.Code)
|
|
}
|
|
if loc := rr.Header().Get("Location"); !strings.Contains(loc, "flash=claim-resent") {
|
|
t.Fatalf("resend redirect = %q", loc)
|
|
}
|
|
after, _ := st.GetClaim("acme")
|
|
if after.Generation != before.Generation+1 {
|
|
t.Fatalf("generation %d → %d, want +1", before.Generation, after.Generation)
|
|
}
|
|
if after.CodeHash == before.CodeHash {
|
|
t.Fatal("resend did not rotate the hash")
|
|
}
|
|
if len(m.sends) != 2 {
|
|
t.Fatalf("sends = %v, want issue+resend", m.sends)
|
|
}
|
|
// Sanity: the stored value is a bcrypt hash (never a plaintext code).
|
|
if bcrypt.CompareHashAndPassword([]byte(after.CodeHash), []byte("definitely-wrong")) == nil {
|
|
t.Fatal("stored hash verified a wrong code?!")
|
|
}
|
|
}
|