[BE-TEST-007] test: Add unit tests for webhook handlers

- Added comprehensive unit tests for all webhook handler methods:
  * RegisterWebhook (success, invalid URL, no events, unauthorized)
  * ListWebhooks (success)
  * DeleteWebhook (success, not found, invalid ID)
  * GetWebhookStats (success)
  * TestWebhook (success, not found)
  * RegenerateAPIKey (success, not found, invalid ID)
- Fixed validation bug in BindAndValidateJSON to properly return errors for binding validation failures
- Fixed compilation errors in profile_handler_test.go and room_handler_test.go
- All tests passing
This commit is contained in:
senke 2025-12-25 01:32:54 +01:00
parent 8de5dc1be2
commit 414663af23
5 changed files with 543 additions and 14 deletions

View file

@ -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
}
}

View file

@ -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()),
)
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}