Files
felhom.eu/hub/internal/web/selfbind_naming_test.go
T
admin b03a105375
gates / gates (push) Successful in 17s
hub v0.105.0: the third name, a machine told to be quiet, and a guard for the hub's own words
Hub only. No controller change, no agent change, no wire change — nothing to bake.
demo-hp untouched: the operator is re-deploying it this evening.

R-323 — the five-word phrase is „Tulajdonosi jelmondat". It was „Visszaállító
jelszó": one word from the name retired last week, and false besides — it restores
nothing, it proves the account owns the box being bound. Five sites, all in the hub;
felhom-controller and felhom-agent carry the name nowhere, so no halt and no bake.
Both suggested names were rejected with reasons: „Fiókjelszó" would collide with the
dashboard login (a DIFFERENT real secret), and „Összekötési jelszó" would leave the
two factors on this page separated only by kód-versus-jelszó — the exact shape being
removed, since the other factor is the „Párosító kód". The chosen name differs on
both axes, stem and noun. Naming only; the acceptance pin drives the real handler.

R-324 — the hub's customer copy is under a guard for the first time. Retired names
banned across all 95 hub files; retrieval stems registered in four declared customer
surfaces. The selftest found a defect in its own instrument on the first run. One
shared vocabulary in scripts/, drift-checked into the controller gate rather than
copied (R-325 removes the scaffold).

R-321 — a machine we told to be quiet is no longer reported as dead, and it was two
doors, not one: because the state is RECORDED rather than deleted, the morning
deadline check can skip it too. A deleted state returns "", which is not "down" —
R-195's shape returning through a second door. The clock runs from the report the hub
can see, so re-enabling starts it there and emits no recovery for an outage that never
happened. Three red-proofs; the one that matters showed a genuinely dead machine
sitting at "disabled" when the suppression was made unconditional.

R-326 — "which claims are unproven" is answerable by a command now. The nine I have
been repeating was the count of claims the 9 August pass DOWNGRADED, not the count of
unproven ones. The real figures: 55 claims, 23 walked, 32 not — and only 6 of those 32
cite evidence. Its first run found a stale claim (R-327).
2026-08-13 15:50:32 +02:00

133 lines
6.4 KiB
Go

