// Package oidctest — flow.go // // 提供「站在 caller (BFF backend) 角度」模擬完整 OIDC redirect flow 的 helper。 // 主要用於 e2e 整合測試:把「使用者打開瀏覽器、輸入帳密、按下同意」這幾個人工步驟 // 黑箱化成一個函式呼叫。 package oidctest import ( "fmt" "net/http" "net/url" "testing" ) // SimulateAuthorizationFlow 模擬「使用者打開 /authorize → 同意登入 → IdP 302 回 redirect_uri」。 // // 給定一個由 visionA-backend 產出的 authorize URL(內含 client_id / redirect_uri / // state / code_challenge / nonce),本函式: // // 1. 對 fake server 的 /authorize 發 GET,禁止 redirect 自動跟隨 // 2. fake server 會 302 回 redirect_uri?code=&state= // 3. 把 Location header 取出回傳 — 這就是 caller 接著要打的 callback URL // // caller 通常拿到 callback URL 之後,會「以 BFF backend client 的角色」打 // /api/auth/callback?code=...&state=...,讓 BFF 完成 token exchange + 建 session。 // // 用 *testing.T 直接 Fatalf 而非回 error,是因為 e2e test 寫法統一: // 任何模擬步驟出錯都應該讓 test 立即停。caller 不必到處檢查 err。 func (s *Server) SimulateAuthorizationFlow(t *testing.T, authorizeURL string) string { t.Helper() if authorizeURL == "" { t.Fatalf("oidctest: SimulateAuthorizationFlow: authorizeURL is empty") } // 用一個拒絕 redirect 的 client,這樣我們才能取到 Location header。 client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } req, err := http.NewRequest(http.MethodGet, authorizeURL, nil) if err != nil { t.Fatalf("oidctest: build authorize request: %v", err) } resp, err := client.Do(req) if err != nil { t.Fatalf("oidctest: GET /authorize failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusFound && resp.StatusCode != http.StatusSeeOther { t.Fatalf("oidctest: /authorize 預期 302/303,得 %d", resp.StatusCode) } loc := resp.Header.Get("Location") if loc == "" { t.Fatalf("oidctest: /authorize 回應缺 Location header") } // sanity check:確認 Location 是合法 URL 且帶 code 參數 u, err := url.Parse(loc) if err != nil { t.Fatalf("oidctest: callback URL 不是合法 URL: %v", err) } if u.Query().Get("code") == "" { t.Fatalf("oidctest: callback URL 缺 code 參數: %s", loc) } return loc } // AuthorizeRedirectError 是 ForceAuthorizeFailure 模擬「IdP 直接拒絕授權」場景時的回傳 error。 // 不直接讓 fake server 回 4xx 是因為真 IdP 通常會 302 帶 ?error=... 回 redirect_uri, // 讓 caller (BFF) 自行處理。 type AuthorizeRedirectError struct { Error string ErrorDescription string } // String 讓 caller 容易在 test failure 訊息中顯示。 func (e AuthorizeRedirectError) String() string { return fmt.Sprintf("authorize_error code=%q desc=%q", e.Error, e.ErrorDescription) }