package user import ( "bytes" "context" "database/sql" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/common" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) // MockUserService is a mock implementation of Service for testing type MockUserService struct { mock.Mock } func (m *MockUserService) GetUserByID(userID uuid.UUID) (*UserResponse, error) { args := m.Called(userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*UserResponse), args.Error(1) } func (m *MockUserService) GetUsers(page, limit int, search string) ([]UserResponse, int, error) { args := m.Called(page, limit, search) if args.Get(0) == nil { return nil, args.Int(1), args.Error(2) } return args.Get(0).([]UserResponse), args.Int(1), args.Error(2) } func (m *MockUserService) UpdateUser(userID uuid.UUID, req UpdateUserRequest) (*UserResponse, error) { args := m.Called(userID, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*UserResponse), args.Error(1) } func (m *MockUserService) ChangePassword(userID uuid.UUID, currentPassword, newPassword string) error { args := m.Called(userID, currentPassword, newPassword) return args.Error(0) } func (m *MockUserService) GetUserPreferences(userID uuid.UUID) (*UserPreferencesResponse, error) { args := m.Called(userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*UserPreferencesResponse), args.Error(1) } func (m *MockUserService) UpdateUserPreferences(userID uuid.UUID, req UserPreferencesRequest) (*UserPreferencesResponse, error) { args := m.Called(userID, req) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*UserPreferencesResponse), args.Error(1) } func (m *MockUserService) DeleteAccount(userID uuid.UUID, password, reason string) error { args := m.Called(userID, password, reason) return args.Error(0) } func (m *MockUserService) RecoverAccount(email, password string) error { args := m.Called(email, password) return args.Error(0) } func (m *MockUserService) RequestDataDeletion(userID uuid.UUID, password, reason string) error { args := m.Called(userID, password, reason) return args.Error(0) } func (m *MockUserService) GetAccountStatus(userID uuid.UUID) (*AccountStatus, error) { args := m.Called(userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*AccountStatus), args.Error(1) } // MockDataExportService is a mock implementation of DataExportService for testing type MockDataExportService struct { mock.Mock } func (m *MockDataExportService) ExportUserDataAsJSON(ctx context.Context, userID uuid.UUID) ([]byte, error) { args := m.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]byte), args.Error(1) } // setupTestUserRouter creates a test router with user handlers func setupTestUserRouter(mockService *MockUserService, mockDataExportService *MockDataExportService) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() handler := NewHandlerWithInterfaces(mockService, mockDataExportService) 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.GET("/me", handler.GetMe) users.PATCH("/me", handler.UpdateMe) users.POST("/me/password", handler.ChangePassword) users.GET("", handler.GetUsers) users.GET("/except-me", handler.GetUsersExceptMe) users.GET("/search", handler.SearchUsers) users.GET("/:id/avatar", handler.GetUserAvatar) users.GET("/me/preferences", handler.GetPreferences) users.PATCH("/me/preferences", handler.UpdatePreferences) users.DELETE("/me", handler.DeleteAccount) users.POST("/recover", handler.RecoverAccount) users.GET("/me/export", handler.ExportData) users.POST("/me/delete-request", handler.RequestDataDeletion) users.GET("/me/status", handler.GetAccountStatus) } return router } func TestUserHandler_GetMe_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() expectedUser := &UserResponse{ ID: userID, Email: "test@example.com", Username: "testuser", IsActive: true, IsVerified: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } mockService.On("GetUserByID", userID).Return(expectedUser, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) } func TestUserHandler_GetMe_Unauthorized(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me", nil) // No X-User-ID header router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestUserHandler_GetMe_NotFound(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() mockService.On("GetUserByID", userID).Return(nil, assert.AnError) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_UpdateMe_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() username := "newusername" reqBody := UpdateUserRequest{ Username: &username, } expectedUser := &UserResponse{ ID: userID, Username: username, Email: "test@example.com", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } mockService.On("UpdateUser", userID, reqBody).Return(expectedUser, nil) body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/users/me", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) } func TestUserHandler_ChangePassword_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() reqBody := map[string]interface{}{ "current_password": "oldpass123", "new_password": "newpass123", } mockService.On("ChangePassword", userID, "oldpass123", "newpass123").Return(nil) body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/users/me/password", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_ChangePassword_InvalidRequest(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() reqBody := map[string]interface{}{ "current_password": "oldpass123", // Missing new_password } body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/users/me/password", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestUserHandler_GetUsers_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) expectedUsers := []UserResponse{ { ID: uuid.New(), Username: "user1", Email: "user1@example.com", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, { ID: uuid.New(), Username: "user2", Email: "user2@example.com", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, } mockService.On("GetUsers", 1, 20, "").Return(expectedUsers, 2, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users?page=1&limit=20", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) } func TestUserHandler_SearchUsers_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) query := "test" expectedUsers := []UserResponse{ { ID: uuid.New(), Username: "testuser", Email: "test@example.com", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, } mockService.On("GetUsers", 1, 20, query).Return(expectedUsers, 1, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/search?q=test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_SearchUsers_MissingQuery(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/search", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestUserHandler_GetUserAvatar_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() avatarURL := "https://example.com/avatar.jpg" expectedUser := &UserResponse{ ID: userID, Username: "testuser", Email: "test@example.com", Avatar: sql.NullString{ String: avatarURL, Valid: true, }, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } mockService.On("GetUserByID", userID).Return(expectedUser, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/"+userID.String()+"/avatar", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusFound, w.Code) assert.Equal(t, avatarURL, w.Header().Get("Location")) mockService.AssertExpectations(t) } func TestUserHandler_GetUserAvatar_NoAvatar(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() expectedUser := &UserResponse{ ID: userID, Username: "testuser", Email: "test@example.com", Avatar: sql.NullString{ Valid: false, }, IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } mockService.On("GetUserByID", userID).Return(expectedUser, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/"+userID.String()+"/avatar", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_GetPreferences_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() expectedPreferences := &UserPreferencesResponse{ UserID: userID, Theme: "dark", Language: "en", Timezone: "UTC", Notifications: NotificationSettings{ Email: true, Push: false, }, Privacy: PrivacySettings{ ShowEmail: false, }, Audio: AudioSettings{ AutoPlay: true, Quality: "high", Volume: 0.8, }, UpdatedAt: time.Now(), } mockService.On("GetUserPreferences", userID).Return(expectedPreferences, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me/preferences", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) } func TestUserHandler_DeleteAccount_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() reqBody := map[string]interface{}{ "password": "password123", "reason": "No longer needed", "confirm_text": "DELETE", } mockService.On("DeleteAccount", userID, "password123", "No longer needed").Return(nil) body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/users/me", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_DeleteAccount_InvalidConfirmText(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() reqBody := map[string]interface{}{ "password": "password123", "reason": "No longer needed", "confirm_text": "WRONG", } body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/users/me", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestUserHandler_ExportData_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() exportData := []byte(`{"user_id":"` + userID.String() + `","profile":{}}`) mockDataExportService.On("ExportUserDataAsJSON", mock.Anything, userID).Return(exportData, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me/export", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Header().Get("Content-Type"), "application/json") assert.Contains(t, w.Header().Get("Content-Disposition"), "attachment") mockDataExportService.AssertExpectations(t) } func TestUserHandler_GetAccountStatus_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() expectedStatus := &AccountStatus{ UserID: userID, Status: "active", IsActive: true, IsVerified: true, CreatedAt: time.Now(), } mockService.On("GetAccountStatus", userID).Return(expectedStatus, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/me/status", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) assert.True(t, response["success"].(bool)) } func TestUserHandler_RecoverAccount_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) reqBody := map[string]interface{}{ "email": "test@example.com", "password": "password123", } mockService.On("RecoverAccount", "test@example.com", "password123").Return(nil) body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/users/recover", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_RequestDataDeletion_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() reqBody := map[string]interface{}{ "password": "password123", "reason": "GDPR", } mockService.On("RequestDataDeletion", userID, "password123", "GDPR").Return(nil) body, _ := json.Marshal(reqBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/users/me/delete-request", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) } func TestUserHandler_GetUsersExceptMe_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() otherUserID := uuid.New() users := []UserResponse{ {ID: userID, Username: "me"}, {ID: otherUserID, Username: "other"}, } // Mock returns both users mockService.On("GetUsers", 1, 20, "").Return(users, 2, nil) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/users/except-me", nil) req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) // Verify data only contains "other" user // response["data"] is the payload map (gin.H) payload := response["data"].(map[string]interface{}) // payload["data"] is the users list data := payload["data"].([]interface{}) assert.Len(t, data, 1) assert.Equal(t, "other", data[0].(map[string]interface{})["username"]) } func TestUserHandler_UpdatePreferences_Success(t *testing.T) { mockService := new(MockUserService) mockDataExportService := new(MockDataExportService) router := setupTestUserRouter(mockService, mockDataExportService) userID := uuid.New() theme := "light" language := "fr" reqPreference := UserPreferencesRequest{ Theme: &theme, Language: &language, } expectedResponse := &UserPreferencesResponse{ UserID: userID, Theme: "light", Language: "fr", } mockService.On("UpdateUserPreferences", userID, reqPreference).Return(expectedResponse, nil) body, _ := json.Marshal(reqPreference) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/users/me/preferences", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) mockService.AssertExpectations(t) }