480 lines
17 KiB
Go
480 lines
17 KiB
Go
//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")
|
|
}
|