diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index b62d819ba..74a36c006 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -5281,7 +5281,7 @@ "description": "Test webhook registration, triggering, and management", "owner": "backend", "estimated_hours": 4, - "status": "todo", + "status": "completed", "files_involved": [], "implementation_steps": [ { @@ -5302,7 +5302,21 @@ "Unit tests", "Integration tests" ], - "notes": "" + "notes": "", + "completion": { + "completed_at": "2025-12-25T01:32:54.026884", + "completed_by": "autonomous-agent", + "notes": "Added comprehensive unit tests for webhook handlers including RegisterWebhook, ListWebhooks, DeleteWebhook, GetWebhookStats, TestWebhook, and RegenerateAPIKey. Fixed validation bug in BindAndValidateJSON to properly return errors for binding validation failures. All tests passing.", + "files_modified": [ + "veza-backend-api/internal/handlers/webhook_handlers_test.go", + "veza-backend-api/internal/handlers/common.go", + "veza-backend-api/internal/handlers/profile_handler_test.go", + "veza-backend-api/internal/handlers/room_handler_test.go" + ] + }, + "progress_tracking": { + "last_updated": "2025-12-25T01:32:54.026895" + } }, { "id": "BE-TEST-008", @@ -11185,11 +11199,11 @@ ] }, "progress_tracking": { - "completed": 126, + "completed": 127, "in_progress": 0, "todo": 141, "blocked": 0, - "last_updated": "2025-12-25T00:30:24.196150Z", - "completion_percentage": 47.19101123595505 + "last_updated": "2025-12-25T01:32:54.026907", + "completion_percentage": 47.565543071161045 } } \ No newline at end of file diff --git a/veza-backend-api/internal/handlers/common.go b/veza-backend-api/internal/handlers/common.go index 4580f9ee9..d467814ab 100644 --- a/veza-backend-api/internal/handlers/common.go +++ b/veza-backend-api/internal/handlers/common.go @@ -294,12 +294,17 @@ func (h *CommonHandler) BindAndValidateJSON(c *gin.Context, obj interface{}) *ap } // Pour les autres erreurs de binding, on considère que c'est une erreur de validation - // et on va laisser le validator s'en occuper + // Les erreurs de validation de binding Gin (comme "required", "url", "min") doivent être retournées h.logger.Debug("JSON binding error (will be handled by validator)", zap.Error(err), zap.String("request_id", requestID), zap.String("endpoint", c.Request.URL.Path), ) + // Retourner l'erreur de validation de binding + return apperrors.New( + apperrors.ErrCodeValidation, + fmt.Sprintf("Validation failed: %s", err.Error()), + ) } } diff --git a/veza-backend-api/internal/handlers/profile_handler_test.go b/veza-backend-api/internal/handlers/profile_handler_test.go index 9e9d9d96e..83715eb3e 100644 --- a/veza-backend-api/internal/handlers/profile_handler_test.go +++ b/veza-backend-api/internal/handlers/profile_handler_test.go @@ -76,7 +76,7 @@ func setupTestProfileHandler(t *testing.T) (*ProfileHandler, *gorm.DB, *gin.Engi // Create database wrapper for SocialService // Note: SocialService uses raw SQL, so we need to get the underlying sql.DB - sqlDB, err := db.DB() + sqlDB, err = db.DB() require.NoError(t, err) dbWrapper := &database.Database{} dbWrapper.DB = sqlDB @@ -110,13 +110,13 @@ func setupTestProfileHandler(t *testing.T) (*ProfileHandler, *gorm.DB, *gin.Engi // Helper to create a test user func createTestUserForProfile(id uuid.UUID, username string) *models.User { return &models.User{ - ID: id, - Username: username, - Email: fmt.Sprintf("%s@example.com", username), - IsActive: true, + ID: id, + Username: username, + Email: fmt.Sprintf("%s@example.com", username), + IsActive: true, IsVerified: true, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } } @@ -601,4 +601,3 @@ func TestProfileHandler_DeleteUser_Forbidden(t *testing.T) { assert.Equal(t, http.StatusForbidden, w.Code) } - diff --git a/veza-backend-api/internal/handlers/room_handler_test.go b/veza-backend-api/internal/handlers/room_handler_test.go index cb7dd3ebd..52cebd0af 100644 --- a/veza-backend-api/internal/handlers/room_handler_test.go +++ b/veza-backend-api/internal/handlers/room_handler_test.go @@ -20,8 +20,11 @@ type MockRoomService struct { CreateRoomFunc func(ctx context.Context, userID uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error) GetUserRoomsFunc func(ctx context.Context, userID uuid.UUID) ([]*services.RoomResponse, error) GetRoomFunc func(ctx context.Context, roomID uuid.UUID) (*services.RoomResponse, error) + UpdateRoomFunc func(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req services.UpdateRoomRequest) (*services.RoomResponse, error) AddMemberFunc func(ctx context.Context, roomID, userID uuid.UUID) error + RemoveMemberFunc func(ctx context.Context, roomID, userID uuid.UUID) error GetRoomHistoryFunc func(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]services.ChatMessageResponse, error) + DeleteRoomFunc func(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error } func (m *MockRoomService) CreateRoom(ctx context.Context, userID uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error) { @@ -59,6 +62,27 @@ func (m *MockRoomService) GetRoomHistory(ctx context.Context, roomID uuid.UUID, return nil, nil } +func (m *MockRoomService) UpdateRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req services.UpdateRoomRequest) (*services.RoomResponse, error) { + if m.UpdateRoomFunc != nil { + return m.UpdateRoomFunc(ctx, roomID, userID, req) + } + return nil, nil +} + +func (m *MockRoomService) RemoveMember(ctx context.Context, roomID, userID uuid.UUID) error { + if m.RemoveMemberFunc != nil { + return m.RemoveMemberFunc(ctx, roomID, userID) + } + return nil +} + +func (m *MockRoomService) DeleteRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error { + if m.DeleteRoomFunc != nil { + return m.DeleteRoomFunc(ctx, roomID, userID) + } + return nil +} + func TestRoomHandler_CreateRoom(t *testing.T) { // Setup gin.SetMode(gin.TestMode) diff --git a/veza-backend-api/internal/handlers/webhook_handlers_test.go b/veza-backend-api/internal/handlers/webhook_handlers_test.go new file mode 100644 index 000000000..746572fb6 --- /dev/null +++ b/veza-backend-api/internal/handlers/webhook_handlers_test.go @@ -0,0 +1,487 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "veza-backend-api/internal/models" + "veza-backend-api/internal/services" + "veza-backend-api/internal/workers" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// setupTestWebhookHandler creates a test handler with real services and in-memory database +func setupTestWebhookHandler(t *testing.T) (*WebhookHandler, *gorm.DB, *gin.Engine, func()) { + gin.SetMode(gin.TestMode) + logger := zaptest.NewLogger(t) + + // Setup in-memory SQLite database + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + db.Exec("PRAGMA foreign_keys = ON") + + // Auto-migrate models + err = db.AutoMigrate( + &models.User{}, + &models.Webhook{}, + ) + require.NoError(t, err) + + // Setup webhook service + jwtSecret := "test-secret-key" + webhookService := services.NewWebhookService(db, logger, jwtSecret) + + // Setup webhook worker + webhookWorker := workers.NewWebhookWorker( + db, + webhookService, + logger, + 100, // Queue size + 5, // Workers + 3, // Max retries + ) + + handler := NewWebhookHandler(webhookService, webhookWorker, logger) + + router := gin.New() + router.Use(func(c *gin.Context) { + // Mock auth middleware - set user_id from header if present + userIDStr := c.GetHeader("X-User-ID") + if userIDStr != "" { + uid, err := uuid.Parse(userIDStr) + if err == nil { + c.Set("user_id", uid) + } + } + c.Next() + }) + + cleanup := func() { + // Database cleanup handled by test + } + + return handler, db, router, cleanup +} + +// Helper to create a test user +func createTestUserForWebhook(id uuid.UUID, username string) *models.User { + return &models.User{ + ID: id, + Username: username, + Email: fmt.Sprintf("%s@example.com", username), + IsActive: true, + IsVerified: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +// TestWebhookHandler_RegisterWebhook_Success tests successful webhook registration +func TestWebhookHandler_RegisterWebhook_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.POST("/webhooks", handler.RegisterWebhook()) + + registerReq := map[string]interface{}{ + "url": "https://example.com/webhook", + "events": []string{"track.created", "track.updated"}, + } + body, _ := json.Marshal(registerReq) + req := httptest.NewRequest(http.MethodPost, "/webhooks", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) +} + +// TestWebhookHandler_RegisterWebhook_InvalidURL tests webhook registration with invalid URL +func TestWebhookHandler_RegisterWebhook_InvalidURL(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.POST("/webhooks", handler.RegisterWebhook()) + + registerReq := map[string]interface{}{ + "url": "not-a-valid-url", + "events": []string{"track.created"}, + } + body, _ := json.Marshal(registerReq) + req := httptest.NewRequest(http.MethodPost, "/webhooks", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +// TestWebhookHandler_RegisterWebhook_NoEvents tests webhook registration with no events +func TestWebhookHandler_RegisterWebhook_NoEvents(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.POST("/webhooks", handler.RegisterWebhook()) + + registerReq := map[string]interface{}{ + "url": "https://example.com/webhook", + "events": []string{}, + } + body, _ := json.Marshal(registerReq) + req := httptest.NewRequest(http.MethodPost, "/webhooks", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +// TestWebhookHandler_RegisterWebhook_Unauthorized tests webhook registration without authentication +func TestWebhookHandler_RegisterWebhook_Unauthorized(t *testing.T) { + handler, _, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + router.POST("/webhooks", handler.RegisterWebhook()) + + registerReq := map[string]interface{}{ + "url": "https://example.com/webhook", + "events": []string{"track.created"}, + } + body, _ := json.Marshal(registerReq) + req := httptest.NewRequest(http.MethodPost, "/webhooks", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + // No X-User-ID header + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusUnauthorized, w.Code) +} + +// TestWebhookHandler_ListWebhooks_Success tests successful webhook listing +func TestWebhookHandler_ListWebhooks_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + // Create test webhooks with unique API keys + logger := zaptest.NewLogger(t) + webhookService := services.NewWebhookService(db, logger, "test-secret") + for i := 0; i < 3; i++ { + apiKey, err := webhookService.GenerateAPIKey() + require.NoError(t, err) + webhook := &models.Webhook{ + ID: uuid.New(), + UserID: userID, + URL: fmt.Sprintf("https://example.com/webhook%d", i+1), + Events: []string{"track.created"}, + Active: true, + APIKey: apiKey, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = db.Create(webhook).Error + require.NoError(t, err) + } + + router.GET("/webhooks", handler.ListWebhooks()) + + req := httptest.NewRequest(http.MethodGet, "/webhooks", nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) +} + +// TestWebhookHandler_DeleteWebhook_Success tests successful webhook deletion +func TestWebhookHandler_DeleteWebhook_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + // Create test webhook + webhookID := uuid.New() + webhook := &models.Webhook{ + ID: webhookID, + UserID: userID, + URL: "https://example.com/webhook", + Events: []string{"track.created"}, + Active: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = db.Create(webhook).Error + require.NoError(t, err) + + router.DELETE("/webhooks/:id", handler.DeleteWebhook()) + + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/webhooks/%s", webhookID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) +} + +// TestWebhookHandler_DeleteWebhook_NotFound tests webhook deletion with non-existent webhook +func TestWebhookHandler_DeleteWebhook_NotFound(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.DELETE("/webhooks/:id", handler.DeleteWebhook()) + + nonExistentID := uuid.New() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/webhooks/%s", nonExistentID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +// TestWebhookHandler_DeleteWebhook_InvalidID tests webhook deletion with invalid ID format +func TestWebhookHandler_DeleteWebhook_InvalidID(t *testing.T) { + handler, _, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + router.DELETE("/webhooks/:id", handler.DeleteWebhook()) + + req := httptest.NewRequest(http.MethodDelete, "/webhooks/invalid-id", nil) + req.Header.Set("X-User-ID", uuid.New().String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +// TestWebhookHandler_GetWebhookStats_Success tests successful webhook stats retrieval +func TestWebhookHandler_GetWebhookStats_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.GET("/webhooks/stats", handler.GetWebhookStats()) + + req := httptest.NewRequest(http.MethodGet, "/webhooks/stats", nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) +} + +// TestWebhookHandler_TestWebhook_Success tests successful webhook test +func TestWebhookHandler_TestWebhook_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + // Create test webhook + webhookID := uuid.New() + webhook := &models.Webhook{ + ID: webhookID, + UserID: userID, + URL: "https://example.com/webhook", + Events: []string{"track.created"}, + Active: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = db.Create(webhook).Error + require.NoError(t, err) + + router.POST("/webhooks/:id/test", handler.TestWebhook()) + + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/webhooks/%s/test", webhookID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) +} + +// TestWebhookHandler_TestWebhook_NotFound tests webhook test with non-existent webhook +func TestWebhookHandler_TestWebhook_NotFound(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.POST("/webhooks/:id/test", handler.TestWebhook()) + + nonExistentID := uuid.New() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/webhooks/%s/test", nonExistentID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +// TestWebhookHandler_RegenerateAPIKey_Success tests successful API key regeneration +func TestWebhookHandler_RegenerateAPIKey_Success(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + // Create test webhook + webhookID := uuid.New() + webhook := &models.Webhook{ + ID: webhookID, + UserID: userID, + URL: "https://example.com/webhook", + Events: []string{"track.created"}, + Active: true, + APIKey: "old_api_key", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + err = db.Create(webhook).Error + require.NoError(t, err) + + router.POST("/webhooks/:id/regenerate-key", handler.RegenerateAPIKey()) + + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/webhooks/%s/regenerate-key", webhookID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var response APIResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response.Success) + + // Verify that API key is present in response + dataBytes, _ := json.Marshal(response.Data) + var data map[string]interface{} + err = json.Unmarshal(dataBytes, &data) + require.NoError(t, err) + assert.Contains(t, data, "api_key") + assert.NotEmpty(t, data["api_key"]) +} + +// TestWebhookHandler_RegenerateAPIKey_NotFound tests API key regeneration with non-existent webhook +func TestWebhookHandler_RegenerateAPIKey_NotFound(t *testing.T) { + handler, db, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + // Create test user + userID := uuid.New() + user := createTestUserForWebhook(userID, "testuser") + err := db.Create(user).Error + require.NoError(t, err) + + router.POST("/webhooks/:id/regenerate-key", handler.RegenerateAPIKey()) + + nonExistentID := uuid.New() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/webhooks/%s/regenerate-key", nonExistentID.String()), nil) + req.Header.Set("X-User-ID", userID.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +// TestWebhookHandler_RegenerateAPIKey_InvalidID tests API key regeneration with invalid ID format +func TestWebhookHandler_RegenerateAPIKey_InvalidID(t *testing.T) { + handler, _, router, cleanup := setupTestWebhookHandler(t) + defer cleanup() + + router.POST("/webhooks/:id/regenerate-key", handler.RegenerateAPIKey()) + + req := httptest.NewRequest(http.MethodPost, "/webhooks/invalid-id/regenerate-key", nil) + req.Header.Set("X-User-ID", uuid.New().String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +}