diff --git a/api/middleware.go b/api/middleware.go index 3bdeac3..4889912 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -9,7 +9,6 @@ import "github.com/gin-gonic/gin" func SetupCORS() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, "+ "X-CSRF-Token, Authorization, accept, origin, "+ diff --git a/api/middleware_test.go b/api/middleware_test.go new file mode 100644 index 0000000..612f1f2 --- /dev/null +++ b/api/middleware_test.go @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package api + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertCORSHeaders(t *testing.T, headers http.Header) { + t.Helper() + + assert.Equal(t, "*", headers.Get("Access-Control-Allow-Origin")) + assert.Empty(t, headers.Get("Access-Control-Allow-Credentials"), + "Allow-Credentials must not be set alongside a wildcard Allow-Origin") + assert.Equal( + t, + "Content-Type, Content-Length, Accept-Encoding, "+ + "X-CSRF-Token, Authorization, accept, origin, "+ + "Cache-Control, X-Requested-With", + headers.Get("Access-Control-Allow-Headers"), + ) + assert.Equal(t, "POST, OPTIONS, GET, PUT, DELETE", headers.Get("Access-Control-Allow-Methods")) +} + +func setupTestRouter(t *testing.T) *gin.Engine { + t.Helper() + + previousMode := gin.Mode() + gin.SetMode(gin.TestMode) + t.Cleanup(func() { gin.SetMode(previousMode) }) + + router := gin.New() + router.Use(SetupCORS()) + return router +} + +func TestSetupCORS_NormalGET(t *testing.T) { + router := setupTestRouter(t) + + handlerReached := false + router.GET("/test", func(c *gin.Context) { + handlerReached = true + c.String(http.StatusOK, "ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + assert.True(t, handlerReached, "handler should be reached for normal GET") + assert.Equal(t, "ok", rec.Body.String()) + assertCORSHeaders(t, rec.Header()) +} + +func TestSetupCORS_OPTIONS(t *testing.T) { + router := setupTestRouter(t) + + handlerReached := false + router.OPTIONS("/test", func(c *gin.Context) { + handlerReached = true + c.String(http.StatusOK, "should not be reached") + }) + + req := httptest.NewRequest(http.MethodOptions, "/test", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusNoContent, rec.Code) + assert.False(t, handlerReached, "handler must not be reached on OPTIONS preflight") + assert.Empty(t, rec.Body.String()) + assertCORSHeaders(t, rec.Header()) +} + +func TestSetupCORS_ErrorResponses(t *testing.T) { + tests := []struct { + name string + statusCode int + handler gin.HandlerFunc + }{ + { + name: "400 Bad Request with AbortWithStatusJSON", + statusCode: http.StatusBadRequest, + handler: func(c *gin.Context) { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + }, + }, + { + name: "403 Forbidden with AbortWithStatus", + statusCode: http.StatusForbidden, + handler: func(c *gin.Context) { + c.AbortWithStatus(http.StatusForbidden) + }, + }, + { + name: "404 Not Found from handler", + statusCode: http.StatusNotFound, + handler: func(c *gin.Context) { + c.String(http.StatusNotFound, "not found") + }, + }, + { + name: "500 Internal Server Error with AbortWithStatus", + statusCode: http.StatusInternalServerError, + handler: func(c *gin.Context) { + c.AbortWithStatus(http.StatusInternalServerError) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + router := setupTestRouter(t) + router.GET("/error", tt.handler) + + req := httptest.NewRequest(http.MethodGet, "/error", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, tt.statusCode, rec.Code) + assertCORSHeaders(t, rec.Header()) + }) + } +} + +func TestSetupCORS_UnroutedRoute(t *testing.T) { + router := setupTestRouter(t) + + req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusNotFound, rec.Code) + assertCORSHeaders(t, rec.Header()) +} + +func TestSetupCORS_OtherMethods(t *testing.T) { + methods := []string{http.MethodPost, http.MethodPut, http.MethodDelete} + + for _, method := range methods { + t.Run(method, func(t *testing.T) { + router := setupTestRouter(t) + router.Handle(method, "/test", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(method, "/test", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + assertCORSHeaders(t, rec.Header()) + }) + } +} diff --git a/go.mod b/go.mod index cc48c54..89c1421 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-sql-driver/mysql v1.9.3 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.5 @@ -23,6 +24,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect @@ -51,6 +53,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/spf13/afero v1.9.5 // indirect