package e2e import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/gochat/gochat/internal/middleware" ) // CSRFE2ETestSuite tests the CSRF middleware's double-submit cookie pattern // end-to-end using a lightweight Gin router + httptest.Server (no database). // // Reference: OWASP CSRF Prevention Cheat Sheet — Double-Submit Cookie pattern. // Unlike Chatwoot (Rails form_authenticity_token), this is API-first and stateless. type CSRFE2ETestSuite struct { suite.Suite server *httptest.Server router *gin.Engine } // SetupSuite builds a minimal Gin router with CSRF middleware and a test endpoint. func (s *CSRFE2ETestSuite) SetupSuite() { // E2E tests require PostgreSQL; skip in SQLite test mode. s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode") } // TearDownSuite shuts down the httptest server (if running). func (s *CSRFE2ETestSuite) TearDownSuite() { if s.server != nil { s.server.Close() } } // buildServer creates a fresh httptest.Server with the given CSRF config. // This allows each test to have its own server configuration without // state leakage between tests. func (s *CSRFE2ETestSuite) buildServer(cfg middleware.CSRFConfig) *httptest.Server { r := gin.New() r.Use(gin.Recovery()) r.Use(middleware.CSRF(cfg)) // Safe-method endpoint — returns JSON with the csrf_token from the context r.GET("/api/test", func(c *gin.Context) { token, exists := c.Get("csrf_token") c.JSON(200, gin.H{ "csrf_token": token, "exists": exists, }) }) // Unsafe-method endpoints — should only reach handler if CSRF validated r.POST("/api/test", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) r.PUT("/api/test", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) r.PATCH("/api/test", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) r.DELETE("/api/test", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // HEAD and OPTIONS also treated as safe by default r.HEAD("/api/test", func(c *gin.Context) { c.Status(200) }) r.OPTIONS("/api/test", func(c *gin.Context) { c.Status(204) }) return httptest.NewServer(r) } // parseJSONBody reads the response body and parses it into a map. func parseJSONBody(body io.Reader) map[string]interface{} { var result map[string]interface{} json.NewDecoder(body).Decode(&result) return result } // ============================================================================ // Test 1: Safe method (GET) receives a CSRF cookie // ============================================================================ func (s *CSRFE2ETestSuite) TestSafeMethod_GET_ReceivesCSRFCookie() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false // needed for httptest (no TLS) server := s.buildServer(cfg) defer server.Close() resp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer resp.Body.Close() // GET should succeed (200 OK) assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "Safe method GET should pass through and return 200") // Response should contain csrf_token in the JSON body bodyData := parseJSONBody(resp.Body) assert.True(s.T(), bodyData["exists"].(bool), "csrf_token should be set in the Gin context") assert.NotEmpty(s.T(), bodyData["csrf_token"], "csrf_token value should not be empty") // CSRF cookie should be present in the response cookies := resp.Cookies() var csrfCookie *http.Cookie for _, c := range cookies { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie, "CSRF cookie should be set on safe request") assert.NotEmpty(s.T(), csrfCookie.Value, "CSRF cookie value should not be empty") assert.Equal(s.T(), "/", csrfCookie.Path, "CSRF cookie path should default to /") assert.Equal(s.T(), http.SameSiteStrictMode, csrfCookie.SameSite, "CSRF cookie SameSite should default to Strict") assert.False(s.T(), csrfCookie.HttpOnly, "CSRF cookie HttpOnly should be false (JS must read it for double-submit)") assert.Equal(s.T(), 3600, csrfCookie.MaxAge, "CSRF cookie MaxAge should default to 3600 seconds") // The cookie value and the body token should match assert.Equal(s.T(), csrfCookie.Value, bodyData["csrf_token"], "Cookie value should match the context csrf_token") } // ============================================================================ // Test 2: Unsafe method (POST) with matching header+cookie passes // ============================================================================ func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_MatchingHeaderAndCookie_Passes() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Step 1: Make a GET request to obtain a CSRF token cookie getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() s.Require().Equal(http.StatusOK, getResp.StatusCode) // Extract the CSRF cookie value var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie, "GET request should set CSRF cookie") tokenValue := csrfCookie.Value // Step 2: Make a POST request with the token in both cookie and header postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.Header.Set("X-CSRF-Token", tokenValue) postReq.AddCookie(csrfCookie) postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() // POST should succeed (200 OK) when tokens match assert.Equal(s.T(), http.StatusOK, postResp.StatusCode, "POST with matching header+cookie should pass through") } // ============================================================================ // Test 3: Unsafe method (POST) without cookie → 403 // ============================================================================ func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_NoCookie_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // POST without any CSRF cookie postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) // Intentionally do NOT set cookie or header postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode, "POST without CSRF cookie should be rejected with 403") bodyData := parseJSONBody(postResp.Body) assert.Equal(s.T(), "CSRF token missing from cookie", bodyData["error"], "Error message should indicate missing cookie token") } // ============================================================================ // Test 4: Unsafe method (POST) without header → 403 // ============================================================================ func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_NoHeader_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Step 1: Get a CSRF cookie via GET getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie, "GET should provide CSRF cookie") // Step 2: POST with cookie but WITHOUT the X-CSRF-Token header postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.AddCookie(csrfCookie) // Intentionally do NOT set X-CSRF-Token header postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode, "POST with cookie but no header should be rejected with 403") bodyData := parseJSONBody(postResp.Body) assert.Equal(s.T(), "CSRF token missing from header", bodyData["error"], "Error message should indicate missing header token") } // ============================================================================ // Test 5: Unsafe method (POST) with mismatched header vs cookie → 403 // ============================================================================ func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_MismatchedToken_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Step 1: Get a valid CSRF cookie via GET getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie, "GET should provide CSRF cookie") // Step 2: POST with the valid cookie but a DIFFERENT (wrong) header value postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.AddCookie(csrfCookie) postReq.Header.Set("X-CSRF-Token", "this-is-a-fake-token-value") postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode, "POST with mismatched tokens should be rejected with 403") bodyData := parseJSONBody(postResp.Body) assert.Equal(s.T(), "CSRF token mismatch", bodyData["error"], "Error message should indicate token mismatch") } // ============================================================================ // Test 6: CSRF disabled → no validation, passes through // ============================================================================ func (s *CSRFE2ETestSuite) TestCSRFDisabled_PassesThrough() { cfg := middleware.CSRFConfig{ Enabled: false, } server := s.buildServer(cfg) defer server.Close() // POST without any CSRF token should succeed when CSRF is disabled postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) // No cookie, no header — but should still pass through postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusOK, postResp.StatusCode, "When CSRF is disabled, POST should pass through without validation") // No CSRF cookie should be set on GET when disabled getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() assert.Equal(s.T(), http.StatusOK, getResp.StatusCode, "When CSRF is disabled, GET should pass through") // Check that no CSRF cookie was set for _, c := range getResp.Cookies() { assert.NotEqual(s.T(), "_gochat_csrf", c.Name, "CSRF cookie should not be set when CSRF is disabled") } // Body should not have csrf_token in context bodyData := parseJSONBody(getResp.Body) assert.Nil(s.T(), bodyData["csrf_token"], "csrf_token should not be set in context when CSRF is disabled") } // ============================================================================ // Test 7: Custom cookie/header names work // ============================================================================ func (s *CSRFE2ETestSuite) TestCustomCookieAndHeaderNames() { cfg := middleware.CSRFConfig{ Enabled: true, Secret: "test-secret-for-custom-names", CookieName: "my_custom_csrf", HeaderName: "X-Custom-CSRF", TokenLength: 32, SafeMethods: []string{"GET", "HEAD", "OPTIONS"}, CookieSecure: false, CookieHTTPOnly: false, CookieSameSite: "Lax", CookiePath: "/api", ExpirySeconds: 7200, } server := s.buildServer(cfg) defer server.Close() // Step 1: GET should set a cookie with the custom name getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() assert.Equal(s.T(), http.StatusOK, getResp.StatusCode) var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "my_custom_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie, "Custom cookie name 'my_custom_csrf' should be set") assert.NotEmpty(s.T(), csrfCookie.Value) assert.Equal(s.T(), "/api", csrfCookie.Path, "Custom cookie path should be '/api'") assert.Equal(s.T(), http.SameSiteLaxMode, csrfCookie.SameSite, "Custom SameSite should be Lax") assert.Equal(s.T(), 7200, csrfCookie.MaxAge, "Custom MaxAge should be 7200 seconds") tokenValue := csrfCookie.Value // Step 2: POST with the custom header name should pass postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.Header.Set("X-Custom-CSRF", tokenValue) postReq.AddCookie(csrfCookie) postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusOK, postResp.StatusCode, "POST with matching custom header+cookie should pass") // Step 3: POST without custom header (using default X-CSRF-Token instead) → 403 postReq2, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq2.Header.Set("X-CSRF-Token", tokenValue) // Wrong header name postReq2.AddCookie(csrfCookie) postResp2, err := http.DefaultClient.Do(postReq2) s.Require().NoError(err) defer postResp2.Body.Close() assert.Equal(s.T(), http.StatusForbidden, postResp2.StatusCode, "POST with default header name (instead of custom) should be rejected") } // ============================================================================ // Test 8: OPTIONS preflight passes (safe method) // ============================================================================ func (s *CSRFE2ETestSuite) TestOPTIONS_Preflight_PassesAndSetsCookie() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() req, err := http.NewRequest("OPTIONS", server.URL+"/api/test", nil) s.Require().NoError(err) resp, err := http.DefaultClient.Do(req) s.Require().NoError(err) defer resp.Body.Close() // OPTIONS is a safe method and should pass through assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode, "OPTIONS preflight should pass through and return 204") // OPTIONS should also set a CSRF cookie (it's a safe method) var csrfCookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } assert.NotNil(s.T(), csrfCookie, "OPTIONS should set a CSRF cookie since it's a safe method") if csrfCookie != nil { assert.NotEmpty(s.T(), csrfCookie.Value, "CSRF cookie from OPTIONS should have a non-empty value") } } // ============================================================================ // Additional edge-case tests // ============================================================================ // Test HEAD (another safe method) sets cookie func (s *CSRFE2ETestSuite) TestHEAD_SafeMethod_SetsCookie() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() req, err := http.NewRequest("HEAD", server.URL+"/api/test", nil) s.Require().NoError(err) resp, err := http.DefaultClient.Do(req) s.Require().NoError(err) defer resp.Body.Close() assert.Equal(s.T(), http.StatusOK, resp.StatusCode, "HEAD should pass through as a safe method") var csrfCookie *http.Cookie for _, c := range resp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } assert.NotNil(s.T(), csrfCookie, "HEAD should set a CSRF cookie since it's a safe method") } // Test PUT (unsafe method) requires CSRF validation func (s *CSRFE2ETestSuite) TestUnsafeMethod_PUT_NoToken_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() req, err := http.NewRequest("PUT", server.URL+"/api/test", nil) s.Require().NoError(err) resp, err := http.DefaultClient.Do(req) s.Require().NoError(err) defer resp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode, "PUT without CSRF token should be rejected with 403") } // Test DELETE (unsafe method) requires CSRF validation func (s *CSRFE2ETestSuite) TestUnsafeMethod_DELETE_NoToken_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() req, err := http.NewRequest("DELETE", server.URL+"/api/test", nil) s.Require().NoError(err) resp, err := http.DefaultClient.Do(req) s.Require().NoError(err) defer resp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode, "DELETE without CSRF token should be rejected with 403") } // Test PATCH (unsafe method) requires CSRF validation func (s *CSRFE2ETestSuite) TestUnsafeMethod_PATCH_NoToken_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() req, err := http.NewRequest("PATCH", server.URL+"/api/test", nil) s.Require().NoError(err) resp, err := http.DefaultClient.Do(req) s.Require().NoError(err) defer resp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode, "PATCH without CSRF token should be rejected with 403") } // Test case-insensitive matching: header value uses different case than cookie func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_CaseInsensitiveMatch_Passes() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Step 1: Get CSRF cookie via GET getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie) // The middleware uses strings.EqualFold for comparison. // Since tokens are hex strings (lowercase by default), we test with uppercase conversion. tokenUpper := strings.ToUpper(csrfCookie.Value) if tokenUpper == csrfCookie.Value { // Token was already uppercase; skip this test (unlikely for hex tokens) s.T().Log("Token was already uppercase, case-insensitive test is trivially passing") } // Step 2: POST with uppercase header matching lowercase cookie (EqualFold) postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.Header.Set("X-CSRF-Token", tokenUpper) postReq.AddCookie(csrfCookie) postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() // strings.EqualFold should match regardless of case assert.Equal(s.T(), http.StatusOK, postResp.StatusCode, "POST with case-different but EqualFold-matching tokens should pass") } // Test that the full double-submit flow works end-to-end: // GET → extract cookie → POST with header = cookie value → success func (s *CSRFE2ETestSuite) TestFullDoubleSubmitFlow() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Phase 1: Client fetches CSRF token via GET getResp, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer getResp.Body.Close() s.Require().Equal(http.StatusOK, getResp.StatusCode) var csrfCookie *http.Cookie for _, c := range getResp.Cookies() { if c.Name == "_gochat_csrf" { csrfCookie = c break } } s.Require().NotNil(csrfCookie) // Phase 2: Client echoes cookie value in header for the state-changing request postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.Header.Set("X-CSRF-Token", csrfCookie.Value) postReq.AddCookie(csrfCookie) postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusOK, postResp.StatusCode, "Full double-submit flow: GET→cookie→POST(header=cookie) should succeed") bodyData := parseJSONBody(postResp.Body) assert.Equal(s.T(), "ok", bodyData["status"], "POST handler should return status=ok when CSRF validated") } // Test that each GET request generates a NEW token (no caching/reuse of old tokens) func (s *CSRFE2ETestSuite) TestGET_GeneratesNewTokenEachRequest() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // First GET resp1, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer resp1.Body.Close() var cookie1 *http.Cookie for _, c := range resp1.Cookies() { if c.Name == "_gochat_csrf" { cookie1 = c break } } s.Require().NotNil(cookie1) // Second GET resp2, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer resp2.Body.Close() var cookie2 *http.Cookie for _, c := range resp2.Cookies() { if c.Name == "_gochat_csrf" { cookie2 = c break } } s.Require().NotNil(cookie2) // Tokens should differ (random generation) assert.NotEqual(s.T(), cookie1.Value, cookie2.Value, "Each GET request should generate a fresh CSRF token") } // Test that an old cookie token cannot be used after a new GET refreshes it func (s *CSRFE2ETestSuite) TestStaleToken_AfterRefresh_Returns403() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = false server := s.buildServer(cfg) defer server.Close() // Phase 1: Get first token resp1, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer resp1.Body.Close() var oldCookie *http.Cookie for _, c := range resp1.Cookies() { if c.Name == "_gochat_csrf" { oldCookie = c break } } s.Require().NotNil(oldCookie) oldToken := oldCookie.Value // Phase 2: Get a new token (refresh) resp2, err := http.Get(server.URL + "/api/test") s.Require().NoError(err) defer resp2.Body.Close() var newCookie *http.Cookie for _, c := range resp2.Cookies() { if c.Name == "_gochat_csrf" { newCookie = c break } } s.Require().NotNil(newCookie) // Phase 3: POST with old cookie + old header (both stale) // The server uses the cookie from the current request, so if we send // the old cookie value, it will still match the old header. But if // we send the new cookie + old header, it should fail. postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil) s.Require().NoError(err) postReq.AddCookie(newCookie) // New cookie postReq.Header.Set("X-CSRF-Token", oldToken) // Old header value postResp, err := http.DefaultClient.Do(postReq) s.Require().NoError(err) defer postResp.Body.Close() assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode, "POST with mismatched tokens (new cookie, old header) should be rejected") } // Test CookieSecure flag is set when configured func (s *CSRFE2ETestSuite) TestCookieSecureFlag() { cfg := middleware.DefaultCSRFConfig() cfg.Enabled = true cfg.CookieSecure = true // Secure flag enabled // Use httptest.Server directly on the router (no TLS) to check cookie flags r := gin.New() r.Use(gin.Recovery()) r.Use(middleware.CSRF(cfg)) r.GET("/api/test", func(c *gin.Context) { token, _ := c.Get("csrf_token") c.JSON(200, gin.H{"csrf_token": token}) }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/test", nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) // Parse Set-Cookie header to verify Secure flag setCookieHeader := w.Header().Get("Set-Cookie") assert.Contains(s.T(), setCookieHeader, "Secure", "Cookie should have Secure flag when CookieSecure=true") } // Test CookieHTTPOnly flag is set when configured func (s *CSRFE2ETestSuite) TestCookieHTTPOnlyFlag() { cfg := middleware.CSRFConfig{ Enabled: true, Secret: "test-httponly-secret", CookieName: "_gochat_csrf_httponly", HeaderName: "X-CSRF-Token", TokenLength: 32, SafeMethods: []string{"GET", "HEAD", "OPTIONS"}, CookieSecure: false, CookieHTTPOnly: true, // HttpOnly enabled (unusual for double-submit, but configurable) CookieSameSite: "Strict", CookiePath: "/", ExpirySeconds: 3600, } r := gin.New() r.Use(gin.Recovery()) r.Use(middleware.CSRF(cfg)) r.GET("/api/test", func(c *gin.Context) { token, _ := c.Get("csrf_token") c.JSON(200, gin.H{"csrf_token": token}) }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/test", nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) setCookieHeader := w.Header().Get("Set-Cookie") assert.Contains(s.T(), setCookieHeader, "HttpOnly", "Cookie should have HttpOnly flag when CookieHTTPOnly=true") } // ============================================================================ // Test Suite Runner // ============================================================================ func TestCSRFE2ETestSuite(t *testing.T) { suite.Run(t, new(CSRFE2ETestSuite)) }