From bb362de466ec2188592546589e424ebd35939ee6 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 25 Mar 2026 23:37:28 +0000 Subject: [PATCH] test(cloudcp): enforce Pulse Account bootstrap parity --- internal/cloudcp/portal/handlers_test.go | 93 ++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/internal/cloudcp/portal/handlers_test.go b/internal/cloudcp/portal/handlers_test.go index 61a3c3680..5e92e5fea 100644 --- a/internal/cloudcp/portal/handlers_test.go +++ b/internal/cloudcp/portal/handlers_test.go @@ -3,6 +3,7 @@ package portal import ( "encoding/json" "fmt" + "html/template" "net/http" "net/http/httptest" "sort" @@ -62,6 +63,21 @@ func renderLoginHTML(t *testing.T, nonce string) string { return rec.Body.String() } +func extractPortalBootstrapJSONFromHTML(t *testing.T, html string) string { + t.Helper() + startMarker := ``) + if end < 0 { + t.Fatal("bootstrap script closing tag not found in portal HTML") + } + return html[start : start+end] +} + func newPortalSessionFixture(t *testing.T) (*registry.TenantRegistry, *cpauth.Service, string, string, string) { t.Helper() @@ -975,6 +991,83 @@ func TestHandlePortalBootstrap_RevokedSessionUnauthorized(t *testing.T) { } } +func TestPortalBootstrapHTMLAndHandlerStayInSync(t *testing.T) { + reg, sessionSvc, token, accountID, _ := newPortalSessionFixture(t) + + if err := reg.Create(®istry.Tenant{ + ID: "t_sync", + AccountID: accountID, + DisplayName: "Sync Workspace", + State: registry.TenantStateActive, + CreatedAt: time.Date(2026, 3, 25, 11, 0, 0, 0, time.UTC), + HealthCheckOK: true, + }); err != nil { + t.Fatal(err) + } + + claims, err := validatePortalSessionClaims(httptest.NewRequest(http.MethodGet, "/portal", nil), nil, nil) + if err == nil || claims != nil { + t.Fatal("expected nil-session validation to require auth") + } + + loadedAccounts, err := loadPortalAccountsForUser(reg, strings.TrimSpace("u_missing")) + if err != nil { + t.Fatalf("loadPortalAccountsForUser for missing user: %v", err) + } + if len(loadedAccounts) != 0 { + t.Fatalf("missing user accounts = %d, want 0", len(loadedAccounts)) + } + + req := httptest.NewRequest(http.MethodGet, "/api/portal/bootstrap", nil) + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() + HandlePortalBootstrap(sessionSvc, reg).ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("bootstrap status = %d, want %d", rec.Code, http.StatusOK) + } + + claimsReq := httptest.NewRequest(http.MethodGet, "/portal", nil) + claimsReq.Header.Set("Authorization", "Bearer "+token) + validClaims, err := validatePortalSessionClaims(claimsReq, sessionSvc, reg) + if err != nil { + t.Fatalf("validatePortalSessionClaims: %v", err) + } + accounts, err := loadPortalAccountsForUser(reg, validClaims.UserID) + if err != nil { + t.Fatalf("loadPortalAccountsForUser: %v", err) + } + html := renderPortalHTML(t, portalPageData{ + Nonce: "test-nonce", + Email: validClaims.Email, + PublicSiteURL: defaultPublicSiteURL, + SupportEmail: defaultSupportEmail, + CommercialAPIBaseURL: defaultCommercialAPIBaseURL, + PortalPath: defaultPortalPath, + LogoutPath: defaultLogoutPath, + AccountAPIBasePath: defaultAccountAPIBasePath, + PortalAPIBasePath: defaultPortalAPIBasePath, + Accounts: accounts, + Styles: portalStyles, + Script: portalScript, + BootstrapJSON: mustBootstrapJSON(t, validClaims.Email, accounts), + }) + + handlerJSON := strings.TrimSpace(rec.Body.String()) + pageJSON := strings.TrimSpace(extractPortalBootstrapJSONFromHTML(t, html)) + if handlerJSON != pageJSON { + t.Fatalf("bootstrap payload drift:\nhandler=%s\npage=%s", handlerJSON, pageJSON) + } +} + +func mustBootstrapJSON(t *testing.T, email string, accounts []portalPageAccount) template.JS { + t.Helper() + bootstrapJSON, err := buildPortalBootstrapJSON(email, accounts) + if err != nil { + t.Fatalf("buildPortalBootstrapJSON: %v", err) + } + return bootstrapJSON +} + func TestPortalLoginTemplate_UsesPulseAccountBranding(t *testing.T) { html := renderLoginHTML(t, "test-nonce")