veza/veza-backend-api/internal/integration/e2e_test.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

257 lines
8.4 KiB
Go

//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")
}