//go:build integration // +build integration package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "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" ) // setupProfileIntegrationTestRouter crée un router de test pour les tests de profile // BE-SEC-001: Tests d'intégration pour vérification ownership func setupProfileIntegrationTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, func()) { gin.SetMode(gin.TestMode) // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Enable foreign keys for SQLite db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate - include all models needed for PermissionService err = db.AutoMigrate( &models.User{}, &models.Role{}, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, ) require.NoError(t, err) // Setup logger logger := zap.NewNop() // Setup services userRepo := repositories.NewGormUserRepository(db) userService := services.NewUserServiceWithDB(userRepo, db) profileHandler := NewProfileHandler(userService, logger) // Setup PermissionService for admin checks permissionService := services.NewPermissionService(db) profileHandler.SetPermissionService(permissionService) // Create router router := gin.New() v1 := router.Group("/api/v1") { // Protected routes with mock auth middleware protected := v1.Group("/users") protected.Use(func(c *gin.Context) { // Mock auth middleware - set user_id from header // This simulates RequireAuth() middleware userIDStr := c.GetHeader("X-User-ID") if userIDStr == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) c.Abort() return } uid, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user id"}) c.Abort() return } c.Set("user_id", uid) c.Next() }) { // Note: In real router, RequireOwnershipOrAdmin middleware would be here // For this test, we're testing the handler's ownership check directly protected.PUT("/:id", profileHandler.UpdateProfile) } } cleanup := func() { // Database will be closed automatically } return router, db, cleanup } // createTestUser crée un utilisateur de test func createTestUserForProfile(t *testing.T, db *gorm.DB, userID uuid.UUID, username string, isAdmin bool) *models.User { user := &models.User{ ID: userID, Username: username, Slug: username, Email: username + "@example.com", PasswordHash: "hashed_password", IsActive: true, CreatedAt: time.Now(), } if isAdmin { user.Role = "admin" user.IsAdmin = true } else { user.Role = "user" } // Create user first err := db.Create(user).Error require.NoError(t, err) // Then create admin role and assign it if admin if isAdmin { // Create admin role and assign it to user adminRole := &models.Role{ Name: "admin", DisplayName: "Administrator", IsSystem: true, IsActive: true, } err = db.FirstOrCreate(adminRole, models.Role{Name: "admin"}).Error require.NoError(t, err) userRole := &models.UserRole{ UserID: userID, RoleID: adminRole.ID, RoleName: "admin", IsActive: true, } err = db.Create(userRole).Error require.NoError(t, err) } return user } // BE-SEC-001: Test that user cannot update another user's profile func TestUpdateProfile_UserCannotUpdateOtherUserProfile(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupProfileIntegrationTestRouter(t) defer cleanup() // Create two users ownerID := uuid.New() otherUserID := uuid.New() createTestUserForProfile(t, db, ownerID, "owner", false) createTestUserForProfile(t, db, otherUserID, "otheruser", false) // Try to update owner's profile as otherUser updateReq := UpdateProfileRequest{ FirstName: "Hacked", Bio: "I shouldn't be able to do this", } body, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+ownerID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", otherUserID.String()) // otherUser is authenticated w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should return 403 Forbidden assert.Equal(t, http.StatusForbidden, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) // Error can be a string or a map, check both if errorStr, ok := response["error"].(string); ok { assert.Contains(t, errorStr, "cannot update other user's profile") } else if errorMap, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorMap["message"].(string); ok { assert.Contains(t, message, "cannot update other user's profile") } } } // BE-SEC-001: Test that admin can update any profile func TestUpdateProfile_AdminCanUpdateAnyProfile(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupProfileIntegrationTestRouter(t) defer cleanup() // Create two users: owner and admin ownerID := uuid.New() adminID := uuid.New() createTestUserForProfile(t, db, ownerID, "owner", false) createTestUserForProfile(t, db, adminID, "admin", true) // Admin tries to update owner's profile updateReq := UpdateProfileRequest{ FirstName: "Updated by Admin", Bio: "Admin updated this profile", } body, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+ownerID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", adminID.String()) // admin is authenticated w := httptest.NewRecorder() router.ServeHTTP(w, req) // Admin should be able to update assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.NotNil(t, response["data"]) } // BE-SEC-001: Test that user can update their own profile func TestUpdateProfile_UserCanUpdateOwnProfile(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupProfileIntegrationTestRouter(t) defer cleanup() // Create a user userID := uuid.New() createTestUserForProfile(t, db, userID, "testuser", false) // User tries to update their own profile updateReq := UpdateProfileRequest{ FirstName: "Updated First Name", Bio: "Updated bio", } body, _ := json.Marshal(updateReq) req := httptest.NewRequest(http.MethodPut, "/api/v1/users/"+userID.String(), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) // user is authenticated w := httptest.NewRecorder() router.ServeHTTP(w, req) // User should be able to update their own profile assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.NotNil(t, response["data"]) // Verify the profile was actually updated profileData := response["data"].(map[string]interface{}) profile := profileData["profile"].(map[string]interface{}) assert.Equal(t, "Updated First Name", profile["first_name"]) }