package api import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "visiona-backend/internal/usersession" ) func init() { gin.SetMode(gin.TestMode) } // newTestSessionManager 是給 middleware_test 用的最小 SessionManager fixture。 // // OB5 起 AuthMiddleware 必須有 SessionManager — Static fallback 已拔除。 func newTestSessionManager() *usersession.Manager { return usersession.NewManager(usersession.NewInMemoryStore(), usersession.CookieConfig{ Name: "visiona_session", Path: "/", HTTPOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 86400, SigningKey: []byte("middleware-test-signing-key-32b-aa"), }) } // TestRequestIDMiddleware_GeneratesNew 驗證沒帶 header 時會產生新的 request id。 func TestRequestIDMiddleware_GeneratesNew(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, RequestIDFrom(c)) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) body := w.Body.String() assert.NotEmpty(t, body, "request id 應寫入 context") assert.Equal(t, body, w.Header().Get("X-Request-ID"), "header 應與 context 一致") } // TestRequestIDMiddleware_PreservesIncoming 驗證帶 header 時會沿用。 func TestRequestIDMiddleware_PreservesIncoming(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, RequestIDFrom(c)) }) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("X-Request-ID", "upstream-123") r.ServeHTTP(w, req) assert.Equal(t, "upstream-123", w.Body.String()) assert.Equal(t, "upstream-123", w.Header().Get("X-Request-ID")) } // TestAuthMiddleware_NoCookie_Rejects 驗證沒 cookie 時 → 401 + "no_session"。 func TestAuthMiddleware_NoCookie_Rejects(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) r.Use(AuthMiddleware(Deps{SessionManager: newTestSessionManager()})) r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "should not reach") }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil)) assert.Equal(t, http.StatusUnauthorized, w.Code) assert.Contains(t, w.Body.String(), "no_session") } // 註:AuthMiddleware 的「pending session → 401」與「authenticated session → 通過」的 // 完整測試在 oidc_auth_test.go(TestOIDCMiddleware_Allows_AuthenticatedSession / // TestOIDCMiddleware_Rejects_PendingSession),因為需要走完整 login flow 才能模擬。 // TestRecoveryMiddleware_CatchesPanic 驗證 handler panic 會被攔成 500 + INTERNAL_ERROR。 func TestRecoveryMiddleware_CatchesPanic(t *testing.T) { r := gin.New() r.Use(RequestIDMiddleware()) r.Use(RecoveryMiddleware(nil)) r.GET("/", func(c *gin.Context) { panic("boom") }) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil)) assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, w.Body.String(), ErrCodeInternalError) } // TestStripBearerPrefix 驗證 Bearer token prefix 處理。 func TestStripBearerPrefix(t *testing.T) { assert.Equal(t, "abc123", StripBearerPrefix("Bearer abc123")) assert.Equal(t, "abc123", StripBearerPrefix("abc123")) assert.Equal(t, "", StripBearerPrefix("")) } // TestCORSMiddleware_AllowsConfiguredOrigin 驗證只放行白名單 Origin。 func TestCORSMiddleware_AllowsConfiguredOrigin(t *testing.T) { r := gin.New() r.Use(CORSMiddleware([]string{"http://localhost:3000"})) r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "ok") }) // Allowed origin w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodOptions, "/", nil) req.Header.Set("Origin", "http://localhost:3000") req.Header.Set("Access-Control-Request-Method", "GET") r.ServeHTTP(w, req) assert.True(t, strings.Contains(w.Header().Get("Access-Control-Allow-Origin"), "localhost:3000"), "預期 Allow-Origin 包含 localhost:3000,實際 header: %v", w.Header()) // Disallowed origin w2 := httptest.NewRecorder() req2 := httptest.NewRequest(http.MethodOptions, "/", nil) req2.Header.Set("Origin", "http://evil.example") req2.Header.Set("Access-Control-Request-Method", "GET") r.ServeHTTP(w2, req2) assert.NotContains(t, w2.Header().Get("Access-Control-Allow-Origin"), "evil.example") }