package handlers import ( "net/http" "net/http/httptest" "strings" "testing" windowtypes "github.com/oy/openyield/x/window/types" ) func TestWindowOpenCreatesWindowStatusOpen(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() body := "grantor_holder=holder-alia&grantee=service-1&scope_kind=ReadStash&resource_id=stash-holder-alia&max_actions=5" req := httptest.NewRequest("POST", "/window", strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") mux.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Fatalf("POST /window: status %d, want 302", rec.Code) } loc := rec.Header().Get("Location") if !strings.HasPrefix(loc, "/window/window-") { t.Errorf("POST /window: Location %q, want /window/window-...", loc) } // Extract the windowID and verify it exists with Status=Open + an initial AuditEntry. windowID := strings.TrimPrefix(loc, "/window/") win, ok := srv.Store.GetWindow(windowID) if !ok { t.Fatalf("POST /window: GetWindow(%q) miss", windowID) } if win.Status != windowtypes.StatusOpen { t.Errorf("POST /window: created Window status %q, want Open", win.Status) } audit := srv.Store.GetAuditLog(windowID) if len(audit) != 1 { t.Errorf("POST /window: audit log len %d, want 1 (initial entry)", len(audit)) } if audit[0].Action != "open" { t.Errorf("POST /window: initial audit action %q, want open", audit[0].Action) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowActivateTransitionsOpenToActive(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, err := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) if err != nil { t.Fatalf("OpenWindow: %v", err) } rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/"+win.WindowID+"/activate", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("POST activate: status %d, want 303", rec.Code) } // Lifecycle correctness: assert the real Window.Activate() was invoked // (the handler calls store.ActivateWindow which calls w.Activate()). updated, ok := srv.Store.GetWindow(win.WindowID) if !ok { t.Fatal("window missing after activate") } if updated.Status != windowtypes.StatusActive { t.Errorf("after activate: status %q, want Active (Window.Activate was NOT invoked)", updated.Status) } audit := srv.Store.GetAuditLog(win.WindowID) if len(audit) != 2 { t.Errorf("after activate: audit log len %d, want 2 (initial + activate)", len(audit)) } if audit[1].Action != "activate" { t.Errorf("after activate: audit[1].Action %q, want activate", audit[1].Action) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowRevokeTransitionsToRevoked(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/"+win.WindowID+"/revoke", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("POST revoke: status %d, want 303", rec.Code) } updated, _ := srv.Store.GetWindow(win.WindowID) if updated.Status != windowtypes.StatusRevoked { t.Errorf("after revoke: status %q, want Revoked (Window.Revoke was NOT invoked)", updated.Status) } if !updated.Revoked { t.Errorf("after revoke: Revoked flag false, want true") } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowRevokeIdempotentOnAlreadyRevoked(t *testing.T) { srv := newTestServer(t) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) _ = srv.Store.RevokeWindow(win.WindowID) auditBefore := len(srv.Store.GetAuditLog(win.WindowID)) // Second revoke is a no-op (idempotent): no new AuditEntry. _ = srv.Store.RevokeWindow(win.WindowID) auditAfter := len(srv.Store.GetAuditLog(win.WindowID)) if auditAfter != auditBefore { t.Errorf("idempotent revoke: audit log grew %d -> %d (revoke on already-revoked must be a no-op)", auditBefore, auditAfter) } } func TestWindowRevokeOnExpiredIsNoOp(t *testing.T) { srv := newTestServer(t) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) _ = srv.Store.ExpireWindow(win.WindowID) auditBefore := len(srv.Store.GetAuditLog(win.WindowID)) // Revoke on an Expired window is a no-op (Expired is terminal — v0.2 contract). _ = srv.Store.RevokeWindow(win.WindowID) updated, _ := srv.Store.GetWindow(win.WindowID) if updated.Status != windowtypes.StatusExpired { t.Errorf("revoke-on-expired: status %q, want Expired (terminal state must win)", updated.Status) } auditAfter := len(srv.Store.GetAuditLog(win.WindowID)) if auditAfter != auditBefore { t.Errorf("revoke-on-expired: audit log grew %d -> %d (must be a no-op)", auditBefore, auditAfter) } } func TestWindowExpireTransitionsToExpired(t *testing.T) { srv := newTestServer(t) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) _ = srv.Store.ExpireWindow(win.WindowID) updated, _ := srv.Store.GetWindow(win.WindowID) if updated.Status != windowtypes.StatusExpired { t.Errorf("after expire: status %q, want Expired (Window.Expire was NOT invoked)", updated.Status) } } func TestWindowDetailRendersLifecycleAndAuditLog(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/window/"+win.WindowID, nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /window/%s: status %d, want 200", win.WindowID, rec.Code) } body := rec.Body.String() if !strings.Contains(body, "Active") { t.Errorf("detail: body missing Active badge") } if !strings.Contains(body, "activate") { t.Errorf("detail: body missing activate audit-log entry") } if !strings.Contains(body, "open") { t.Errorf("detail: body missing open audit-log entry") } assertNoBannedTerms(t, body) } func TestWindowDetailMissingReturns404(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/window/window-nobody", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("GET /window/window-nobody: status %d, want 404", rec.Code) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowOpenEmptyGrantorReturns400(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() body := "grantor_holder=&grantee=service-1&scope_kind=ReadStash" req := httptest.NewRequest("POST", "/window", strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("POST /window empty grantor: status %d, want 400", rec.Code) } // G-026: rendered-HTML lexicon check on the ERROR response body too. assertNoBannedTerms(t, rec.Body.String()) } // Compile-time assertion that the handler uses the real x/window/types struct // (D-067: the UI grounds in the real Go type definitions). var _ windowtypes.Window func TestWindowListRendersSeededEmpty(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/window", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /window: status %d, want 200", rec.Code) } body := rec.Body.String() // No windows yet for holder-alia (fresh store) -> empty message. if !strings.Contains(body, "Open a Window") { t.Errorf("GET /window: body missing 'Open a Window' link") } assertNoBannedTerms(t, body) } func TestWindowListRendersCreatedWindows(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 5, PerDurationSeconds: 3600} w, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/window", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /window: status %d, want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, w.WindowID) { t.Errorf("GET /window: body missing created window %s", w.WindowID) } if !strings.Contains(body, "service-1") { t.Errorf("GET /window: body missing grantee service-1") } assertNoBannedTerms(t, body) } func TestWindowNewRendersForm(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/window/new", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET /window/new: status %d, want 200", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "Open a Window") { t.Errorf("GET /window/new: body missing 'Open a Window' label") } if !strings.Contains(body, "ReadStash") { t.Errorf("GET /window/new: body missing ScopeKind option ReadStash") } if !strings.Contains(body, "ProcessPassActForStand") { t.Errorf("GET /window/new: body missing ScopeKind option ProcessPassActForStand") } assertNoBannedTerms(t, body) } func TestWindowOpenEmptyGranteeReturns400(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() body := "grantor_holder=holder-alia&grantee=&scope_kind=ReadStash" req := httptest.NewRequest("POST", "/window", strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("POST /window empty grantee: status %d, want 400", rec.Code) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowActivateNotFoundReturns400(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/window-nobody/activate", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("POST activate nobody: status %d, want 400", rec.Code) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowRevokeNotFoundReturns400(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/window-nobody/revoke", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("POST revoke nobody: status %d, want 400", rec.Code) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowExpireNotFoundReturns400(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/window-nobody/expire", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("POST expire nobody: status %d, want 400", rec.Code) } assertNoBannedTerms(t, rec.Body.String()) } func TestWindowRevokeAndExpireHandlersRedirect(t *testing.T) { srv := newTestServer(t) mux := http.NewServeMux() srv.Register(mux) scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-holder-alia"} rl := windowtypes.RateLimit{MaxActions: 10, PerDurationSeconds: 3600} win, _ := srv.Store.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl) _ = srv.Store.ActivateWindow(win.WindowID) rec := httptest.NewRecorder() req := httptest.NewRequest("POST", "/window/"+win.WindowID+"/revoke", nil) mux.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("POST revoke: status %d, want 303", rec.Code) } // Expire on a revoked window: revoked is not terminal for expire, so it // transitions to Expired (Window.Expire sets status unconditionally). rec2 := httptest.NewRecorder() req2 := httptest.NewRequest("POST", "/window/"+win.WindowID+"/expire", nil) mux.ServeHTTP(rec2, req2) if rec2.Code != http.StatusSeeOther { t.Fatalf("POST expire: status %d, want 303", rec2.Code) } }