package web
// R-323 — THE THIRD NAME. One secret, one name, and none of them a near-homograph of another.
//
// Three secrets a customer can hold, and they must be tellable apart by a hurried reader:
//
// three words takes control of the dashboard „Beállító kód"
// ten words opens the sealed off-site backups „Helyreállítási kód"
// five words proves the account owns the box being bound „Tulajdonosi jelmondat" ← this one
//
// The five-word phrase used to be „Visszaállító jelszó" — one word away from „Visszaállító kód",
// which R-295 had just retired for colliding with „Helyreállítási kód". Found while shipping that
// rename and deliberately NOT swept in with it; ruled on separately by the operator 2026-08-13.
//
// WHY NOT THE OBVIOUS CANDIDATES, since a test is where the reasoning survives:
//
// „Összekötési jelszó" — the OTHER factor on this very page is the „Párosító kód". Naming this one
// after the same act would leave the two factors a customer types in one sitting separated
// only by kód-versus-jelszó, which is structurally the „Visszaállító kód"/„Visszaállító
// jelszó" trap being removed.
// „Fiókjelszó" — there IS an account password (the dashboard login). This would collide
// with a DIFFERENT real secret, i.e. trade one homograph for a worse one.
//
// „Tulajdonosi jelmondat" is distinct on BOTH axes — stem (Tulajdonosi vs Beállító / Helyreállítási
// / Párosító) and noun (jelmondat vs kód / jelszó) — and it says what the phrase actually does.
import (
"net/http"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// retiredThirdName is the name this change removes. It is a SUBSTRING check on the stem so that a
// possessive or accusative form („visszaállító jelszavadat") cannot slip past a check written
// against the nominative — the R-299 lesson, where a guard matched one inflection of a Hungarian
// verb and the plural walked straight by.
const retiredThirdName = "isszaállító jelsz"
// The binding page names the secret „Tulajdonosi jelmondat" and nowhere carries the retired name.
// Driven through the real handler, so a template that never renders is visible here.
func TestSelfBind_ThirdSecretNaming(t *testing.T) {
s, st := newTestServer(t)
selfBindSetup(t, st, "acme", testCode)
token := mintLink(t, st, "acme", selfBindTTL)
body := bindGET(t, s, token).Body.String()
if !strings.Contains(body, "Tulajdonosi jelmondat") {
t.Errorf("the field label must name the secret „Tulajdonosi jelmondat”:\n%s", body)
}
if strings.Contains(body, retiredThirdName) {
t.Error("the retired name „Visszaállító jelszó” is back on the binding page")
}
// The two factors on this page must not be near-homographs of each other. The pairing code keeps
// its name; what matters is that the SECOND factor no longer differs from it by one noun.
if !strings.Contains(body, "Párosító kód") {
t.Error("the pairing code lost its name — the two factors must both be named")
}
// Neither of the OTHER two secrets may be named on this page: a customer who reads „kód" here
// twice is the failure this whole arc exists to prevent.
for _, other := range []string{"Beállító kód", "Helyreállítási kód"} {
if strings.Contains(body, other) {
t.Errorf("the binding page names a different secret (%q) — that is the collision", other)
}
}
}
// The failure banner must name the secret the same way the label does. A page that asks for a
// „Tulajdonosi jelmondat" and then complains about „a jelszó" has two names for one secret again,
// which is the defect in miniature.
func TestSelfBind_FailureBannerUsesTheSameName(t *testing.T) {
s, st := newTestServer(t)
selfBindSetup(t, st, "acme", testCode)
token := mintLink(t, st, "acme", selfBindTTL)
rr := bindPOST(t, s, token, testCodeFmt, "wrong wrong wrong wrong wrong")
body := rr.Body.String()
if !strings.Contains(body, "tulajdonosi jelmondat") {
t.Errorf("the failure banner does not name the secret the way the label does:\n%s", body)
}
if strings.Contains(body, retiredThirdName) {
t.Error("the retired name is on the failure banner")
}
}
// The e-mail that carries the link names it identically. The mail is read BEFORE the page, so a
// mismatch here is the customer's first impression of two different secrets.
func TestSelfBindEmail_UsesTheSameName(t *testing.T) {
subject, body := notify.FormatSelfBindEmail("acme", "https://hub.example/bind/tok")
if !strings.Contains(body, "tulajdonosi jelmondatodat") {
t.Errorf("the self-bind mail does not name the secret „Tulajdonosi jelmondat”:\n%s", body)
}
if strings.Contains(subject+body, retiredThirdName) {
t.Error("the retired name is still in the self-bind mail")
}
// It must still say what the thing IS — a rename that leaves the customer unable to recognise
// what they are holding has fixed a collision and broken the delivery.
if !strings.Contains(body, "5 szóból álló kifejezést") {
t.Error("the mail no longer says the phrase is five words")
}
}
// THE ACCEPTANCE PIN — the whole point of calling this naming rather than function.
//
// The SAME phrase, typed the same messy way a human types it (odd spacing, mixed case, mixed
// separators), still binds the appliance after the rename. If this ever goes red, the rename stopped
// being a rename.
func TestSelfBindPassphrase_StillAcceptedAfterTheRename(t *testing.T) {
s, st := newTestServer(t)
id := selfBindSetup(t, st, "acme", testCode)
token := mintLink(t, st, "acme", selfBindTTL)
rr := bindPOST(t, s, token, testCodeFmt, " Alpha Beta gamma-delta epsilon ")
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "egy percen belül") {
t.Fatalf("the same passphrase stopped being accepted: code=%d body=%q", rr.Code, rr.Body.String())
}
if a, _ := st.GetAppliance(id); a == nil || a.Status != store.ApplianceBound {
t.Fatalf("the appliance did not bind: %+v", a)
}
// The form field NAME is deliberately still `passphrase`: renaming customer copy must not touch
// the wire. A changed field name would be an acceptance change wearing a rename's clothes.
body := bindGET(t, s, mintLink(t, st, "acme", selfBindTTL)).Body.String()
if !strings.Contains(body, `name="passphrase"`) {
t.Error("the form field name changed — that is not a rename, that is a wire change")
}
}