veza/veza-backend-api/tests/contract/critical_endpoints_test.go

481 lines
17 KiB
Go
Raw Normal View History

//go:build integration
// +build integration
package contract
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/api"
"veza-backend-api/internal/config"
"veza-backend-api/internal/core/marketplace"
"veza-backend-api/internal/database"
"veza-backend-api/internal/metrics"
"veza-backend-api/internal/middleware"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"github.com/stretchr/testify/require"
)
// testAuthMiddleware reads X-User-ID header for integration tests (marketplace routes)
type testAuthMiddleware struct{}
func (t *testAuthMiddleware) RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" {
if userID, err := uuid.Parse(userIDStr); err == nil {
c.Set("user_id", userID)
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
}
}
func (t *testAuthMiddleware) RequireContentCreatorRole() gin.HandlerFunc { return t.RequireAuth() }
func (t *testAuthMiddleware) RequireOwnershipOrAdmin(_ string, _ middleware.ResourceOwnerResolver) gin.HandlerFunc {
return t.RequireAuth()
}
// migrateContractDB runs AutoMigrate for all models needed by contract tests.
func migrateContractDB(t *testing.T, db *gorm.DB, extra ...interface{}) {
t.Helper()
allModels := []interface{}{
&models.User{},
&models.RefreshToken{},
&models.StorageQuota{},
&models.Track{},
&models.Session{},
&models.Notification{},
&models.Room{},
&models.RoomMember{},
&models.Message{},
&models.Playlist{},
&models.PlaylistTrack{},
&models.UserPresence{},
&models.Role{},
&models.UserRole{},
&models.Permission{},
&models.RolePermission{},
}
allModels = append(allModels, extra...)
require.NoError(t, db.AutoMigrate(allModels...))
// Add download_count to tracks if missing (analytics handler expects it)
_ = db.Exec("ALTER TABLE tracks ADD COLUMN download_count INTEGER DEFAULT 0").Error
}
func setupContractRouter(t *testing.T, db *gorm.DB, marketSvc *marketplace.Service) *gin.Engine {
t.Helper()
os.Setenv("ENABLE_CLAMAV", "false")
os.Setenv("CLAMAV_REQUIRED", "false")
gin.SetMode(gin.TestMode)
router := gin.New()
sqlDB, err := db.DB()
require.NoError(t, err)
vezaDB := &database.Database{DB: sqlDB, GormDB: db, Logger: zap.NewNop()}
cfg := &config.Config{
HyperswitchWebhookSecret: "test-secret",
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
AuthRateLimitLoginAttempts: 100,
AuthRateLimitLoginWindow: 15,
MarketplaceServiceOverride: marketSvc,
}
require.NoError(t, cfg.InitServicesForTest())
require.NoError(t, cfg.InitMiddlewaresForTest())
cfg.AuthMiddlewareOverride = &testAuthMiddleware{}
apiRouter := api.NewAPIRouter(vezaDB, cfg)
require.NoError(t, apiRouter.Setup(router))
return router
}
func openContractDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1) // SQLite :memory: - each new connection gets empty DB
return db
}
func TestContract_Login(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
// No user - login fails with 401
router := setupContractRouter(t, db, nil)
body, _ := json.Marshal(map[string]string{"email": "nobody@test.com", "password": "wrong"})
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.StatusUnauthorized, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), false)
require.NotNil(t, resp["error"])
}
func TestContract_Register(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
router := setupContractRouter(t, db, nil)
body, _ := json.Marshal(map[string]interface{}{
"email": "contract-" + uuid.New().String() + "@test.com",
"username": "contractuser",
"password": "ValidPassword123!",
"password_confirmation": "ValidPassword123!",
})
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.True(t, w.Code == http.StatusCreated || w.Code == http.StatusBadRequest, "register: %s", w.Body.String())
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), w.Code == http.StatusCreated)
if w.Code == http.StatusCreated {
data, ok := resp["data"].(map[string]interface{})
require.True(t, ok)
RequireDataKeys(t, data, "user", "token")
}
}
func TestContract_GetTracks(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
router := setupContractRouter(t, db, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tracks", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
data, ok := resp["data"].(map[string]interface{})
require.True(t, ok)
// Tracks may be in "tracks" or "items" depending on handler
require.True(t, data["tracks"] != nil || data["items"] != nil || data["data"] != nil,
"data should have tracks/items: %v", data)
}
func TestContract_GetUser(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
userID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{
ID: userID,
Email: "me@test.com",
Username: "meuser",
PasswordHash: string(hash),
IsVerified: true,
}).Error)
router := setupContractRouter(t, db, nil)
// Login to get token
loginBody, _ := json.Marshal(map[string]string{"email": "me@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
data := loginResp["data"].(map[string]interface{})
tokenData := data["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
// GET /auth/me
req = httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
userData, ok := resp["data"].(map[string]interface{})
require.True(t, ok)
RequireDataKeys(t, userData, "id", "email", "username")
}
func TestContract_CreateTrack(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
userID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{
ID: userID,
Email: "creator@test.com",
Username: "creator",
PasswordHash: string(hash),
IsVerified: true,
Role: "content_creator",
}).Error)
// RequireContentCreatorRole checks user_roles; create creator role and assign
creatorRole := models.Role{ID: uuid.New(), Name: "creator", DisplayName: "Creator", IsSystem: true}
require.NoError(t, db.Create(&creatorRole).Error)
require.NoError(t, db.Create(&models.UserRole{
UserID: userID, RoleID: creatorRole.ID, RoleName: "creator", IsActive: true,
}).Error)
router := setupContractRouter(t, db, nil)
// Login
loginBody, _ := json.Marshal(map[string]string{"email": "creator@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
tokenData := loginResp["data"].(map[string]interface{})["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
// POST /tracks without file -> expect 400 (validation)
req = httptest.NewRequest(http.MethodPost, "/api/v1/tracks", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusUnsupportedMediaType)
ValidateAPIResponseEnvelope(t, w.Body.Bytes(), false)
}
func TestContract_Search(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
router := setupContractRouter(t, db, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/search?q=test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Search uses PostgreSQL-specific SQL (ILIKE); may return 500 on SQLite
require.True(t, w.Code == http.StatusOK || w.Code == http.StatusInternalServerError,
"expected 200 or 500, got %d: %s", w.Code, w.Body.String())
if w.Code == http.StatusOK {
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
require.Contains(t, resp, "data")
}
// On 500, Search returns raw gin.H{"error": "..."} - no standard envelope
}
func TestContract_CreateOrder(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db,
&marketplace.Product{}, &marketplace.Order{}, &marketplace.OrderItem{}, &marketplace.License{}, &marketplace.SellerTransfer{},
)
userID := uuid.New()
trackID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{ID: userID, Email: "buyer@test.com", Username: "buyer", PasswordHash: string(hash), IsVerified: true}).Error)
require.NoError(t, db.Create(&models.Track{ID: trackID, UserID: userID, FilePath: "/test.mp3"}).Error)
product := &marketplace.Product{
ID: uuid.New(), SellerID: userID, Title: "Test", Price: 9.99, ProductType: "track",
TrackID: &trackID, Status: marketplace.ProductStatusActive,
}
require.NoError(t, db.Create(product).Error)
storageService := services.NewTrackStorageService("uploads/test", false, zap.NewNop())
marketSvc := marketplace.NewService(db, zap.NewNop(), storageService,
marketplace.WithPaymentProvider(&mockPaymentProvider{paymentID: "pay_1", clientSecret: "sec"}),
marketplace.WithHyperswitchConfig(true, "/"),
)
router := setupContractRouter(t, db, marketSvc)
// Login
loginBody, _ := json.Marshal(map[string]string{"email": "buyer@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
tokenData := loginResp["data"].(map[string]interface{})["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
// Create order via marketplace (uses AuthMiddlewareOverride -> X-User-ID)
orderBody, _ := json.Marshal(map[string]interface{}{
"items": []map[string]string{{"product_id": product.ID.String()}},
})
req = httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewReader(orderBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-ID", userID.String())
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.True(t, w.Code == http.StatusCreated || w.Code == http.StatusOK || w.Code >= 400)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), w.Code < 400)
if w.Code < 400 {
data, _ := resp["data"].(map[string]interface{})
if data != nil {
require.True(t, data["order"] != nil || data["payment_id"] != nil || data["id"] != nil)
}
}
}
type mockPaymentProvider struct {
paymentID string
clientSecret string
}
func (m *mockPaymentProvider) CreatePayment(_ context.Context, _ int64, _, _, _ string, _ map[string]string) (string, string, error) {
return m.paymentID, m.clientSecret, nil
}
func (m *mockPaymentProvider) GetPayment(_ context.Context, _ string) (string, error) {
return "succeeded", nil
}
func TestContract_GetNotifications(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
userID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{
ID: userID, Email: "notif@test.com", Username: "notifuser",
PasswordHash: string(hash), IsVerified: true,
}).Error)
router := setupContractRouter(t, db, nil)
loginBody, _ := json.Marshal(map[string]string{"email": "notif@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
tokenData := loginResp["data"].(map[string]interface{})["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
req = httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
require.Contains(t, resp, "data")
}
func TestContract_GetConversations(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
userID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{
ID: userID, Email: "conv@test.com", Username: "convuser",
PasswordHash: string(hash), IsVerified: true,
}).Error)
router := setupContractRouter(t, db, nil)
loginBody, _ := json.Marshal(map[string]string{"email": "conv@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
tokenData := loginResp["data"].(map[string]interface{})["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
req = httptest.NewRequest(http.MethodGet, "/api/v1/conversations", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
require.Contains(t, resp, "data")
}
func TestContract_GetAnalytics(t *testing.T) {
db := openContractDB(t)
migrateContractDB(t, db)
userID := uuid.New()
hash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
require.NoError(t, db.Create(&models.User{
ID: userID, Email: "analytics@test.com", Username: "analyticsuser",
PasswordHash: string(hash), IsVerified: true,
}).Error)
router := setupContractRouter(t, db, nil)
loginBody, _ := json.Marshal(map[string]string{"email": "analytics@test.com", "password": "password123"})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var loginResp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &loginResp))
tokenData := loginResp["data"].(map[string]interface{})["token"].(map[string]interface{})
accessToken := tokenData["access_token"].(string)
req = httptest.NewRequest(http.MethodGet, "/api/v1/analytics", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
resp := ValidateAPIResponseEnvelope(t, w.Body.Bytes(), true)
require.Contains(t, resp, "data")
}