From fb25465e21c0e821821e1e6aa809ff5f409312b4 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 28 Dec 2025 22:30:38 +0100 Subject: [PATCH] [T0-006] test(backend): Ajout tests pour role_handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tests complets pour role_handler.go (22 tests) - Interface RoleServiceInterface créée pour permettre le mock - Tests couvrent GetRoles, GetRole, CreateRole, UpdateRole, DeleteRole, AssignRole, RevokeRole, GetUserRoles - Couverture actuelle: 30.3% (objectif: 80%) Files: veza-backend-api/internal/handlers/role_handler.go veza-backend-api/internal/handlers/role_handler_test.go VEZA_ROADMAP.json Hours: 16 estimated, 19 actual --- VEZA_ROADMAP.json | 10 +- .../veza-backend-api_internal_config.out | 4 +- .../veza-backend-api_internal_logging.out | 2 +- .../veza-backend-api_internal_workers.out | 8 +- veza-backend-api/coverage_total.out | 14 +- .../internal/handlers/role_handler.go | 24 +- .../internal/handlers/role_handler_test.go | 688 ++++++++++++++++++ 7 files changed, 731 insertions(+), 19 deletions(-) create mode 100644 veza-backend-api/internal/handlers/role_handler_test.go diff --git a/VEZA_ROADMAP.json b/VEZA_ROADMAP.json index cc72669e7..da9d1afb9 100644 --- a/VEZA_ROADMAP.json +++ b/VEZA_ROADMAP.json @@ -235,7 +235,7 @@ "priority": "P0", "status": "in_progress", "estimated_hours": 16, - "actual_hours": 17, + "actual_hours": 19, "started_at": "2025-12-28T15:13:09Z", "completed_at": null, "dependencies": ["T0-001", "T0-005"], @@ -269,21 +269,23 @@ "veza-backend-api/internal/handlers/search_handlers_test.go", "veza-backend-api/internal/handlers/comment_handler_test.go", "veza-backend-api/internal/handlers/avatar_handler_test.go", - "veza-backend-api/internal/handlers/notification_handlers_test.go" + "veza-backend-api/internal/handlers/notification_handlers_test.go", + "veza-backend-api/internal/handlers/role_handler_test.go" ], "to_modify": [ "veza-backend-api/internal/models/role.go", "veza-backend-api/internal/handlers/search_handlers.go", "veza-backend-api/internal/handlers/comment_handler.go", "veza-backend-api/internal/handlers/avatar_handler.go", - "veza-backend-api/internal/handlers/notification_handlers.go" + "veza-backend-api/internal/handlers/notification_handlers.go", + "veza-backend-api/internal/handlers/role_handler.go" ] }, "commands": { "verify": ["cd veza-backend-api && ./scripts/test_coverage_one_by_one.sh"], "test": ["cd veza-backend-api && go test ./internal/api/handlers -run TestRBACHandlers -v", "cd veza-backend-api && go test ./internal/api/user -run TestUserHandler -v", "cd veza-backend-api && go test ./internal/services -run TestSocialService -v", "cd veza-backend-api && go test ./internal/services -run TestCacheService -v", "cd veza-backend-api && go test ./internal/services -run TestNotificationService -v", "cd veza-backend-api && go test ./internal/services -run TestPasswordService_ -v", "cd veza-backend-api && go test ./internal/services -run TestMetadataService -v", "cd veza-backend-api && go test ./internal/services -run TestBackupService -v", "cd veza-backend-api && go test ./internal/services -run TestJobService -v", "cd veza-backend-api && go test ./internal/services -run TestAuditService -v", "cd veza-backend-api && go test ./internal/services -run TestAccountLockoutService -v", "cd veza-backend-api && go test ./internal/services -run TestEmailService_ -v", "cd veza-backend-api && go test ./internal/services -run TestRoleService_ -v"] }, - "implementation_notes": "Progrès réalisés: 1) Scripts créés pour exécuter les tests par groupes/packages individuels (évite les crashes RAM), 2) Tests complets pour handlers RBAC (16 tests, tous passent), 3) Tests complets pour handlers user (16 tests, tous passent), 4) Tests complets pour service social (18 tests, tous passent), 5) Tests complets pour service cache (20 tests, tous passent), 6) Tests complets pour service notification (15 tests, tous passent), 7) Tests complets pour service password (15 tests, tous passent, certains skip car nécessitent PostgreSQL NOW()), 8) Tests complets pour service metadata (14 tests, tous passent), 9) Tests complets pour service backup (15 tests, tous passent, 1 skip car nécessite PostgreSQL pg_dump), 10) Tests complets pour service job (14 tests, tous passent), 11) Tests complets pour service audit (20 tests, tous passent, 2 skip car bug dans service avec UserID nil), 12) Tests complets pour service account_lockout (18 tests, tous passent), 13) Tests complets pour service email (28 tests, tous passent, 1 skip car nécessite DB réelle), 14) Tests complets pour service role (24 tests, tous passent), 15) Hook GORM ajouté dans UserRole.BeforeCreate pour remplir automatiquement RoleName depuis RoleID, 16) Interfaces créées (RBACServiceInterface, UserServiceInterface, DataExportServiceInterface) pour permettre le mock dans les tests, 17) Mock créé pour JobEnqueuer interface, 18) Tests complets pour search_handlers.go (6 tests, tous passent) - interface SearchServiceInterface créée, 19) Tests complets pour comment_handler.go (12 tests, tous passent) - interface CommentServiceInterface créée avec toutes les méthodes nécessaires, 20) Tests complets pour avatar_handler.go (15 tests, tous passent) - interfaces ImageServiceInterface et UserServiceInterfaceForAvatar créées, 21) Tests complets pour notification_handlers.go (14 tests, tous passent) - interface NotificationServiceInterface créée, 22) Couverture actuelle: 30.3% (objectif: 80%). Prochaines étapes: Créer des tests pour les autres handlers critiques (track, playlist, room, message, auth, oauth, password_reset, etc.) et services manquants pour atteindre 80%. Cette tâche nécessite encore environ 4-6 heures de travail pour créer suffisamment de tests.", + "implementation_notes": "Progrès réalisés: 1) Scripts créés pour exécuter les tests par groupes/packages individuels (évite les crashes RAM), 2) Tests complets pour handlers RBAC (16 tests, tous passent), 3) Tests complets pour handlers user (16 tests, tous passent), 4) Tests complets pour service social (18 tests, tous passent), 5) Tests complets pour service cache (20 tests, tous passent), 6) Tests complets pour service notification (15 tests, tous passent), 7) Tests complets pour service password (15 tests, tous passent, certains skip car nécessitent PostgreSQL NOW()), 8) Tests complets pour service metadata (14 tests, tous passent), 9) Tests complets pour service backup (15 tests, tous passent, 1 skip car nécessite PostgreSQL pg_dump), 10) Tests complets pour service job (14 tests, tous passent), 11) Tests complets pour service audit (20 tests, tous passent, 2 skip car bug dans service avec UserID nil), 12) Tests complets pour service account_lockout (18 tests, tous passent), 13) Tests complets pour service email (28 tests, tous passent, 1 skip car nécessite DB réelle), 14) Tests complets pour service role (24 tests, tous passent), 15) Hook GORM ajouté dans UserRole.BeforeCreate pour remplir automatiquement RoleName depuis RoleID, 16) Interfaces créées (RBACServiceInterface, UserServiceInterface, DataExportServiceInterface) pour permettre le mock dans les tests, 17) Mock créé pour JobEnqueuer interface, 18) Tests complets pour search_handlers.go (6 tests, tous passent) - interface SearchServiceInterface créée, 19) Tests complets pour comment_handler.go (12 tests, tous passent) - interface CommentServiceInterface créée avec toutes les méthodes nécessaires, 20) Tests complets pour avatar_handler.go (15 tests, tous passent) - interfaces ImageServiceInterface et UserServiceInterfaceForAvatar créées, 21) Tests complets pour notification_handlers.go (14 tests, tous passent) - interface NotificationServiceInterface créée, 22) Tests complets pour role_handler.go (22 tests, tous passent) - interface RoleServiceInterface créée, 23) Couverture actuelle: 30.3% (objectif: 80%). Prochaines étapes: Créer des tests pour les autres handlers critiques (track, playlist, room, message, auth, oauth, password_reset, settings, etc.) et services manquants pour atteindre 80%. Cette tâche nécessite encore environ 4-6 heures de travail pour créer suffisamment de tests.", "blockers": [] }, { diff --git a/veza-backend-api/coverage_groups/veza-backend-api_internal_config.out b/veza-backend-api/coverage_groups/veza-backend-api_internal_config.out index 783bc795b..4e8308885 100644 --- a/veza-backend-api/coverage_groups/veza-backend-api_internal_config.out +++ b/veza-backend-api/coverage_groups/veza-backend-api_internal_config.out @@ -445,8 +445,8 @@ veza-backend-api/internal/config/watcher.go:91.4,94.29 2 0 veza-backend-api/internal/config/watcher.go:94.29,97.50 3 0 veza-backend-api/internal/config/watcher.go:97.50,99.6 1 0 veza-backend-api/internal/config/watcher.go:99.11,101.6 1 0 -veza-backend-api/internal/config/watcher.go:104.38,105.11 1 0 -veza-backend-api/internal/config/watcher.go:105.11,107.5 1 0 +veza-backend-api/internal/config/watcher.go:104.38,105.11 1 1 +veza-backend-api/internal/config/watcher.go:105.11,107.5 1 1 veza-backend-api/internal/config/watcher.go:108.4,108.51 1 0 veza-backend-api/internal/config/watcher.go:110.21,112.28 1 1 veza-backend-api/internal/config/watcher.go:112.28,114.5 1 0 diff --git a/veza-backend-api/coverage_groups/veza-backend-api_internal_logging.out b/veza-backend-api/coverage_groups/veza-backend-api_internal_logging.out index 8d7580ef4..c2e3e47cc 100644 --- a/veza-backend-api/coverage_groups/veza-backend-api_internal_logging.out +++ b/veza-backend-api/coverage_groups/veza-backend-api_internal_logging.out @@ -56,7 +56,7 @@ veza-backend-api/internal/logging/logger.go:340.24,342.3 1 1 veza-backend-api/internal/logging/logger.go:344.2,345.25 2 1 veza-backend-api/internal/logging/logger.go:349.44,353.6 2 1 veza-backend-api/internal/logging/logger.go:353.6,354.10 1 1 -veza-backend-api/internal/logging/logger.go:355.28,356.40 1 1 +veza-backend-api/internal/logging/logger.go:355.28,356.40 1 0 veza-backend-api/internal/logging/logger.go:357.11,359.60 2 1 veza-backend-api/internal/logging/logger.go:359.60,361.5 1 1 veza-backend-api/internal/logging/logger.go:362.4,362.14 1 1 diff --git a/veza-backend-api/coverage_groups/veza-backend-api_internal_workers.out b/veza-backend-api/coverage_groups/veza-backend-api_internal_workers.out index 2d0697847..80b2b14a7 100644 --- a/veza-backend-api/coverage_groups/veza-backend-api_internal_workers.out +++ b/veza-backend-api/coverage_groups/veza-backend-api_internal_workers.out @@ -77,17 +77,17 @@ veza-backend-api/internal/workers/job_worker.go:94.2,96.32 1 1 veza-backend-api/internal/workers/job_worker.go:100.48,107.43 3 1 veza-backend-api/internal/workers/job_worker.go:107.43,109.3 1 1 veza-backend-api/internal/workers/job_worker.go:113.63,118.45 3 1 -veza-backend-api/internal/workers/job_worker.go:118.45,120.3 1 0 +veza-backend-api/internal/workers/job_worker.go:118.45,120.3 1 1 veza-backend-api/internal/workers/job_worker.go:122.2,122.6 1 1 veza-backend-api/internal/workers/job_worker.go:122.6,123.10 1 1 veza-backend-api/internal/workers/job_worker.go:124.21,125.10 1 1 veza-backend-api/internal/workers/job_worker.go:126.19,127.47 1 0 veza-backend-api/internal/workers/job_worker.go:127.47,129.5 1 0 veza-backend-api/internal/workers/job_worker.go:135.46,150.25 3 1 -veza-backend-api/internal/workers/job_worker.go:150.25,152.3 1 0 -veza-backend-api/internal/workers/job_worker.go:154.2,154.29 1 1 +veza-backend-api/internal/workers/job_worker.go:150.25,152.3 1 1 +veza-backend-api/internal/workers/job_worker.go:154.2,154.29 1 0 veza-backend-api/internal/workers/job_worker.go:154.29,156.3 1 0 -veza-backend-api/internal/workers/job_worker.go:157.2,157.12 1 1 +veza-backend-api/internal/workers/job_worker.go:157.2,157.12 1 0 veza-backend-api/internal/workers/job_worker.go:161.70,167.6 4 1 veza-backend-api/internal/workers/job_worker.go:167.6,168.10 1 1 veza-backend-api/internal/workers/job_worker.go:169.21,171.10 2 1 diff --git a/veza-backend-api/coverage_total.out b/veza-backend-api/coverage_total.out index 854502912..1508f05b4 100644 --- a/veza-backend-api/coverage_total.out +++ b/veza-backend-api/coverage_total.out @@ -1322,8 +1322,8 @@ veza-backend-api/internal/config/watcher.go:91.4,94.29 2 0 veza-backend-api/internal/config/watcher.go:94.29,97.50 3 0 veza-backend-api/internal/config/watcher.go:97.50,99.6 1 0 veza-backend-api/internal/config/watcher.go:99.11,101.6 1 0 -veza-backend-api/internal/config/watcher.go:104.38,105.11 1 0 -veza-backend-api/internal/config/watcher.go:105.11,107.5 1 0 +veza-backend-api/internal/config/watcher.go:104.38,105.11 1 1 +veza-backend-api/internal/config/watcher.go:105.11,107.5 1 1 veza-backend-api/internal/config/watcher.go:108.4,108.51 1 0 veza-backend-api/internal/config/watcher.go:110.21,112.28 1 1 veza-backend-api/internal/config/watcher.go:112.28,114.5 1 0 @@ -3283,7 +3283,7 @@ veza-backend-api/internal/logging/logger.go:340.24,342.3 1 1 veza-backend-api/internal/logging/logger.go:344.2,345.25 2 1 veza-backend-api/internal/logging/logger.go:349.44,353.6 2 1 veza-backend-api/internal/logging/logger.go:353.6,354.10 1 1 -veza-backend-api/internal/logging/logger.go:355.28,356.40 1 1 +veza-backend-api/internal/logging/logger.go:355.28,356.40 1 0 veza-backend-api/internal/logging/logger.go:357.11,359.60 2 1 veza-backend-api/internal/logging/logger.go:359.60,361.5 1 1 veza-backend-api/internal/logging/logger.go:362.4,362.14 1 1 @@ -5693,17 +5693,17 @@ veza-backend-api/internal/workers/job_worker.go:94.2,96.32 1 1 veza-backend-api/internal/workers/job_worker.go:100.48,107.43 3 1 veza-backend-api/internal/workers/job_worker.go:107.43,109.3 1 1 veza-backend-api/internal/workers/job_worker.go:113.63,118.45 3 1 -veza-backend-api/internal/workers/job_worker.go:118.45,120.3 1 0 +veza-backend-api/internal/workers/job_worker.go:118.45,120.3 1 1 veza-backend-api/internal/workers/job_worker.go:122.2,122.6 1 1 veza-backend-api/internal/workers/job_worker.go:122.6,123.10 1 1 veza-backend-api/internal/workers/job_worker.go:124.21,125.10 1 1 veza-backend-api/internal/workers/job_worker.go:126.19,127.47 1 0 veza-backend-api/internal/workers/job_worker.go:127.47,129.5 1 0 veza-backend-api/internal/workers/job_worker.go:135.46,150.25 3 1 -veza-backend-api/internal/workers/job_worker.go:150.25,152.3 1 0 -veza-backend-api/internal/workers/job_worker.go:154.2,154.29 1 1 +veza-backend-api/internal/workers/job_worker.go:150.25,152.3 1 1 +veza-backend-api/internal/workers/job_worker.go:154.2,154.29 1 0 veza-backend-api/internal/workers/job_worker.go:154.29,156.3 1 0 -veza-backend-api/internal/workers/job_worker.go:157.2,157.12 1 1 +veza-backend-api/internal/workers/job_worker.go:157.2,157.12 1 0 veza-backend-api/internal/workers/job_worker.go:161.70,167.6 4 1 veza-backend-api/internal/workers/job_worker.go:167.6,168.10 1 1 veza-backend-api/internal/workers/job_worker.go:169.21,171.10 2 1 diff --git a/veza-backend-api/internal/handlers/role_handler.go b/veza-backend-api/internal/handlers/role_handler.go index 948c31af4..faba1c03b 100644 --- a/veza-backend-api/internal/handlers/role_handler.go +++ b/veza-backend-api/internal/handlers/role_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "net/http" "time" @@ -13,9 +14,22 @@ import ( "go.uber.org/zap" ) +// RoleServiceInterface defines the interface for role operations +// This allows for easier testing with mocks +type RoleServiceInterface interface { + GetRoles(ctx context.Context) ([]models.Role, error) + GetRole(ctx context.Context, roleID uuid.UUID) (*models.Role, error) + CreateRole(ctx context.Context, role *models.Role) error + UpdateRole(ctx context.Context, roleID uuid.UUID, updates *models.Role) error + DeleteRole(ctx context.Context, roleID uuid.UUID) error + AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID, assignedBy uuid.UUID, expiresAt *time.Time) error + RevokeRoleFromUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error + GetUserRoles(ctx context.Context, userID uuid.UUID) ([]models.Role, error) +} + // RoleHandler gère les endpoints de gestion des rôles type RoleHandler struct { - roleService *services.RoleService + roleService RoleServiceInterface commonHandler *CommonHandler } @@ -27,6 +41,14 @@ func NewRoleHandler(roleService *services.RoleService, logger *zap.Logger) *Role } } +// NewRoleHandlerWithInterface crée un nouveau RoleHandler avec une interface (pour les tests) +func NewRoleHandlerWithInterface(roleService RoleServiceInterface, logger *zap.Logger) *RoleHandler { + return &RoleHandler{ + roleService: roleService, + commonHandler: NewCommonHandler(logger), + } +} + // GetRoles récupère tous les rôles // BE-API-007: Implement roles management endpoints func (h *RoleHandler) GetRoles(c *gin.Context) { diff --git a/veza-backend-api/internal/handlers/role_handler_test.go b/veza-backend-api/internal/handlers/role_handler_test.go new file mode 100644 index 000000000..7ba2d6579 --- /dev/null +++ b/veza-backend-api/internal/handlers/role_handler_test.go @@ -0,0 +1,688 @@ +package handlers + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "veza-backend-api/internal/common" + "veza-backend-api/internal/models" + "veza-backend-api/internal/services" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +// MockRoleService implements RoleService interface for testing +type MockRoleService struct { + mock.Mock +} + +func (m *MockRoleService) GetRoles(ctx context.Context) ([]models.Role, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]models.Role), args.Error(1) +} + +func (m *MockRoleService) GetRole(ctx context.Context, roleID uuid.UUID) (*models.Role, error) { + args := m.Called(ctx, roleID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.Role), args.Error(1) +} + +func (m *MockRoleService) CreateRole(ctx context.Context, role *models.Role) error { + args := m.Called(ctx, role) + return args.Error(0) +} + +func (m *MockRoleService) UpdateRole(ctx context.Context, roleID uuid.UUID, updates *models.Role) error { + args := m.Called(ctx, roleID, updates) + return args.Error(0) +} + +func (m *MockRoleService) DeleteRole(ctx context.Context, roleID uuid.UUID) error { + args := m.Called(ctx, roleID) + return args.Error(0) +} + +func (m *MockRoleService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID, assignedBy uuid.UUID, expiresAt *time.Time) error { + args := m.Called(ctx, userID, roleID, assignedBy, expiresAt) + return args.Error(0) +} + +func (m *MockRoleService) RevokeRoleFromUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error { + args := m.Called(ctx, userID, roleID) + return args.Error(0) +} + +func (m *MockRoleService) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]models.Role, error) { + args := m.Called(ctx, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]models.Role), args.Error(1) +} + +func setupTestRoleRouter(mockService *MockRoleService) *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.New() + + logger := zap.NewNop() + handler := NewRoleHandlerWithInterface(mockService, logger) + + api := router.Group("/api/v1") + api.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 { + common.SetUserIDInContext(c, uid) + } + } + c.Next() + }) + { + api.GET("/roles", handler.GetRoles) + api.GET("/roles/:id", handler.GetRole) + api.POST("/roles", handler.CreateRole) + api.PUT("/roles/:id", handler.UpdateRole) + api.DELETE("/roles/:id", handler.DeleteRole) + api.POST("/users/:userId/roles", handler.AssignRole) + api.DELETE("/users/:userId/roles/:roleId", handler.RevokeRole) + api.GET("/users/:id/roles", handler.GetUserRoles) + } + + return router +} + +func TestRoleHandler_GetRoles_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + expectedRoles := []models.Role{ + { + ID: uuid.New(), + Name: "admin", + Description: "Administrator role", + IsSystem: true, + }, + { + ID: uuid.New(), + Name: "user", + Description: "Regular user role", + IsSystem: true, + }, + } + + mockService.On("GetRoles", mock.Anything).Return(expectedRoles, nil) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/roles", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_GetRoles_ServiceError(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + mockService.On("GetRoles", mock.Anything).Return(nil, assert.AnError) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/roles", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_GetRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + expectedRole := &models.Role{ + ID: roleID, + Name: "admin", + Description: "Administrator role", + IsSystem: true, + } + + mockService.On("GetRole", mock.Anything, roleID).Return(expectedRole, nil) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_GetRole_InvalidID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/roles/invalid", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "GetRole") +} + +func TestRoleHandler_GetRole_NotFound(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + mockService.On("GetRole", mock.Anything, roleID).Return(nil, assert.AnError) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_CreateRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + role := models.Role{ + Name: "moderator", + Description: "Moderator role", + IsSystem: false, + } + + mockService.On("CreateRole", mock.Anything, &role).Return(nil) + + body, _ := json.Marshal(role) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/roles", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusCreated, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_CreateRole_InvalidJSON(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/roles", bytes.NewBuffer([]byte("invalid json"))) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "CreateRole") +} + +func TestRoleHandler_CreateRole_ServiceError(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + role := models.Role{ + Name: "moderator", + Description: "Moderator role", + IsSystem: false, + } + + mockService.On("CreateRole", mock.Anything, &role).Return(assert.AnError) + + body, _ := json.Marshal(role) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/roles", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_UpdateRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + updates := models.Role{ + Name: "updated_moderator", + Description: "Updated moderator role", + } + + mockService.On("UpdateRole", mock.Anything, roleID, &updates).Return(nil) + + body, _ := json.Marshal(updates) + + // Execute + req, _ := http.NewRequest("PUT", "/api/v1/roles/"+roleID.String(), bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_UpdateRole_InvalidID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + updates := models.Role{ + Name: "updated_moderator", + } + body, _ := json.Marshal(updates) + + // Execute + req, _ := http.NewRequest("PUT", "/api/v1/roles/invalid", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "UpdateRole") +} + +func TestRoleHandler_UpdateRole_NotFound(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + updates := models.Role{ + Name: "updated_moderator", + } + + mockService.On("UpdateRole", mock.Anything, roleID, &updates).Return(assert.AnError) + + body, _ := json.Marshal(updates) + + // Execute + req, _ := http.NewRequest("PUT", "/api/v1/roles/"+roleID.String(), bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_DeleteRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + mockService.On("DeleteRole", mock.Anything, roleID).Return(nil) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_DeleteRole_InvalidID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/roles/invalid", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "DeleteRole") +} + +func TestRoleHandler_DeleteRole_NotFound(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + // The handler returns 400 for "role not found" or "cannot delete system role", but 500 for other errors + mockService.On("DeleteRole", mock.Anything, roleID).Return(assert.AnError) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert - handler returns 500 for generic errors, 400 only for specific error messages + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_DeleteRole_RoleNotFoundMessage(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + // Mock error with specific message that handler checks for 400 + mockService.On("DeleteRole", mock.Anything, roleID).Return(&mockError{message: "role not found"}) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert - handler checks error.Error() == "role not found" for 400 + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_DeleteRole_SystemRole(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + // Mock error with specific message that handler checks for 400 + mockService.On("DeleteRole", mock.Anything, roleID).Return(&mockError{message: "cannot delete system role"}) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert - handler checks error.Error() == "cannot delete system role" for 400 + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertExpectations(t) +} + +// mockError is a simple error type for testing +type mockError struct { + message string +} + +func (e *mockError) Error() string { + return e.message +} + +func TestRoleHandler_AssignRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + roleID := uuid.New() + assignedBy := uuid.New() + + reqBody := map[string]interface{}{ + "role_id": roleID.String(), + } + body, _ := json.Marshal(reqBody) + + mockService.On("AssignRoleToUser", mock.Anything, userID, roleID, assignedBy, (*time.Time)(nil)).Return(nil) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/roles", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", assignedBy.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_AssignRole_Unauthorized(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + roleID := uuid.New() + + reqBody := map[string]interface{}{ + "role_id": roleID.String(), + } + body, _ := json.Marshal(reqBody) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/roles", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden) + mockService.AssertNotCalled(t, "AssignRoleToUser") +} + +func TestRoleHandler_AssignRole_InvalidUserID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + assignedBy := uuid.New() + + reqBody := map[string]interface{}{ + "role_id": roleID.String(), + } + body, _ := json.Marshal(reqBody) + + // Execute + req, _ := http.NewRequest("POST", "/api/v1/users/invalid/roles", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", assignedBy.String()) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "AssignRoleToUser") +} + +func TestRoleHandler_RevokeRole_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + roleID := uuid.New() + + mockService.On("RevokeRoleFromUser", mock.Anything, userID, roleID).Return(nil) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_RevokeRole_InvalidUserID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + roleID := uuid.New() + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/users/invalid/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "RevokeRoleFromUser") +} + +func TestRoleHandler_RevokeRole_InvalidRoleID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/roles/invalid", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "RevokeRoleFromUser") +} + +func TestRoleHandler_RevokeRole_NotFound(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + roleID := uuid.New() + + mockService.On("RevokeRoleFromUser", mock.Anything, userID, roleID).Return(assert.AnError) + + // Execute + req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/roles/"+roleID.String(), nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_GetUserRoles_Success(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + expectedRoles := []models.Role{ + { + ID: uuid.New(), + Name: "admin", + }, + { + ID: uuid.New(), + Name: "user", + }, + } + + mockService.On("GetUserRoles", mock.Anything, userID).Return(expectedRoles, nil) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/users/"+userID.String()+"/roles", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusOK, w.Code) + mockService.AssertExpectations(t) +} + +func TestRoleHandler_GetUserRoles_InvalidID(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/users/invalid/roles", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusBadRequest, w.Code) + mockService.AssertNotCalled(t, "GetUserRoles") +} + +func TestRoleHandler_GetUserRoles_ServiceError(t *testing.T) { + // Setup + mockService := new(MockRoleService) + router := setupTestRoleRouter(mockService) + + userID := uuid.New() + + mockService.On("GetUserRoles", mock.Anything, userID).Return(nil, assert.AnError) + + // Execute + req, _ := http.NewRequest("GET", "/api/v1/users/"+userID.String()+"/roles", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assert + assert.Equal(t, http.StatusInternalServerError, w.Code) + mockService.AssertExpectations(t) +} + +func TestNewRoleHandler(t *testing.T) { + // Setup + mockService := &services.RoleService{} + logger := zap.NewNop() + + // Execute + handler := NewRoleHandler(mockService, logger) + + // Assert + assert.NotNil(t, handler) + assert.NotNil(t, handler.roleService) + assert.NotNil(t, handler.commonHandler) +} +