// oidc_test_helper_test.go — OB5 起測試共用的 OIDC 認證 helper。 // // 為什麼需要這份:OB5 移除 StaticAuth 之後,所有走 AuthMiddleware 的 integration test // 都必須先走完 OIDC login flow 拿 cookie。把這段樣板抽成 helper 讓每個 test // 不必重複「fake OIDC server + login + callback + 取 cookie」這 30 行程式碼。 // // 設計選擇: // - 同個 oidctest.NewServer 在 fixture 整個生命週期共用 — 多個 client 各自登入即可 // - AuthenticatedClient 拿 cookie 後就和原本的 http.DefaultClient 行為一致, // 之後每次打 /api/* 都自動帶 cookie,handler 看到的 UserContext = 預先 set 的 sub // - 不暴露 fake OIDC URL 給 test caller — caller 透過 fixture method 操作即可 package main import ( "net/http" "net/http/cookiejar" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/require" ) // AuthenticatedClient 走完整 OIDC login flow,回傳已帶 visiona_session cookie 的 *http.Client。 // // userID / email:simulate 登入後 backend session 裡會記錄的 OIDC sub / email。 // 同個 fixture 可以呼叫多次(不同 userID),各自拿到獨立的 cookie jar,模擬多 user。 // // 任何步驟出錯直接 t.Fatalf — caller 不必檢 err。 // // 注意:回傳的 client 「會自動跟 redirect」(CheckRedirect=nil),方便 caller 直接打 // /api/* 端點不用自己處理 302。如果你需要做 BFF flow 的 step-by-step assert,請改用 // oidc_e2e_test.go 裡的 newCookieClient。 func (f *testFixture) AuthenticatedClient(t *testing.T, userID, email string) *http.Client { t.Helper() if f.fakeOIDC == nil { t.Fatalf("AuthenticatedClient: fixture.fakeOIDC is nil — fixture wasn't built with setupFixture (which now wires fake OIDC)") } // 設定下一輪 ExchangeCode 的 id_token claims f.fakeOIDC.SetNextIDTokenClaims(map[string]any{ "sub": userID, "email": email, "name": userID, // 簡化:name 與 sub 一致,測試夠用 }) jar, err := cookiejar.New(nil) require.NoError(t, err) // login flow 期間用「不自動 redirect」客戶端控制 302 step-by-step flowClient := &http.Client{ Jar: jar, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 10 * time.Second, } // Step 1: GET /api/auth/login → 應 302 to fake OIDC /authorize loc := getExpect302(t, flowClient, f.apiServer.URL+"/api/auth/login") require.True(t, strings.HasPrefix(loc, f.fakeOIDC.URL+"/authorize"), "login 應 302 to fake OIDC /authorize,得 %s", loc) // Step 2: 模擬 IdP 同意登入 → 拿 callback URL callbackURL := f.fakeOIDC.SimulateAuthorizationFlow(t, loc) // Step 3: GET callback → backend 完成 token exchange + 寫 cookie session → 302 to PostLoginURL _ = getExpect302(t, flowClient, callbackURL) // 驗 cookie 已 set u, err := url.Parse(f.apiServer.URL) require.NoError(t, err) cookies := flowClient.Jar.Cookies(u) var sessCookie *http.Cookie for _, c := range cookies { if c.Name == "visiona_session" { sessCookie = c break } } require.NotNil(t, sessCookie, "expected visiona_session cookie after callback") require.NotEmpty(t, sessCookie.Value, "visiona_session cookie 應有值") // 回傳一個共用同個 cookie jar、但會自動跟 redirect 的 client, // 讓 caller 寫 client.Get/Post 不必處理 302。 return &http.Client{ Jar: jar, Timeout: 30 * time.Second, } }