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) }