diff --git a/internal/core/server/setup_wizard_test.go b/internal/core/server/setup_wizard_test.go index b31738f..983c55c 100644 --- a/internal/core/server/setup_wizard_test.go +++ b/internal/core/server/setup_wizard_test.go @@ -705,3 +705,31 @@ func TestSpec005SetupHoldsMidWizard(t *testing.T) { t.Errorf("GET /setup after finish = %d, want 404 (D-045)", rec.Code) } } + +// TestSpec005TOTPBeginRefusesAClosedWindow: the wizard's TOTP step must refuse +// once the admin's bounded enrollment window has closed (SPEC-004 §4, A1). It +// must not issue a secret, and the refusal must be a clean 4xx — not the +// server fault a bare propagation of the flow's "no pending identity" would be. +func TestSpec005TOTPBeginRefusesAClosedWindow(t *testing.T) { + h := newSetupHarness(t) + h.beginAdmin(t) + + // Backdate the identity so it sits comfortably outside the bounded window. + if _, err := h.db.Exec( + `UPDATE identities SET created_at = ? WHERE user_id = ?`, + store.FormatTime(time.Now().Add(-365*24*time.Hour)), 1, + ); err != nil { + t.Fatalf("backdate the identity: %v", err) + } + if inWindow, err := h.ids.WithinEnrollmentWindow(h.ctx, 1); err != nil || inWindow { + t.Fatalf("precondition: the window is still open (inWindow=%v err=%v)", inWindow, err) + } + + rec := h.post("admin.totp.begin", nil) + if rec.Code != http.StatusConflict { + t.Fatalf("admin.totp.begin past the window = %d, want 409 (A1)", rec.Code) + } + if _, err := h.ids.TOTPSecret(h.ctx, 1); err == nil { + t.Error("the wizard issued a TOTP secret for an identity outside its window (A1)") + } +}