feat(security): v0.901 Ironclad - fix 5 critical/high vulnerabilities
- OAuth: use JWTService+SessionService, httpOnly cookies (VEZA-SEC-001) - Remove PasswordService.GenerateJWT (VEZA-SEC-002) - Hyperswitch webhook: mandatory verification, 500 if secret empty (VEZA-SEC-005) - Auth middleware: TokenBlacklist.IsBlacklisted check (VEZA-SEC-006) - Waveform: ValidateExecPath before exec (VEZA-SEC-007)
This commit is contained in:
parent
5063c95a5c
commit
51984e9a1f
19 changed files with 397 additions and 118 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.101.0
|
||||
0.901
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ func (r *APIRouter) setupAuthRoutes(router *gin.RouterGroup) error {
|
|||
checkUsernameGroup.GET("/check-username", handlers.CheckUsername(authService))
|
||||
|
||||
// BE-API-042: OAuth routes
|
||||
jwtSecretBytes := []byte(r.config.JWTSecret)
|
||||
oauthService := services.NewOAuthService(r.db, r.logger, jwtSecretBytes)
|
||||
oauthService := services.NewOAuthService(r.db, r.logger, jwtService, sessionService, userService)
|
||||
baseURL := os.Getenv("BASE_URL")
|
||||
if baseURL == "" {
|
||||
appDomain := os.Getenv("APP_DOMAIN")
|
||||
|
|
@ -142,7 +141,7 @@ func (r *APIRouter) setupAuthRoutes(router *gin.RouterGroup) error {
|
|||
oauthService.InitializeConfigs(googleClientID, googleClientSecret, githubClientID, githubClientSecret, discordClientID, discordClientSecret, spotifyClientID, spotifyClientSecret, baseURL)
|
||||
}
|
||||
|
||||
oauthHandler := handlers.NewOAuthHandler(oauthService, r.logger, r.config.CORSOrigins, r.config.FrontendURL)
|
||||
oauthHandler := handlers.NewOAuthHandler(oauthService, r.logger, r.config.CORSOrigins, r.config.FrontendURL, r.config)
|
||||
oauthGroup := authGroup.Group("/oauth")
|
||||
{
|
||||
oauthGroup.GET("/providers", oauthHandler.GetOAuthProviders)
|
||||
|
|
|
|||
|
|
@ -62,15 +62,16 @@ func (r *APIRouter) hyperswitchWebhookHandler() gin.HandlerFunc {
|
|||
response.InternalServerError(c, "Failed to read webhook body")
|
||||
return
|
||||
}
|
||||
if webhookSecret != "" {
|
||||
sig := c.GetHeader("x-webhook-signature-512")
|
||||
if err := hyperswitch.VerifyWebhookSignature(body, sig, webhookSecret); err != nil {
|
||||
r.logger.Warn("Hyperswitch webhook: signature verification failed", zap.Error(err))
|
||||
response.Unauthorized(c, "Invalid webhook signature")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
r.logger.Warn("Hyperswitch webhook: HYPERSWITCH_WEBHOOK_SECRET not set, skipping signature verification")
|
||||
if webhookSecret == "" {
|
||||
r.logger.Error("Hyperswitch webhook: HYPERSWITCH_WEBHOOK_SECRET not configured, rejecting webhook")
|
||||
response.InternalServerError(c, "Webhook secret not configured")
|
||||
return
|
||||
}
|
||||
sig := c.GetHeader("x-webhook-signature-512")
|
||||
if err := hyperswitch.VerifyWebhookSignature(body, sig, webhookSecret); err != nil {
|
||||
r.logger.Warn("Hyperswitch webhook: signature verification failed", zap.Error(err))
|
||||
response.Unauthorized(c, "Invalid webhook signature")
|
||||
return
|
||||
}
|
||||
if err := marketService.ProcessPaymentWebhook(c.Request.Context(), body); err != nil {
|
||||
r.logger.Error("Hyperswitch webhook: processing failed", zap.Error(err))
|
||||
|
|
|
|||
92
veza-backend-api/internal/api/routes_webhooks_test.go
Normal file
92
veza-backend-api/internal/api/routes_webhooks_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/config"
|
||||
"veza-backend-api/internal/database"
|
||||
"veza-backend-api/internal/metrics"
|
||||
)
|
||||
|
||||
func setupWebhookTestRouter(t *testing.T, webhookSecret string) *gin.Engine {
|
||||
t.Helper()
|
||||
os.Setenv("ENABLE_CLAMAV", "false")
|
||||
os.Setenv("CLAMAV_REQUIRED", "false")
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
|
||||
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
sqlDB, err := gormDB.DB()
|
||||
require.NoError(t, err)
|
||||
|
||||
vezaDB := &database.Database{
|
||||
DB: sqlDB,
|
||||
GormDB: gormDB,
|
||||
Logger: zap.NewNop(),
|
||||
}
|
||||
|
||||
cfg := &config.Config{
|
||||
HyperswitchWebhookSecret: webhookSecret,
|
||||
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,
|
||||
RateLimitWindow: 60,
|
||||
AuthRateLimitLoginAttempts: 10,
|
||||
AuthRateLimitLoginWindow: 15,
|
||||
}
|
||||
require.NoError(t, cfg.InitServicesForTest())
|
||||
require.NoError(t, cfg.InitMiddlewaresForTest())
|
||||
|
||||
apiRouter := NewAPIRouter(vezaDB, cfg)
|
||||
require.NoError(t, apiRouter.Setup(router))
|
||||
return router
|
||||
}
|
||||
|
||||
// TestHyperswitchWebhook_EmptySecret_Returns500 verifies that when HyperswitchWebhookSecret
|
||||
// is empty, the webhook handler returns 500 (VEZA-SEC-005).
|
||||
func TestHyperswitchWebhook_EmptySecret_Returns500(t *testing.T) {
|
||||
router := setupWebhookTestRouter(t, "")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader([]byte(`{"test": true}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code, "empty webhook secret must return 500")
|
||||
}
|
||||
|
||||
// TestHyperswitchWebhook_ValidSecret_InvalidSignature_Returns401 verifies that with a configured secret,
|
||||
// invalid signature returns 401 (smoke test that verification path is exercised).
|
||||
func TestHyperswitchWebhook_ValidSecret_InvalidSignature_Returns401(t *testing.T) {
|
||||
router := setupWebhookTestRouter(t, "test-secret-at-least-32-chars-long")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader([]byte(`{"test": true}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code, "invalid signature must return 401")
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ type Config struct {
|
|||
S3StorageService *services.S3StorageService // BE-SVC-005: S3 storage service
|
||||
APIKeyService *services.APIKeyService // v0.102 Lot C: developer API keys
|
||||
PresenceService *services.PresenceService // v0.301 Lot P1: user presence (online/away/offline)
|
||||
TokenBlacklist *services.TokenBlacklist // VEZA-SEC-006: token revocation (nil if Redis unavailable)
|
||||
|
||||
// Middlewares
|
||||
RateLimiter *middleware.RateLimiter
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ func (c *Config) initMiddlewares() error {
|
|||
c.JWTService,
|
||||
c.UserService,
|
||||
c.APIKeyService,
|
||||
c.TokenBlacklist, // VEZA-SEC-006: nil if Redis unavailable (implements TokenBlacklistChecker)
|
||||
c.Logger,
|
||||
)
|
||||
if c.PresenceService != nil {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ func (c *Config) initServices() error {
|
|||
// Service de cache (only when Redis is available; nil client causes panics)
|
||||
if c.RedisClient != nil {
|
||||
c.CacheService = services.NewCacheService(c.RedisClient, c.Logger)
|
||||
c.TokenBlacklist = services.NewTokenBlacklist(c.RedisClient) // VEZA-SEC-006
|
||||
}
|
||||
|
||||
// Service de playlist
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"veza-backend-api/internal/config"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -14,16 +18,17 @@ import (
|
|||
// OAuthServiceInterface defines the methods needed for OAuth handlers
|
||||
type OAuthServiceInterface interface {
|
||||
GetAuthURL(provider string) (string, error)
|
||||
HandleCallback(provider, code, state string) (*services.OAuthUser, string, error)
|
||||
HandleCallback(ctx context.Context, provider, code, state, ipAddress, userAgent string) (*services.OAuthUser, *models.TokenPair, error)
|
||||
GetAvailableProviders() []string
|
||||
}
|
||||
|
||||
// OAuthHandlers handles OAuth authentication flows
|
||||
type OAuthHandlers struct {
|
||||
oauthService OAuthServiceInterface
|
||||
logger interface{}
|
||||
logger interface{}
|
||||
allowedRedirectOrigins []string // SECURITY: allowlist for OAuth redirect URLs
|
||||
frontendURL string // URL du frontend pour redirect OAuth (depuis config)
|
||||
frontendURL string // URL du frontend pour redirect OAuth (depuis config)
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// OAuthHandlersInstance is the global instance
|
||||
|
|
@ -39,22 +44,24 @@ func InitOAuthHandlers(oauthService *services.OAuthService) {
|
|||
// NewOAuthHandler creates a new OAuth handler instance
|
||||
// BE-API-042: Implement OAuth callback endpoint
|
||||
// frontendURL: from config.FrontendURL (FRONTEND_URL or VITE_FRONTEND_URL env)
|
||||
func NewOAuthHandler(oauthService *services.OAuthService, logger interface{}, allowedRedirectOrigins []string, frontendURL string) *OAuthHandlers {
|
||||
func NewOAuthHandler(oauthService *services.OAuthService, logger interface{}, allowedRedirectOrigins []string, frontendURL string, cfg *config.Config) *OAuthHandlers {
|
||||
return &OAuthHandlers{
|
||||
oauthService: oauthService,
|
||||
logger: logger,
|
||||
allowedRedirectOrigins: allowedRedirectOrigins,
|
||||
frontendURL: frontendURL,
|
||||
frontendURL: frontendURL,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOAuthHandlerWithInterface creates a new OAuth handler instance with an interface (for testing)
|
||||
func NewOAuthHandlerWithInterface(oauthService OAuthServiceInterface, logger interface{}) *OAuthHandlers {
|
||||
func NewOAuthHandlerWithInterface(oauthService OAuthServiceInterface, logger interface{}, cfg *config.Config) *OAuthHandlers {
|
||||
return &OAuthHandlers{
|
||||
oauthService: oauthService,
|
||||
logger: logger,
|
||||
allowedRedirectOrigins: nil, // Tests use nil = dev fallback
|
||||
logger: logger,
|
||||
allowedRedirectOrigins: nil, // Tests use nil = dev fallback
|
||||
frontendURL: "http://localhost:5173", // Tests use localhost
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,22 +134,54 @@ func (oh *OAuthHandlers) OAuthCallback(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Handle callback
|
||||
user, token, err := oh.oauthService.HandleCallback(provider, code, state)
|
||||
// Handle callback (VEZA-SEC-001: returns TokenPair, creates session)
|
||||
user, tokens, err := oh.oauthService.HandleCallback(c.Request.Context(), provider, code, state, c.ClientIP(), c.Request.UserAgent())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to frontend with token (frontendURL from config)
|
||||
frontendURL := oh.frontendURL
|
||||
// SECURITY: Validate redirect URL against allowlist to prevent open redirect
|
||||
frontendURL := oh.frontendURL
|
||||
if !oh.isAllowedRedirectOrigin(frontendURL) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid redirect configuration"})
|
||||
return
|
||||
}
|
||||
redirectURL := fmt.Sprintf("%s/auth/callback?token=%s&user_id=%s", strings.TrimSuffix(frontendURL, "/"), token, user.ID.String())
|
||||
|
||||
// VEZA-SEC-001: Set httpOnly cookies (same as login flow)
|
||||
if oh.cfg != nil {
|
||||
refreshTokenExpires := 14 * 24 * time.Hour
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: tokens.RefreshToken,
|
||||
Path: oh.cfg.CookiePath,
|
||||
Domain: oh.cfg.CookieDomain,
|
||||
MaxAge: int(refreshTokenExpires.Seconds()),
|
||||
HttpOnly: oh.cfg.CookieHttpOnly,
|
||||
Secure: oh.cfg.ShouldUseSecureCookies(),
|
||||
SameSite: oh.cfg.GetCookieSameSite(),
|
||||
}
|
||||
http.SetCookie(c.Writer, refreshTokenCookie)
|
||||
|
||||
accessTokenExpires := 5 * time.Minute // Match JWTService default
|
||||
if oh.cfg.JWTService != nil {
|
||||
accessTokenExpires = oh.cfg.JWTService.GetConfig().AccessTokenTTL
|
||||
}
|
||||
accessTokenCookie := &http.Cookie{
|
||||
Name: "access_token",
|
||||
Value: tokens.AccessToken,
|
||||
Path: oh.cfg.CookiePath,
|
||||
Domain: oh.cfg.CookieDomain,
|
||||
MaxAge: int(accessTokenExpires.Seconds()),
|
||||
HttpOnly: oh.cfg.CookieHttpOnly,
|
||||
Secure: oh.cfg.ShouldUseSecureCookies(),
|
||||
SameSite: oh.cfg.GetCookieSameSite(),
|
||||
}
|
||||
http.SetCookie(c.Writer, accessTokenCookie)
|
||||
}
|
||||
|
||||
// Redirect to frontend (tokens in cookies, not URL)
|
||||
redirectURL := fmt.Sprintf("%s/auth/callback?user_id=%s", strings.TrimSuffix(frontendURL, "/"), user.ID.String())
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -25,12 +28,15 @@ func (m *MockOAuthService) GetAuthURL(provider string) (string, error) {
|
|||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockOAuthService) HandleCallback(provider, code, state string) (*services.OAuthUser, string, error) {
|
||||
args := m.Called(provider, code, state)
|
||||
func (m *MockOAuthService) HandleCallback(ctx context.Context, provider, code, state, ipAddress, userAgent string) (*services.OAuthUser, *models.TokenPair, error) {
|
||||
args := m.Called(ctx, provider, code, state, ipAddress, userAgent)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.String(1), args.Error(2)
|
||||
return nil, nil, args.Error(2)
|
||||
}
|
||||
return args.Get(0).(*services.OAuthUser), args.String(1), args.Error(2)
|
||||
if args.Get(1) == nil {
|
||||
return args.Get(0).(*services.OAuthUser), nil, args.Error(2)
|
||||
}
|
||||
return args.Get(0).(*services.OAuthUser), args.Get(1).(*models.TokenPair), args.Error(2)
|
||||
}
|
||||
|
||||
func (m *MockOAuthService) GetAvailableProviders() []string {
|
||||
|
|
@ -46,7 +52,7 @@ func setupTestOAuthRouter(mockService *MockOAuthService) *gin.Engine {
|
|||
router := gin.New()
|
||||
|
||||
logger := zap.NewNop()
|
||||
handler := NewOAuthHandlerWithInterface(mockService, logger)
|
||||
handler := NewOAuthHandlerWithInterface(mockService, logger, nil)
|
||||
|
||||
api := router.Group("/api/v1/auth/oauth")
|
||||
{
|
||||
|
|
@ -133,9 +139,13 @@ func TestOAuthHandlers_OAuthCallback_Success(t *testing.T) {
|
|||
ID: userID,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
token := "test-jwt-token"
|
||||
tokens := &models.TokenPair{
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
ExpiresIn: int(5 * time.Minute.Seconds()),
|
||||
}
|
||||
|
||||
mockService.On("HandleCallback", "google", "test-code", "test-state").Return(mockUser, token, nil)
|
||||
mockService.On("HandleCallback", mock.Anything, "google", "test-code", "test-state", mock.Anything, mock.Anything).Return(mockUser, tokens, nil)
|
||||
|
||||
// Execute
|
||||
req, _ := http.NewRequest("GET", "/api/v1/auth/oauth/google/callback?code=test-code&state=test-state", nil)
|
||||
|
|
@ -145,8 +155,10 @@ func TestOAuthHandlers_OAuthCallback_Success(t *testing.T) {
|
|||
// Assert
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
||||
location := w.Header().Get("Location")
|
||||
assert.Contains(t, location, "token=test-jwt-token")
|
||||
assert.Contains(t, location, "user_id="+userID.String())
|
||||
assert.Contains(t, location, "/auth/callback")
|
||||
// Tokens now in cookies, not URL
|
||||
assert.NotContains(t, location, "token=")
|
||||
mockService.AssertExpectations(t)
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +197,7 @@ func TestOAuthHandlers_OAuthCallback_ServiceError(t *testing.T) {
|
|||
mockService := new(MockOAuthService)
|
||||
router := setupTestOAuthRouter(mockService)
|
||||
|
||||
mockService.On("HandleCallback", "google", "test-code", "test-state").Return(nil, "", assert.AnError)
|
||||
mockService.On("HandleCallback", mock.Anything, "google", "test-code", "test-state", mock.Anything, mock.Anything).Return(nil, nil, assert.AnError)
|
||||
|
||||
// Execute
|
||||
req, _ := http.NewRequest("GET", "/api/v1/auth/oauth/google/callback?code=test-code&state=test-state", nil)
|
||||
|
|
@ -203,7 +215,7 @@ func TestNewOAuthHandlerWithInterface(t *testing.T) {
|
|||
logger := zap.NewNop()
|
||||
|
||||
// Execute
|
||||
handler := NewOAuthHandlerWithInterface(mockService, logger)
|
||||
handler := NewOAuthHandlerWithInterface(mockService, logger, nil)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, handler)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ type PresenceUpdater interface {
|
|||
UpdatePresence(ctx context.Context, userID uuid.UUID, status string) error
|
||||
}
|
||||
|
||||
// TokenBlacklistChecker checks if a token is blacklisted (VEZA-SEC-006)
|
||||
type TokenBlacklistChecker interface {
|
||||
IsBlacklisted(ctx context.Context, token string) (bool, error)
|
||||
}
|
||||
|
||||
// AuthMiddleware middleware d'authentification avec validation de session
|
||||
// ÉTAPE 3.4: Utilise des interfaces pour permettre l'injection de dépendances et les tests
|
||||
// v0.102: Supports X-API-Key for developer API keys (when apiKeyService is set)
|
||||
|
|
@ -51,11 +56,13 @@ type AuthMiddleware struct {
|
|||
userService *services.UserService // T0204: Check TokenVersion
|
||||
apiKeyService *services.APIKeyService // v0.102: Optional, for X-API-Key auth
|
||||
presenceService PresenceUpdater // v0.301: Optional, updates last_seen_at on auth
|
||||
tokenBlacklist TokenBlacklistChecker // VEZA-SEC-006: Optional, nil if Redis unavailable
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAuthMiddleware crée un nouveau middleware d'authentification
|
||||
// apiKeyService can be nil; when set, X-API-Key header is accepted as alternative to JWT
|
||||
// tokenBlacklist can be nil; when set (Redis available), blacklisted tokens are rejected (VEZA-SEC-006)
|
||||
func NewAuthMiddleware(
|
||||
sessionService SessionValidator,
|
||||
auditService AuditRecorder,
|
||||
|
|
@ -63,6 +70,7 @@ func NewAuthMiddleware(
|
|||
jwtService *services.JWTService,
|
||||
userService *services.UserService,
|
||||
apiKeyService *services.APIKeyService,
|
||||
tokenBlacklist TokenBlacklistChecker,
|
||||
logger *zap.Logger,
|
||||
) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
|
|
@ -72,6 +80,7 @@ func NewAuthMiddleware(
|
|||
jwtService: jwtService,
|
||||
userService: userService,
|
||||
apiKeyService: apiKeyService,
|
||||
tokenBlacklist: tokenBlacklist,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
|
@ -174,6 +183,22 @@ func (am *AuthMiddleware) authenticate(c *gin.Context) (uuid.UUID, bool) {
|
|||
return uuid.Nil, false
|
||||
}
|
||||
|
||||
// VEZA-SEC-006: Check token blacklist (revoked tokens)
|
||||
if am.tokenBlacklist != nil {
|
||||
blacklisted, err := am.tokenBlacklist.IsBlacklisted(c.Request.Context(), tokenString)
|
||||
if err != nil {
|
||||
am.logger.Warn("Token blacklist check failed", zap.Error(err))
|
||||
response.Unauthorized(c, "Invalid token")
|
||||
c.Abort()
|
||||
return uuid.Nil, false
|
||||
}
|
||||
if blacklisted {
|
||||
response.Unauthorized(c, "Token revoked")
|
||||
c.Abort()
|
||||
return uuid.Nil, false
|
||||
}
|
||||
}
|
||||
|
||||
userID := claims.UserID
|
||||
|
||||
// T0204: Check TokenVersion against DB to ensure immediate revocation
|
||||
|
|
|
|||
|
|
@ -253,6 +253,16 @@ func (m *MockPermissionService) HasPermission(ctx context.Context, userID uuid.U
|
|||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
// MockTokenBlacklist for VEZA-SEC-006 tests
|
||||
type MockTokenBlacklist struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockTokenBlacklist) IsBlacklisted(ctx context.Context, token string) (bool, error) {
|
||||
args := m.Called(ctx, token)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
// setupTestAuthMiddleware crée un AuthMiddleware configuré pour les tests
|
||||
// ÉTAPE 3.4: Utilise les interfaces pour permettre l'injection directe des mocks
|
||||
func setupTestAuthMiddleware(t *testing.T, jwtService *services.JWTService) (*AuthMiddleware, *MockSessionService, *MockAuditService, *MockPermissionService, *MockUserRepository) {
|
||||
|
|
@ -275,7 +285,7 @@ func setupTestAuthMiddleware(t *testing.T, jwtService *services.JWTService) (*Au
|
|||
|
||||
// ÉTAPE 3.4: Les mocks implémentent maintenant directement les interfaces
|
||||
// Plus besoin de wrappers ou de hacks - injection directe des mocks
|
||||
authMiddleware := NewAuthMiddleware(mockSessionService, mockAuditService, mockPermissionService, jwtService, userService, nil, logger)
|
||||
authMiddleware := NewAuthMiddleware(mockSessionService, mockAuditService, mockPermissionService, jwtService, userService, nil, nil, logger)
|
||||
|
||||
// Mock defaults for GetByID for generic tests (assume user found and version 0)
|
||||
// We use .Maybe() because not all tests will hit it (e.g. invalid token format)
|
||||
|
|
@ -340,6 +350,57 @@ func TestAuthMiddleware_ValidToken(t *testing.T) {
|
|||
mockSessionService.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// TestAuthMiddleware_BlacklistedToken_Returns401 verifies that a valid JWT but blacklisted token returns 401 (VEZA-SEC-006)
|
||||
func TestAuthMiddleware_BlacklistedToken_Returns401(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
_, _, _, _, mockUserRepository := setupTestAuthMiddleware(t, nil)
|
||||
|
||||
userUUID := uuid.MustParse("00000000-0000-0000-0000-000000000042")
|
||||
token := generateTestToken(t, userUUID, 15*time.Minute)
|
||||
|
||||
// Create middleware with mock blacklist that returns true (blacklisted)
|
||||
mockBlacklist := new(MockTokenBlacklist)
|
||||
mockBlacklist.On("IsBlacklisted", mock.Anything, token).Return(true, nil)
|
||||
|
||||
// Rebuild auth middleware with blacklist
|
||||
logger, _ := zap.NewDevelopment()
|
||||
userService := services.NewUserService(mockUserRepository)
|
||||
mockUserRepository.On("GetByID", userUUID.String()).Return(&models.User{
|
||||
ID: userUUID,
|
||||
TokenVersion: 0,
|
||||
}, nil)
|
||||
|
||||
jwtService := setupTestJWTService(t)
|
||||
mockSessionService2 := new(MockSessionService)
|
||||
mockAuditService2 := new(MockAuditService)
|
||||
mockPermissionService2 := new(MockPermissionService)
|
||||
mockAuditService2.On("LogAction", mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
|
||||
authWithBlacklist := NewAuthMiddleware(
|
||||
mockSessionService2, mockAuditService2, mockPermissionService2,
|
||||
jwtService, userService, nil, mockBlacklist, logger,
|
||||
)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(authWithBlacklist.RequireAuth())
|
||||
router.GET("/test", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
var response map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response))
|
||||
assert.False(t, response["success"].(bool))
|
||||
errorObj := response["error"].(map[string]interface{})
|
||||
assert.Equal(t, "Token revoked", errorObj["message"])
|
||||
mockBlacklist.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
authMiddleware, _, _, _, _ := setupTestAuthMiddleware(t, nil)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ func setupTestAuthMiddlewareWithRBAC(t *testing.T, permissionChecker PermissionC
|
|||
jwtService := setupTestJWTService(t) // Reuse helper from auth_middleware_test.go
|
||||
userService := services.NewUserService(mockUserRepository)
|
||||
|
||||
authMiddleware := NewAuthMiddleware(mockSessionService, mockAuditService, permissionChecker, jwtService, userService, nil, logger)
|
||||
authMiddleware := NewAuthMiddleware(mockSessionService, mockAuditService, permissionChecker, jwtService, userService, nil, nil, logger)
|
||||
|
||||
return authMiddleware, mockSessionService, mockAuditService, mockUserRepository
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import (
|
|||
"time"
|
||||
|
||||
"veza-backend-api/internal/database"
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/utils"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
|
|
@ -23,14 +23,16 @@ import (
|
|||
|
||||
// OAuthService handles OAuth authentication
|
||||
type OAuthService struct {
|
||||
db *database.Database
|
||||
logger *zap.Logger
|
||||
googleConfig *oauth2.Config
|
||||
githubConfig *oauth2.Config
|
||||
discordConfig *oauth2.Config
|
||||
spotifyConfig *oauth2.Config
|
||||
jwtSecret []byte
|
||||
circuitBreaker *CircuitBreakerHTTPClient
|
||||
db *database.Database
|
||||
logger *zap.Logger
|
||||
googleConfig *oauth2.Config
|
||||
githubConfig *oauth2.Config
|
||||
discordConfig *oauth2.Config
|
||||
spotifyConfig *oauth2.Config
|
||||
jwtService *JWTService
|
||||
sessionService *SessionService
|
||||
userService *UserService
|
||||
circuitBreaker *CircuitBreakerHTTPClient
|
||||
}
|
||||
|
||||
// OAuthAccount represents an OAuth account linking
|
||||
|
|
@ -61,12 +63,14 @@ type OAuthState struct {
|
|||
}
|
||||
|
||||
// NewOAuthService creates a new OAuth service
|
||||
func NewOAuthService(db *database.Database, logger *zap.Logger, jwtSecret []byte) *OAuthService {
|
||||
func NewOAuthService(db *database.Database, logger *zap.Logger, jwtService *JWTService, sessionService *SessionService, userService *UserService) *OAuthService {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
return &OAuthService{
|
||||
db: db,
|
||||
logger: logger,
|
||||
jwtSecret: jwtSecret,
|
||||
jwtService: jwtService,
|
||||
sessionService: sessionService,
|
||||
userService: userService,
|
||||
circuitBreaker: NewCircuitBreakerHTTPClient(httpClient, "oauth-service", logger),
|
||||
}
|
||||
}
|
||||
|
|
@ -253,12 +257,13 @@ func (os *OAuthService) GetAuthURL(provider string) (string, error) {
|
|||
return url, nil
|
||||
}
|
||||
|
||||
// HandleCallback processes the OAuth callback
|
||||
func (os *OAuthService) HandleCallback(provider, code, state string) (*OAuthUser, string, error) {
|
||||
// HandleCallback processes the OAuth callback.
|
||||
// ipAddress and userAgent are used for session creation (optional, can be empty).
|
||||
func (os *OAuthService) HandleCallback(ctx context.Context, provider, code, state, ipAddress, userAgent string) (*OAuthUser, *models.TokenPair, error) {
|
||||
// Validate state
|
||||
_, err := os.ValidateStateToken(state)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var config *oauth2.Config
|
||||
|
|
@ -272,47 +277,69 @@ func (os *OAuthService) HandleCallback(provider, code, state string) (*OAuthUser
|
|||
case "spotify":
|
||||
config = os.spotifyConfig
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unknown provider: %s", provider)
|
||||
return nil, nil, fmt.Errorf("unknown provider: %s", provider)
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return nil, "", fmt.Errorf("%s OAuth not configured", provider)
|
||||
return nil, nil, fmt.Errorf("%s OAuth not configured", provider)
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
token, err := config.Exchange(context.Background(), code)
|
||||
token, err := config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get user info from provider
|
||||
oauthUser, err := os.getUserInfo(provider, token.AccessToken)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Check if user already exists (by provider account or email) — audit 1.8: OAuth ID lookup first
|
||||
existingUser, err := os.getOrCreateUser(provider, oauthUser)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Save/update OAuth account
|
||||
err = os.saveOAuthAccount(provider, oauthUser, existingUser.ID, token)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Generate JWT for the user
|
||||
jwtToken, err := os.generateJWT(existingUser.ID)
|
||||
// VEZA-SEC-001: Get full user for JWT (TokenVersion, Role, etc.)
|
||||
user, err := os.userService.GetByID(existingUser.ID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
// Generate tokens via JWTService (proper issuer, audience, token_version)
|
||||
tokens, err := os.jwtService.GenerateTokenPair(user)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
// Create session for refresh token validation
|
||||
_, err = os.sessionService.CreateSession(ctx, &SessionCreateRequest{
|
||||
UserID: user.ID,
|
||||
Token: tokens.RefreshToken,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
ExpiresIn: os.jwtService.GetConfig().RefreshTokenTTL,
|
||||
})
|
||||
if err != nil {
|
||||
os.logger.Warn("Failed to create session after OAuth callback",
|
||||
zap.String("user_id", user.ID.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
// Continue - tokens still valid, session is optional for some flows
|
||||
}
|
||||
|
||||
return &OAuthUser{
|
||||
ID: existingUser.ID,
|
||||
Email: existingUser.Email,
|
||||
}, jwtToken, nil
|
||||
}, tokens, nil
|
||||
}
|
||||
|
||||
// OAuthUser represents an OAuth authenticated user
|
||||
|
|
@ -580,14 +607,3 @@ func (os *OAuthService) saveOAuthAccount(provider string, oauthUser *OAuthUser,
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
// generateJWT generates a JWT token for the user
|
||||
func (os *OAuthService) generateJWT(userID uuid.UUID) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID.String(),
|
||||
"sub": userID.String(),
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
|
||||
return token.SignedString(os.jwtSecret)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,26 @@ import (
|
|||
"go.uber.org/zap"
|
||||
|
||||
"veza-backend-api/internal/database"
|
||||
"veza-backend-api/internal/repositories"
|
||||
)
|
||||
|
||||
// setupOAuthServiceForTests creates OAuthService with minimal deps for tests that don't call HandleCallback.
|
||||
// Uses sqlite in-memory when db has no GormDB (e.g. from sqlmock).
|
||||
func setupOAuthServiceForTests(t *testing.T, db *database.Database) *OAuthService {
|
||||
t.Helper()
|
||||
jwtService, err := NewJWTService("test-secret-key-minimum-32-characters-long", "veza-api", "veza-app")
|
||||
require.NoError(t, err)
|
||||
// Use db for SessionService (needs sql.DB)
|
||||
sessionService := NewSessionService(db, zap.NewNop())
|
||||
// UserService needs GormDB - use db.GormDB if available, else nil (tests don't call HandleCallback)
|
||||
var userService *UserService
|
||||
if db.GormDB != nil {
|
||||
userRepo := repositories.NewGormUserRepository(db.GormDB)
|
||||
userService = NewUserServiceWithDB(userRepo, db.GormDB)
|
||||
}
|
||||
return NewOAuthService(db, zap.NewNop(), jwtService, sessionService, userService)
|
||||
}
|
||||
|
||||
// Helper to setup mock DB
|
||||
func setupMockDB(t *testing.T) (*database.Database, sqlmock.Sqlmock) {
|
||||
db, mock, err := sqlmock.New()
|
||||
|
|
@ -158,7 +176,7 @@ func TestOAuthService_GetAuthURL_Discord(t *testing.T) {
|
|||
db, mock := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
svc.InitializeConfigs("", "", "", "", "discord-client", "discord-secret", "", "", "http://localhost:8080")
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO oauth_states`)).
|
||||
|
|
@ -180,7 +198,7 @@ func TestOAuthService_GetAuthURL_Spotify(t *testing.T) {
|
|||
db, mock := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
svc.InitializeConfigs("", "", "", "", "", "", "spotify-client", "spotify-secret", "http://localhost:8080")
|
||||
|
||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO oauth_states`)).
|
||||
|
|
@ -202,7 +220,7 @@ func TestOAuthService_GetAvailableProviders(t *testing.T) {
|
|||
db, _ := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
providers := svc.GetAvailableProviders()
|
||||
assert.Empty(t, providers)
|
||||
|
||||
|
|
@ -243,7 +261,7 @@ func TestOAuthService_GetUserInfo_Discord(t *testing.T) {
|
|||
db, mock := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
svc.InitializeConfigs("", "", "", "", "did", "dsec", "", "", "http://localhost")
|
||||
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
||||
|
||||
|
|
@ -265,7 +283,7 @@ func TestOAuthService_GetUserInfo_Spotify(t *testing.T) {
|
|||
db, _ := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
svc.InitializeConfigs("", "", "", "", "", "", "sid", "ssec", "http://localhost")
|
||||
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
||||
|
||||
|
|
@ -286,7 +304,7 @@ func TestOAuthService_GetUserInfo_Spotify_FallbackEmail(t *testing.T) {
|
|||
db, _ := setupMockDB(t)
|
||||
defer db.DB.Close()
|
||||
|
||||
svc := NewOAuthService(db, zap.NewNop(), []byte("secret"))
|
||||
svc := setupOAuthServiceForTests(t, db)
|
||||
svc.InitializeConfigs("", "", "", "", "", "", "sid", "ssec", "http://localhost")
|
||||
svc.circuitBreaker = NewCircuitBreakerHTTPClient(client, "oauth-test", zap.NewNop())
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
|
@ -256,16 +255,6 @@ func (ps *PasswordService) ChangePassword(userID uuid.UUID, oldPassword, newPass
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateJWT generates a JWT token for the user (used internally)
|
||||
func (ps *PasswordService) GenerateJWT(userID uuid.UUID, secret []byte) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID.String(), // Convert UUID to string for JWT claims
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
|
||||
// UpdatePassword updates a user's password by user ID
|
||||
// T0194: Updates password with bcrypt hash
|
||||
func (ps *PasswordService) UpdatePassword(userID uuid.UUID, newPassword string) error {
|
||||
|
|
|
|||
|
|
@ -329,30 +329,3 @@ func TestPasswordService_UpdatePassword_WeakPassword(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "weak password")
|
||||
}
|
||||
|
||||
func TestPasswordService_GenerateJWT_Success(t *testing.T) {
|
||||
service, _, _ := setupTestPasswordServiceIntegration(t)
|
||||
|
||||
userID := uuid.New()
|
||||
secret := []byte("test_secret_key")
|
||||
|
||||
token, err := service.GenerateJWT(userID, secret)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestPasswordService_GenerateJWT_DifferentUsers(t *testing.T) {
|
||||
service, _, _ := setupTestPasswordServiceIntegration(t)
|
||||
|
||||
userID1 := uuid.New()
|
||||
userID2 := uuid.New()
|
||||
secret := []byte("test_secret_key")
|
||||
|
||||
token1, err1 := service.GenerateJWT(userID1, secret)
|
||||
assert.NoError(t, err1)
|
||||
|
||||
token2, err2 := service.GenerateJWT(userID2, secret)
|
||||
assert.NoError(t, err2)
|
||||
|
||||
assert.NotEqual(t, token1, token2, "Different users should generate different tokens")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"gorm.io/gorm"
|
||||
|
||||
"veza-backend-api/internal/models"
|
||||
"veza-backend-api/internal/utils"
|
||||
)
|
||||
|
||||
type WaveformData struct {
|
||||
|
|
@ -64,6 +65,9 @@ func (s *WaveformService) GenerateWaveformAsync(ctx context.Context, trackID uui
|
|||
}
|
||||
|
||||
func (s *WaveformService) generateWaveform(ctx context.Context, trackID uuid.UUID, inputPath string) error {
|
||||
if !utils.ValidateExecPath(inputPath) {
|
||||
return fmt.Errorf("invalid input path")
|
||||
}
|
||||
wavPath := filepath.Join(s.tempDir, fmt.Sprintf("waveform_%s.wav", trackID))
|
||||
jsonPath := filepath.Join(s.tempDir, fmt.Sprintf("waveform_%s.json", trackID))
|
||||
defer os.Remove(wavPath)
|
||||
|
|
@ -117,6 +121,9 @@ func (s *WaveformService) generateWaveform(ctx context.Context, trackID uuid.UUI
|
|||
}
|
||||
|
||||
func (s *WaveformService) generateFallbackWaveform(ctx context.Context, trackID uuid.UUID, inputPath string) error {
|
||||
if !utils.ValidateExecPath(inputPath) {
|
||||
return fmt.Errorf("invalid input path")
|
||||
}
|
||||
s.logger.Warn("audiowaveform not available, generating fallback waveform via FFmpeg",
|
||||
zap.String("track_id", trackID.String()),
|
||||
)
|
||||
|
|
|
|||
43
veza-backend-api/internal/services/waveform_service_test.go
Normal file
43
veza-backend-api/internal/services/waveform_service_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupTestWaveformService(t *testing.T) *WaveformService {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
logger := zap.NewNop()
|
||||
return NewWaveformService(db, logger, nil)
|
||||
}
|
||||
|
||||
// TestGenerateWaveform_InvalidPath_ReturnsError verifies that paths containing ".." are rejected before exec (VEZA-SEC-007)
|
||||
func TestGenerateWaveform_InvalidPath_ReturnsError(t *testing.T) {
|
||||
svc := setupTestWaveformService(t)
|
||||
ctx := context.Background()
|
||||
trackID := uuid.New()
|
||||
|
||||
err := svc.generateWaveform(ctx, trackID, "/tmp/../etc/passwd")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid input path")
|
||||
}
|
||||
|
||||
// TestGenerateFallbackWaveform_InvalidPath_ReturnsError verifies ValidateExecPath in fallback path (VEZA-SEC-007)
|
||||
func TestGenerateFallbackWaveform_InvalidPath_ReturnsError(t *testing.T) {
|
||||
svc := setupTestWaveformService(t)
|
||||
ctx := context.Background()
|
||||
trackID := uuid.New()
|
||||
|
||||
err := svc.generateFallbackWaveform(ctx, trackID, "/path/with/../traversal")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid input path")
|
||||
}
|
||||
|
|
@ -138,6 +138,7 @@ func setupAuthorizationTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *service
|
|||
jwtService,
|
||||
services.NewUserServiceWithDB(repositories.NewGormUserRepository(db), db),
|
||||
nil,
|
||||
nil, // TokenBlacklist
|
||||
logger,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue