veza/veza-backend-api/internal/handlers/avatar_handler_test.go

478 lines
15 KiB
Go

package handlers
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"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"
)
// MockImageService implements ImageService interface for testing
type MockImageService struct {
mock.Mock
}
func (m *MockImageService) ProcessAvatar(fileHeader *multipart.FileHeader) ([]byte, error) {
args := m.Called(fileHeader)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockImageService) GenerateS3Key(userID uuid.UUID) string {
args := m.Called(userID)
return args.String(0)
}
func (m *MockImageService) UploadToS3(data []byte, key string) (string, error) {
args := m.Called(data, key)
return args.String(0), args.Error(1)
}
func (m *MockImageService) DeleteFromS3(avatarURL string) error {
args := m.Called(avatarURL)
return args.Error(0)
}
// MockUserService implements UserService interface for testing
type MockUserServiceForAvatar struct {
mock.Mock
}
func (m *MockUserServiceForAvatar) GetByID(userID uuid.UUID) (*models.User, error) {
args := m.Called(userID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func (m *MockUserServiceForAvatar) UpdateAvatarURL(userID uuid.UUID, avatarURL string) error {
args := m.Called(userID, avatarURL)
return args.Error(0)
}
func setupTestAvatarRouter(mockImageService *MockImageService, mockUserService *MockUserServiceForAvatar) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewAvatarHandlerWithInterface(mockImageService, mockUserService)
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()
})
{
users := api.Group("/users")
{
users.POST("/:userId/avatar", handler.UploadAvatar)
users.DELETE("/:userId/avatar", handler.DeleteAvatar)
}
}
return router
}
func createMultipartFormData(t *testing.T, fieldName, filename string, content []byte) (*bytes.Buffer, string) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(fieldName, filename)
assert.NoError(t, err)
_, err = part.Write(content)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
return body, writer.FormDataContentType()
}
func TestAvatarHandler_UploadAvatar_Success(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
imageData := []byte("fake image data")
s3Key := "avatars/" + userID.String() + ".jpg"
avatarURL := "https://s3.example.com/" + s3Key
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Mock expectations
mockImageService.On("ProcessAvatar", mock.AnythingOfType("*multipart.FileHeader")).Return(imageData, nil)
mockImageService.On("GenerateS3Key", userID).Return(s3Key)
mockImageService.On("UploadToS3", imageData, s3Key).Return(avatarURL, nil)
mockUserService.On("UpdateAvatarURL", userID, avatarURL).Return(nil)
// Execute
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/avatar", body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockImageService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestAvatarHandler_UploadAvatar_Unauthorized(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
imageData := []byte("fake image data")
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Execute
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/avatar", body)
req.Header.Set("Content-Type", contentType)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden)
mockImageService.AssertNotCalled(t, "ProcessAvatar")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_UploadAvatar_InvalidUserID(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
imageData := []byte("fake image data")
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Execute
req, _ := http.NewRequest("POST", "/api/v1/users/invalid/avatar", body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockImageService.AssertNotCalled(t, "ProcessAvatar")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_UploadAvatar_Forbidden(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID1 := uuid.New()
userID2 := uuid.New()
imageData := []byte("fake image data")
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Execute - userID1 trying to upload avatar for userID2
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID2.String()+"/avatar", body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-User-ID", userID1.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusForbidden, w.Code)
mockImageService.AssertNotCalled(t, "ProcessAvatar")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_UploadAvatar_NoFile(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
// Execute - no file in request
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/avatar", bytes.NewBuffer([]byte{}))
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockImageService.AssertNotCalled(t, "ProcessAvatar")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_UploadAvatar_ProcessAvatarError(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
imageData := []byte("fake image data")
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Mock expectations - ProcessAvatar returns error
mockImageService.On("ProcessAvatar", mock.AnythingOfType("*multipart.FileHeader")).Return(nil, assert.AnError)
// Execute
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/avatar", body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockImageService.AssertExpectations(t)
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_UploadAvatar_UploadToS3Error(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
imageData := []byte("fake image data")
s3Key := "avatars/" + userID.String() + ".jpg"
body, contentType := createMultipartFormData(t, "avatar", "avatar.jpg", imageData)
// Mock expectations - UploadToS3 returns error
mockImageService.On("ProcessAvatar", mock.AnythingOfType("*multipart.FileHeader")).Return(imageData, nil)
mockImageService.On("GenerateS3Key", userID).Return(s3Key)
mockImageService.On("UploadToS3", imageData, s3Key).Return("", assert.AnError)
// Execute
req, _ := http.NewRequest("POST", "/api/v1/users/"+userID.String()+"/avatar", body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, w.Code)
mockImageService.AssertExpectations(t)
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_DeleteAvatar_Success(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
avatarURL := "https://s3.example.com/avatars/" + userID.String() + ".jpg"
user := &models.User{
ID: userID,
Avatar: avatarURL,
}
// Mock expectations
mockUserService.On("GetByID", userID).Return(user, nil)
mockImageService.On("DeleteFromS3", avatarURL).Return(nil)
mockUserService.On("UpdateAvatarURL", userID, "").Return(nil)
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/avatar", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockImageService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestAvatarHandler_DeleteAvatar_Unauthorized(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/avatar", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusForbidden)
mockImageService.AssertNotCalled(t, "DeleteFromS3")
mockUserService.AssertNotCalled(t, "GetByID")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_DeleteAvatar_InvalidUserID(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/invalid/avatar", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusBadRequest, w.Code)
mockImageService.AssertNotCalled(t, "DeleteFromS3")
mockUserService.AssertNotCalled(t, "GetByID")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_DeleteAvatar_Forbidden(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID1 := uuid.New()
userID2 := uuid.New()
// Execute - userID1 trying to delete avatar for userID2
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID2.String()+"/avatar", nil)
req.Header.Set("X-User-ID", userID1.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusForbidden, w.Code)
mockImageService.AssertNotCalled(t, "DeleteFromS3")
mockUserService.AssertNotCalled(t, "GetByID")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_DeleteAvatar_UserNotFound(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
// Mock expectations - GetByID returns error
mockUserService.On("GetByID", userID).Return(nil, assert.AnError)
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/avatar", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusNotFound, w.Code)
mockUserService.AssertExpectations(t)
mockImageService.AssertNotCalled(t, "DeleteFromS3")
mockUserService.AssertNotCalled(t, "UpdateAvatarURL")
}
func TestAvatarHandler_DeleteAvatar_NoAvatar(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
user := &models.User{
ID: userID,
Avatar: "", // No avatar
}
// Mock expectations
mockUserService.On("GetByID", userID).Return(user, nil)
mockUserService.On("UpdateAvatarURL", userID, "").Return(nil)
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/avatar", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockUserService.AssertExpectations(t)
// DeleteFromS3 should not be called if avatar is empty
mockImageService.AssertNotCalled(t, "DeleteFromS3")
}
func TestAvatarHandler_DeleteAvatar_UpdateAvatarURLError(t *testing.T) {
// Setup
mockImageService := new(MockImageService)
mockUserService := new(MockUserServiceForAvatar)
router := setupTestAvatarRouter(mockImageService, mockUserService)
userID := uuid.New()
avatarURL := "https://s3.example.com/avatars/" + userID.String() + ".jpg"
user := &models.User{
ID: userID,
Avatar: avatarURL,
}
// Mock expectations - UpdateAvatarURL returns error
mockUserService.On("GetByID", userID).Return(user, nil)
mockImageService.On("DeleteFromS3", avatarURL).Return(nil)
mockUserService.On("UpdateAvatarURL", userID, "").Return(assert.AnError)
// Execute
req, _ := http.NewRequest("DELETE", "/api/v1/users/"+userID.String()+"/avatar", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, w.Code)
mockImageService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestNewAvatarHandler(t *testing.T) {
// Setup
mockImageService := &services.ImageService{}
mockUserService := &services.UserService{}
// Execute
handler := NewAvatarHandler(mockImageService, mockUserService)
// Assert
assert.NotNil(t, handler)
assert.NotNil(t, handler.imageService)
assert.NotNil(t, handler.userService)
}