//go:build integration // +build integration package integration import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "time" "veza-backend-api/internal/api" "veza-backend-api/internal/config" "veza-backend-api/internal/database" "veza-backend-api/internal/metrics" "veza-backend-api/internal/models" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // setupE2ETestRouter creates a test router with full handler chain for E2E integration tests. func setupE2ETestRouter(t *testing.T) (*gin.Engine, func()) { t.Helper() // Disable ClamAV for tests (avoids exec/connection failures) os.Setenv("ENABLE_CLAMAV", "false") os.Setenv("CLAMAV_REQUIRED", "false") gin.SetMode(gin.TestMode) router := gin.New() logger := zaptest.NewLogger(t) mockGormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) mockGormDB.Exec("PRAGMA foreign_keys = ON") // Auto-migrate models needed for auth, webhooks, and gear require.NoError(t, mockGormDB.AutoMigrate( &models.User{}, &models.RefreshToken{}, &models.Session{}, &models.Role{}, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, &models.Webhook{}, &models.GearItem{}, )) require.NoError(t, mockGormDB.Exec(` CREATE TABLE IF NOT EXISTS email_verification_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL, email TEXT NOT NULL, verified INTEGER NOT NULL DEFAULT 0, used INTEGER NOT NULL DEFAULT 0, verified_at TIMESTAMP, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) `).Error) mockDB := &database.Database{ GormDB: mockGormDB, Logger: logger, } mockConfig := &config.Config{ AppPort: 8080, CORSOrigins: []string{"*"}, Env: "development", JWTSecret: "test-jwt-secret-key-minimum-32-characters-long", JWTIssuer: "veza-api", JWTAudience: "veza-app", UploadDir: "uploads/test", StreamServerURL: "http://localhost:8000", StreamServerInternalAPIKey: "test-internal-api-key", Database: mockDB, Logger: logger, RedisClient: nil, RabbitMQEventBus: nil, ErrorMetrics: metrics.NewErrorMetrics(), HandlerTimeout: 30 * time.Second, RateLimitLimit: 100, RateLimitWindow: 60, AuthRateLimitLoginAttempts: 10, AuthRateLimitLoginWindow: 15, } // Initialize services (required for AuthMiddleware) require.NoError(t, mockConfig.InitServicesForTest()) // Initialize middlewares (AuthMiddleware, EndpointLimiter, SimpleRateLimiter) require.NoError(t, mockConfig.InitMiddlewaresForTest()) apiRouter := api.NewAPIRouter(mockDB, mockConfig) err = apiRouter.Setup(router) require.NoError(t, err) return router, func() {} } // TestHealthEndpoint verifies GET /api/v1/health returns 200. func TestHealthEndpoint(t *testing.T) { router, cleanup := setupE2ETestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // TestAuthFlow_RegisterLoginProfile tests the auth flow: register -> login -> profile. func TestAuthFlow_RegisterLoginProfile(t *testing.T) { router, cleanup := setupE2ETestRouter(t) defer cleanup() // 1. Register registerBody := map[string]string{ "email": "e2e-test@example.com", "password": "SecureP@ssw0rd!", "password_confirmation": "SecureP@ssw0rd!", "username": "e2etestuser", } body, _ := json.Marshal(registerBody) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "register must succeed: %s", w.Body.String()) // 2. Login loginBody := map[string]string{ "email": "e2e-test@example.com", "password": "SecureP@ssw0rd!", } body, _ = json.Marshal(loginBody) req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "login should succeed: %s", w.Body.String()) // Extract access_token from response var loginResp struct { Data struct { AccessToken string `json:"access_token"` } `json:"data"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp)) token := loginResp.Data.AccessToken require.NotEmpty(t, token, "access_token required for profile request") // 3. Get profile (auth/me) req = httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "profile should succeed with valid token: %s", w.Body.String()) } // TestTrackUpload_RequiresAuth verifies POST /api/v1/tracks without auth returns 401. func TestTrackUpload_RequiresAuth(t *testing.T) { router, cleanup := setupE2ETestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", bytes.NewReader([]byte(`{"title":"test"}`))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // TestWebhookRegistration_RequiresHTTPS verifies POST /api/v1/webhooks with http:// URL returns 400. func TestWebhookRegistration_RequiresHTTPS(t *testing.T) { router, cleanup := setupE2ETestRouter(t) defer cleanup() // Register and login to get a valid token registerBody := map[string]string{ "email": "webhook-test@example.com", "password": "SecureP@ssw0rd!", "password_confirmation": "SecureP@ssw0rd!", "username": "webhooktestuser", } body, _ := json.Marshal(registerBody) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "register must succeed for webhook test: %s", w.Body.String()) loginBody := map[string]string{ "email": "webhook-test@example.com", "password": "SecureP@ssw0rd!", } body, _ = json.Marshal(loginBody) req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, "login required for webhook test: %s", w.Body.String()) var loginResp struct { Data struct { AccessToken string `json:"access_token"` } `json:"data"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp)) token := loginResp.Data.AccessToken require.NotEmpty(t, token) // POST webhook with http:// URL - must return 400 webhookBody := map[string]interface{}{ "url": "http://example.com/webhook", "events": []string{"track.uploaded"}, } body, _ = json.Marshal(webhookBody) req = httptest.NewRequest(http.MethodPost, "/api/v1/webhooks", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code, "http:// URL must be rejected with 400: %s", w.Body.String()) } // TestRateLimitHeaders verifies requests to non-excluded endpoints return X-RateLimit-Limit and X-RateLimit-Remaining headers. // Health is excluded from rate limiting; use GET /api/v1/tracks which is rate-limited. func TestRateLimitHeaders(t *testing.T) { router, cleanup := setupE2ETestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/api/v1/tracks", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // 200 (empty list) or 401 depending on route config; rate limit headers must be present assert.NotEmpty(t, w.Header().Get("X-RateLimit-Limit"), "X-RateLimit-Limit header must be present") assert.NotEmpty(t, w.Header().Get("X-RateLimit-Remaining"), "X-RateLimit-Remaining header must be present") }