You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
6.7 KiB
Go
204 lines
6.7 KiB
Go
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)),
|
|
}
|
|
}
|