- Tests complets pour avatar_handler.go (15 tests)
- Tests complets pour notification_handlers.go (14 tests)
- Interfaces créées pour permettre le mock (ImageServiceInterface, UserServiceInterfaceForAvatar, NotificationServiceInterface)
- Couverture actuelle: 30.3% (objectif: 80%)
Files: veza-backend-api/internal/handlers/avatar_handler.go
veza-backend-api/internal/handlers/avatar_handler_test.go
veza-backend-api/internal/handlers/notification_handlers.go
veza-backend-api/internal/handlers/notification_handlers_test.go
VEZA_ROADMAP.json
Hours: 16 estimated, 18 actual
165 lines
5.2 KiB
Go
165 lines
5.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"mime/multipart"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services"
|
|
)
|
|
|
|
// ImageServiceInterface defines the interface for image operations
|
|
// This allows for easier testing with mocks
|
|
type ImageServiceInterface interface {
|
|
ProcessAvatar(fileHeader *multipart.FileHeader) ([]byte, error)
|
|
GenerateS3Key(userID uuid.UUID) string
|
|
UploadToS3(data []byte, key string) (string, error)
|
|
DeleteFromS3(avatarURL string) error
|
|
}
|
|
|
|
// UserServiceInterfaceForAvatar defines the interface for user operations needed by avatar handler
|
|
// This allows for easier testing with mocks
|
|
type UserServiceInterfaceForAvatar interface {
|
|
GetByID(userID uuid.UUID) (*models.User, error)
|
|
UpdateAvatarURL(userID uuid.UUID, avatarURL string) error
|
|
}
|
|
|
|
// AvatarHandler handles avatar-related operations
|
|
type AvatarHandler struct {
|
|
imageService ImageServiceInterface
|
|
userService UserServiceInterfaceForAvatar
|
|
}
|
|
|
|
// NewAvatarHandler creates a new AvatarHandler instance
|
|
func NewAvatarHandler(imageService *services.ImageService, userService *services.UserService) *AvatarHandler {
|
|
return &AvatarHandler{
|
|
imageService: imageService,
|
|
userService: userService,
|
|
}
|
|
}
|
|
|
|
// NewAvatarHandlerWithInterface creates a new AvatarHandler with interfaces (for testing)
|
|
func NewAvatarHandlerWithInterface(imageService ImageServiceInterface, userService UserServiceInterfaceForAvatar) *AvatarHandler {
|
|
return &AvatarHandler{
|
|
imageService: imageService,
|
|
userService: userService,
|
|
}
|
|
}
|
|
|
|
// UploadAvatar handles avatar upload
|
|
// POST /api/v1/users/:userId/avatar
|
|
// BE-API-021: Implement avatar upload endpoint
|
|
// T0221: Validates user_id, file format/size, processes image, uploads to S3, and updates DB
|
|
func (h *AvatarHandler) UploadAvatar(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
|
|
userIDStr := c.Param("userId")
|
|
if userIDStr == "" {
|
|
userIDStr = c.Param("id")
|
|
}
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
|
return
|
|
}
|
|
|
|
// Vérifier que l'utilisateur est authentifié
|
|
authenticatedUserID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Vérifier que l'utilisateur ne peut modifier que son propre avatar
|
|
if userID != authenticatedUserID {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("cannot update other user's avatar"))
|
|
return
|
|
}
|
|
|
|
// Récupérer le fichier
|
|
fileHeader, err := c.FormFile("avatar")
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("no file provided"))
|
|
return
|
|
}
|
|
|
|
// Valider et traiter l'image
|
|
resizedImage, err := h.imageService.ProcessAvatar(fileHeader)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Générer la clé S3
|
|
s3Key := h.imageService.GenerateS3Key(userID)
|
|
|
|
// Upload vers S3 (ou stockage local pour l'instant)
|
|
avatarURL, err := h.imageService.UploadToS3(resizedImage, s3Key)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to upload avatar", err))
|
|
return
|
|
}
|
|
|
|
// Mettre à jour l'URL de l'avatar dans la DB
|
|
if err := h.userService.UpdateAvatarURL(userID, avatarURL); err != nil {
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update avatar", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"avatar_url": avatarURL})
|
|
}
|
|
|
|
// DeleteAvatar handles avatar deletion
|
|
// DELETE /api/v1/users/:userId/avatar
|
|
// BE-API-022: Implement avatar delete endpoint
|
|
// T0222: Validates user_id, deletes file from S3, and sets avatar_url to NULL in DB
|
|
func (h *AvatarHandler) DeleteAvatar(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur depuis l'URL (peut être "id" ou "userId")
|
|
userIDStr := c.Param("userId")
|
|
if userIDStr == "" {
|
|
userIDStr = c.Param("id")
|
|
}
|
|
userID, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
|
return
|
|
}
|
|
|
|
// Vérifier que l'utilisateur est authentifié
|
|
authenticatedUserID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Vérifier que l'utilisateur ne peut supprimer que son propre avatar
|
|
if userID != authenticatedUserID {
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("cannot delete other user's avatar"))
|
|
return
|
|
}
|
|
|
|
// Récupérer l'utilisateur actuel pour obtenir l'URL de l'avatar
|
|
user, err := h.userService.GetByID(userID)
|
|
if err != nil {
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
|
|
return
|
|
}
|
|
|
|
// Supprimer le fichier de S3 (ou stockage local) s'il existe
|
|
if user.Avatar != "" {
|
|
if err := h.imageService.DeleteFromS3(user.Avatar); err != nil {
|
|
// Logger l'erreur mais continuer (le fichier peut déjà être supprimé)
|
|
// En production, vous pourriez vouloir utiliser un logger ici
|
|
_ = err
|
|
}
|
|
}
|
|
|
|
// Mettre l'URL de l'avatar à une chaîne vide (NULL dans la DB)
|
|
if err := h.userService.UpdateAvatarURL(userID, ""); err != nil {
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete avatar", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "avatar deleted"})
|
|
}
|