package tests // integration_http_test.go contains tests for backend behavior. import ( "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" gconfig "knowfoolery/backend/services/gateway-service/internal/infra/config" "knowfoolery/backend/services/gateway-service/internal/infra/proxy" "knowfoolery/backend/services/gateway-service/internal/infra/routing" httpapi "knowfoolery/backend/services/gateway-service/internal/interfaces/http" "knowfoolery/backend/services/gateway-service/internal/interfaces/http/middleware" "knowfoolery/backend/shared/infra/auth/zitadel" ) // TestGateway_PublicRoute_ProxiesAndRewritesPath verifies expected behavior. func TestGateway_PublicRoute_ProxiesAndRewritesPath(t *testing.T) { t.Parallel() var receivedPath string var receivedQuery string client := &http.Client{Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { receivedPath = r.URL.Path receivedQuery = r.URL.RawQuery return jsonResponse(http.StatusOK, `{"ok":true}`), nil })} app := buildTestApp(t, "http://upstream.local", client) req := httptest.NewRequest(http.MethodGet, "/api/v1/leaderboard/top10?window=7d", nil) req.Header.Set("Origin", "http://localhost:5173") res, err := app.Test(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, "/leaderboard/top10", receivedPath) require.Equal(t, "window=7d", receivedQuery) require.Equal(t, "http://localhost:5173", res.Header.Get("Access-Control-Allow-Origin")) require.Equal(t, "DENY", res.Header.Get("X-Frame-Options")) require.Equal(t, "degraded", res.Header.Get("X-RateLimit-Policy")) } // TestGateway_ProtectedRoute_RequiresAuth verifies expected behavior. func TestGateway_ProtectedRoute_RequiresAuth(t *testing.T) { t.Parallel() client := &http.Client{Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return jsonResponse(http.StatusOK, `{"ok":true}`), nil })} app := buildTestApp(t, "http://upstream.local", client) req := httptest.NewRequest(http.MethodGet, "/api/v1/leaderboard/players/player-1", nil) res, err := app.Test(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusUnauthorized, res.StatusCode) } // TestGateway_ProtectedRoute_ForwardsUserHeaders verifies expected behavior. func TestGateway_ProtectedRoute_ForwardsUserHeaders(t *testing.T) { t.Parallel() received := make(map[string]string) client := &http.Client{Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { received["x-user-id"] = r.Header.Get("X-User-ID") received["x-user-email"] = r.Header.Get("X-User-Email") received["x-user-roles"] = r.Header.Get("X-User-Roles") received["x-user-mfa"] = r.Header.Get("X-User-MFA-Verified") return jsonResponse(http.StatusOK, `{"ok":true}`), nil })} app := buildTestApp(t, "http://upstream.local", client) req := httptest.NewRequest(http.MethodGet, "/api/v1/leaderboard/players/player-1", nil) req.Header.Set("Authorization", "Bearer test-token") res, err := app.Test(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, "user-123", received["x-user-id"]) require.Equal(t, "player@example.com", received["x-user-email"]) require.Equal(t, "player", received["x-user-roles"]) require.Equal(t, "true", received["x-user-mfa"]) } // TestGateway_PreflightCors verifies expected behavior. func TestGateway_PreflightCors(t *testing.T) { t.Parallel() client := &http.Client{Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { return jsonResponse(http.StatusOK, `{"ok":true}`), nil })} app := buildTestApp(t, "http://upstream.local", client) req := httptest.NewRequest(http.MethodOptions, "/api/v1/sessions/start", nil) req.Header.Set("Origin", "http://localhost:5173") req.Header.Set("Access-Control-Request-Method", "POST") res, err := app.Test(req) require.NoError(t, err) defer res.Body.Close() require.Equal(t, http.StatusNoContent, res.StatusCode) require.Equal(t, "http://localhost:5173", res.Header.Get("Access-Control-Allow-Origin")) } // buildTestApp is a test helper. func buildTestApp(t *testing.T, upstreamURL string, client *http.Client) *fiber.App { t.Helper() app := fiber.New() app.Use(middleware.RequestContext(nil)) app.Use(middleware.SecurityHeaders(gconfig.SecurityHeadersConfig{ ContentSecurityPolicy: "default-src 'self'", EnableHSTS: false, HSTSMaxAge: 31536000, FrameOptions: "DENY", ContentTypeOptions: true, ReferrerPolicy: "strict-origin-when-cross-origin", PermissionsPolicy: "geolocation=()", })) app.Use(middleware.CORS(gconfig.CORSConfig{ AllowedOrigins: []string{"http://localhost:5173"}, AllowedMethods: "GET,POST,PUT,DELETE,OPTIONS", AllowedHeaders: "Origin,Content-Type,Accept,Authorization", AllowCredentials: true, MaxAgeSeconds: 300, })) authMiddleware := func(c fiber.Ctx) error { path := c.Path() public := []string{ "/api/v1/questions", "/api/v1/leaderboard/top10", "/api/v1/leaderboard/stats", "/api/v1/admin/auth", "/api/v1/users/register", "/api/v1/users/verify-email", } for _, p := range public { if strings.HasPrefix(path, p) { return c.Next() } } if c.Get("Authorization") == "" { return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": "Authorization header required"}) } c.Locals(string(zitadel.ContextKeyUserID), "user-123") c.Locals(string(zitadel.ContextKeyUserEmail), "player@example.com") c.Locals(string(zitadel.ContextKeyUserRoles), []string{"player"}) c.Locals(string(zitadel.ContextKeyMFAVerified), true) return c.Next() } httpapi.RegisterRoutes(app, httpapi.Options{ PublicPrefix: "/api/v1", Upstreams: routing.Upstreams{ GameSession: upstreamURL, QuestionBank: upstreamURL, User: upstreamURL, Leaderboard: upstreamURL, Admin: upstreamURL, }, Proxy: proxy.NewWithClient(client, nil), AuthMiddleware: authMiddleware, RateLimitMiddleware: middleware.RateLimitMiddleware(nil, gconfig.RateLimitConfig{ GeneralRequests: 100, AuthRequests: 5, APIRequests: 60, AdminRequests: 30, Window: time.Minute}, "/api/v1", nil), }) return app } type roundTripperFunc func(*http.Request) (*http.Response, error) // RoundTrip is a test helper. func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return fn(req) } // jsonResponse is a test helper. func jsonResponse(status int, body string) *http.Response { return &http.Response{ StatusCode: status, Header: http.Header{ "Content-Type": []string{"application/json"}, }, Body: io.NopCloser(strings.NewReader(body)), } }