- INT-01: Add E2E streaming tests (upload -> HLS auth) - INT-02: Add E2E cloud tests (CRUD auth, public gear) - INT-03: Split track/handler.go into 4 focused sub-handlers - INT-04: Create migration squash script + MIGRATIONS.md - INT-05: Add Trivy container image scanning CI workflow - INT-06: Replace production console.log with structured logger
257 lines
8.4 KiB
Go
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")
|
|
}
|