package middleware import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func TestSecurityHeaders(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.Use(SecurityHeaders()) router.GET("/test", func(c *gin.Context) { c.String(200, "OK") }) router.GET("/swagger/index.html", func(c *gin.Context) { c.String(200, "Swagger UI") }) tests := []struct { name string path string wantCSP bool wantSTS bool // Strict-Transport-Security (only for HTTPS) }{ { name: "regular API endpoint", path: "/test", wantCSP: true, wantSTS: false, // HTTP request }, { name: "swagger endpoint no CSP", path: "/swagger/index.html", wantCSP: false, wantSTS: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", tt.path, nil) router.ServeHTTP(w, req) // 基础安全头 assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) assert.Equal(t, "DENY", w.Header().Get("X-Frame-Options")) assert.Equal(t, "strict-origin-when-cross-origin", w.Header().Get("Referrer-Policy")) assert.Equal(t, "camera=(), microphone=(), geolocation=()", w.Header().Get("Permissions-Policy")) assert.Equal(t, "same-origin", w.Header().Get("Cross-Origin-Opener-Policy")) assert.Equal(t, "none", w.Header().Get("X-Permitted-Cross-Domain-Policies")) // CSP 头 if tt.wantCSP { assert.NotEmpty(t, w.Header().Get("Content-Security-Policy")) } }) } } func TestShouldAttachCSP(t *testing.T) { tests := []struct { name string routePath string requestPath string expected bool }{ { name: "non-swagger path", routePath: "/api/v1/users", requestPath: "/api/v1/users", expected: true, }, { name: "swagger path", routePath: "/swagger/index.html", requestPath: "/swagger/index.html", expected: false, }, { name: "swagger subpath", routePath: "/swagger/api-docs", requestPath: "/swagger/api-docs", expected: false, }, { name: "empty routePath uses requestPath", routePath: "", requestPath: "/swagger/", expected: false, }, { name: "trimmed spaces", routePath: " /api/v1/users ", requestPath: "/api/v1/users", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := shouldAttachCSP(tt.routePath, tt.requestPath) assert.Equal(t, tt.expected, result) }) } } func TestIsHTTPSRequest(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string setup func(*http.Request) expected bool }{ { name: "plain HTTP request", setup: func(req *http.Request) {}, expected: false, }, { name: "X-Forwarded-Proto is https", setup: func(req *http.Request) { req.Header.Set("X-Forwarded-Proto", "https") }, expected: true, }, { name: "X-Forwarded-Proto is HTTPS (uppercase)", setup: func(req *http.Request) { req.Header.Set("X-Forwarded-Proto", "HTTPS") }, expected: true, }, { name: "X-Forwarded-Proto is http", setup: func(req *http.Request) { req.Header.Set("X-Forwarded-Proto", "http") }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) req, _ := http.NewRequest("GET", "/test", nil) tt.setup(req) c.Request = req result := isHTTPSRequest(c) assert.Equal(t, tt.expected, result) }) } }