package localapi import ( "context" "errors" "strings" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/escrow" ) // R-224 — THE STATUS IS THE DISCRIMINATOR, and this test asserts the CONSEQUENCE (what the HTTP // boundary answers) rather than the mechanism (that the sentinel exists). // // The controller one trust tier down classifies on the STATUS and must never parse the sentence. So // the contract this pins is: four distinguishable situations, four distinct statuses, and the // wrong-code message reachable ONLY from a real refusal. // // Before R-224 the first and last rows both answered 400 with the same sentence — which is how // CAMPAIGN-11 F3 told a customer holding a CORRECT code that it did not open their package. type fakeRecoverer struct{ err error } func (f fakeRecoverer) RecoverOffsiteRepoPassword(context.Context, string) (string, error) { if f.err != nil { return "", f.err } return "0123456789abcdef0123456789abcdef", nil } func TestRecoverOffsitePassword_EachSituationGetsItsOwnStatus(t *testing.T) { cases := []struct { name string err error wantStatus int // mustNotSay guards the specific misattribution each status exists to prevent. mustNotSay []string }{ { name: "fetch failed — the code was NEVER used", err: errors.Join(escrow.ErrBundleFetch, errors.New("hub: transport error: no route to host")), wantStatus: 502, mustNotSay: []string{"did not open"}, }, { name: "wrong code — the bundle WAS fetched and refused it", err: errors.New("escrow: the recovery code did not unwrap the identity escrow"), wantStatus: 400, mustNotSay: []string{"could not be fetched"}, }, { name: "the hub holds no bundle", err: escrow.ErrNoEscrowBlob, wantStatus: 404, mustNotSay: []string{"did not open"}, }, { name: "the bundle predates the repository-password field", err: escrow.ErrNoResticPassword, wantStatus: 409, mustNotSay: []string{"could not be fetched"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { srv := newTestServerS(t, &fakeGuests{}, &fakeBackups{}, &fakeStore{}, nil) srv.escrowRecovery = fakeRecoverer{err: tc.err} w := do(t, srv.Handler(), "POST", "/escrow/recover-offsite-password", "A", `{"vmid":8200,"recovery_code":"correct horse battery staple sedative anaconda wobbly kingdom placard yodel"}`) if w.Code != tc.wantStatus { t.Fatalf("status: got %d, want %d — body=%s", w.Code, tc.wantStatus, w.Body.String()) } for _, phrase := range tc.mustNotSay { if strings.Contains(w.Body.String(), phrase) { t.Fatalf("the %d answer must not say %q — body=%s", tc.wantStatus, phrase, w.Body.String()) } } }) } } // The pair that matters most, stated as its own assertion so a regression cannot hide inside a table: // a fetch failure and a wrong code must never answer with the SAME status. Collapsing them is the // whole of R-224. func TestRecoverOffsitePassword_FetchFailureAndWrongCodeDiffer(t *testing.T) { status := func(err error) int { srv := newTestServerS(t, &fakeGuests{}, &fakeBackups{}, &fakeStore{}, nil) srv.escrowRecovery = fakeRecoverer{err: err} return do(t, srv.Handler(), "POST", "/escrow/recover-offsite-password", "A", `{"vmid":8200,"recovery_code":"correct horse battery staple sedative anaconda wobbly kingdom placard yodel"}`).Code } fetch := status(errors.Join(escrow.ErrBundleFetch, errors.New("no route to host"))) wrong := status(errors.New("escrow: the recovery code did not unwrap the identity escrow")) // RED-PROOF: delete the ErrBundleFetch case from handleRecoverOffsitePassword → both become 400 // → this FAILS. That is the exact pre-R-224 code, and the exact defect CAMPAIGN-11 measured. if fetch == wrong { t.Fatalf("a failed fetch and a wrong code must not share a status (both %d)", fetch) } }