- Added routes in router.go: POST, GET, PUT, DELETE /playlists/:id/collaborators - Applied RequireOwnershipOrAdmin middleware to POST, PUT, DELETE routes - GET route accessible to collaborators (service layer checks permissions) - Fixed UpdateCollaboratorPermission handler to use RespondWithAppError - All handlers already existed in playlist_handler.go - All endpoints properly authenticated and ownership checks enforced Phase: PHASE-1 Priority: P0 Progress: 5/267 (1.9%)
266 lines
7.5 KiB
Go
266 lines
7.5 KiB
Go
//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"])
|
|
}
|