package services import ( "bytes" "context" "errors" "mime/multipart" "os" "strings" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" "veza-backend-api/internal/types" ) // ========== MOCK REPOSITORY ========== type MockUserRepository struct { mock.Mock } func (m *MockUserRepository) GetByID(_ context.Context, id string) (*models.User, error) { args := m.Called(id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.User), args.Error(1) } func (m *MockUserRepository) GetByEmail(_ context.Context, email string) (*models.User, error) { args := m.Called(email) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.User), args.Error(1) } func (m *MockUserRepository) GetByUsername(_ context.Context, username string) (*models.User, error) { args := m.Called(username) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*models.User), args.Error(1) } func (m *MockUserRepository) Create(_ context.Context, user *models.User) error { args := m.Called(user) return args.Error(0) } func (m *MockUserRepository) Update(_ context.Context, user *models.User) error { args := m.Called(user) return args.Error(0) } func (m *MockUserRepository) Delete(_ context.Context, id string) error { args := m.Called(id) return args.Error(0) } // ========== TEST DATABASE SETUP ========== func setupUserTestDB(t *testing.T) *gorm.DB { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("Failed to connect to test database: %v", err) } // Auto-migrate les modèles nécessaires err = db.AutoMigrate( &models.User{}, &models.UserSettings{}, &models.UserProfile{}, ) if err != nil { t.Fatalf("Failed to migrate: %v", err) } return db } // ========== TESTS ========== func TestUserService_GetProfile_Success(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() user := &models.User{ ID: userID, Username: "testuser", FirstName: "Test", LastName: "User", Email: "test@example.com", CreatedAt: time.Now(), IsPublic: true, } mockRepo.On("GetByID", userID.String()).Return(user, nil) // Execute profile, err := service.GetProfile(context.Background(), userID, &userID) // Assert assert.NoError(t, err) assert.NotNil(t, profile) assert.Equal(t, userID, profile.ID) assert.Equal(t, "testuser", profile.Username) mockRepo.AssertExpectations(t) } func TestUserService_GetProfile_NotFound(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() mockRepo.On("GetByID", userID.String()).Return(nil, errors.New("not found")) // Execute profile, err := service.GetProfile(context.Background(), userID, &userID) // Assert assert.Error(t, err) assert.Nil(t, profile) } func TestUserService_GetProfile_Private(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() otherID := uuid.New() bio := "Secret bio" user := &models.User{ ID: userID, Username: "privateuser", IsPublic: false, Bio: bio, } mockRepo.On("GetByID", userID.String()).Return(user, nil) // Execute as another user profile, err := service.GetProfile(context.Background(), userID, &otherID) // Assert assert.NoError(t, err) assert.NotNil(t, profile) assert.Nil(t, profile.Bio, "Bio should be nil for private profile viewed by other") } func TestUserService_GetProfileByUsername_Success(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() username := "testuser" user := &models.User{ ID: userID, Username: username, IsPublic: true, } mockRepo.On("GetByUsername", username).Return(user, nil) mockRepo.On("GetByID", userID.String()).Return(user, nil) // Execute profile, err := service.GetProfileByUsername(context.Background(), username, &userID) // Assert assert.NoError(t, err) assert.Equal(t, username, profile.Username) } func TestUserService_UpdateProfile_Success(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() user := &models.User{ ID: userID, Username: "oldname", Bio: "old bio", } newName := "newname" newBio := "new bio" req := types.UpdateProfileRequest{ Username: &newName, Bio: &newBio, } mockRepo.On("GetByID", userID.String()).Return(user, nil) mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool { return u.Username == "newname" && u.Bio == "new bio" })).Return(nil) // Execute profile, err := service.UpdateProfile(context.Background(), userID, req) // Assert assert.NoError(t, err) assert.Equal(t, "newname", profile.Username) assert.Equal(t, "new bio", *profile.Bio) } func TestUserService_UpdateProfile_WithSocialLinks_Success(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() user := &models.User{ ID: userID, } socialLinks := map[string]interface{}{ "twitter": "https://twitter.com/test", } req := types.UpdateProfileRequest{ SocialLinks: socialLinks, } mockRepo.On("GetByID", userID.String()).Return(user, nil) mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool { return u.SocialLinks != "" && strings.Contains(u.SocialLinks, "twitter") })).Return(nil) // Execute profile, err := service.UpdateProfile(context.Background(), userID, req) // Assert assert.NoError(t, err) assert.NotNil(t, profile.SocialLinks) assert.Equal(t, "https://twitter.com/test", profile.SocialLinks["twitter"]) mockRepo.AssertExpectations(t) } func TestUserService_GetUserSettings_Success(t *testing.T) { // Setup with DB db := setupUserTestDB(t) mockRepo := new(MockUserRepository) service := NewUserServiceWithDB(mockRepo, db) userID := uuid.New() // Create settings in DB settings := models.UserSettings{ UserID: userID, EmailNotifications: true, } db.Create(&settings) // Create profile in DB profile := models.UserProfile{ UserID: userID, Language: "fr", Timezone: "Europe/Paris", } db.Create(&profile) // Execute resp, err := service.GetUserSettings(userID) // Assert assert.NoError(t, err) assert.True(t, resp.Notifications.Email) assert.Equal(t, "fr", resp.Preferences.Language) assert.Equal(t, "Europe/Paris", resp.Preferences.Timezone) } func TestUserService_UpdateUserSettings_Success(t *testing.T) { // Setup with DB db := setupUserTestDB(t) mockRepo := new(MockUserRepository) service := NewUserServiceWithDB(mockRepo, db) userID := uuid.New() // Initial state settings := models.UserSettings{UserID: userID, EmailNotifications: false} db.Create(&settings) profile := models.UserProfile{UserID: userID, Language: "en"} db.Create(&profile) // Request newLang := "es" req := types.UpdateSettingsRequest{ Notifications: &types.NotificationSettings{Email: true}, Preferences: &types.PreferenceSettings{Language: newLang}, } // Execute err := service.UpdateUserSettings(userID, &req) // Assert assert.NoError(t, err) // Verify DB var dbSettings models.UserSettings db.First(&dbSettings, "user_id = ?", userID) assert.True(t, dbSettings.EmailNotifications) var dbProfile models.UserProfile db.First(&dbProfile, "user_id = ?", userID) assert.Equal(t, "es", dbProfile.Language) } func TestUserService_DeleteUser_Success(t *testing.T) { // Setup db := setupUserTestDB(t) mockRepo := new(MockUserRepository) service := NewUserServiceWithDB(mockRepo, db) userID := uuid.New() user := &models.User{ID: userID, IsActive: true} // Create user in DB for soft delete check (if needed by service implementation detail) db.Create(user) mockRepo.On("GetByID", userID.String()).Return(user, nil) mockRepo.On("Delete", userID.String()).Return(nil) // Execute ctx := context.Background() err := service.DeleteUser(ctx, userID) // Assert assert.NoError(t, err) // Verify IsActive updated in DB (as per implementation) var dbUser models.User db.First(&dbUser, "id = ?", userID) assert.False(t, dbUser.IsActive) mockRepo.AssertExpectations(t) } func TestUserService_UploadAvatar(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) // Use temp dir for uploads tmpDir, err := os.MkdirTemp("", "avatar_test") if err != nil { t.Fatal(err) } defer os.RemoveAll(tmpDir) service.SetUploadDir(tmpDir) userID := uuid.New() content := []byte("fake image content") fileHeader := createMultipartFileHeader(t, "avatar.png", content, "image/png") // Execute url, err := service.UploadAvatar(userID, fileHeader) // Assert assert.NoError(t, err) assert.Contains(t, url, "/uploads/avatars/") assert.Contains(t, url, ".png") } func TestUserService_UpdateAvatarURL(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() user := &models.User{ID: userID} newAvatar := "/uploads/avatars/new.png" mockRepo.On("GetByID", userID.String()).Return(user, nil) mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool { return u.Avatar == newAvatar })).Return(nil) // Execute err := service.UpdateAvatarURL(context.Background(), userID, newAvatar) // Assert assert.NoError(t, err) mockRepo.AssertExpectations(t) } func TestUserService_ValidateUsername(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() otherID := uuid.New() currentUsername := "current" newUsername := "newname" takenUsername := "taken" user := &models.User{ ID: userID, Username: currentUsername, } mockRepo.On("GetByID", userID.String()).Return(user, nil) // Case 1: Username available mockRepo.On("GetByUsername", newUsername).Return(nil, gorm.ErrRecordNotFound) // Case 2: Username taken otherUser := &models.User{ID: otherID, Username: takenUsername} mockRepo.On("GetByUsername", takenUsername).Return(otherUser, nil) // Execute Case 1 err := service.ValidateUsername(context.Background(), userID, newUsername) assert.NoError(t, err) // Execute Case 2 err = service.ValidateUsername(context.Background(), userID, takenUsername) assert.Error(t, err) assert.Equal(t, "username already taken", err.Error()) // Case 3: Rate limit // (Skipping rate limit test complexity for now as logic is standard time check) } func TestUserService_CalculateProfileCompletion(t *testing.T) { // Setup mockRepo := new(MockUserRepository) service := NewUserService(mockRepo) userID := uuid.New() avatar := "avatar.png" bio := "bio" socialLinks := `{"twitter": "https://twitter.com/test"}` user := &models.User{ ID: userID, Username: "complete", FirstName: "John", LastName: "Doe", Bio: bio, Avatar: avatar, IsPublic: true, SocialLinks: socialLinks, } mockRepo.On("GetByID", userID.String()).Return(user, nil) // Execute completion, err := service.CalculateProfileCompletion(context.Background(), userID) // Assert assert.NoError(t, err) assert.Equal(t, 100, completion.Percentage) assert.Empty(t, completion.Missing) } // Helper func createMultipartFileHeader(t *testing.T, filename string, content []byte, contentType string) *multipart.FileHeader { body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", filename) if err != nil { t.Fatal(err) } _, err = part.Write(content) if err != nil { t.Fatal(err) } err = writer.Close() if err != nil { t.Fatal(err) } reader := multipart.NewReader(body, writer.Boundary()) form, err := reader.ReadForm(1024 * 1024) if err != nil { t.Fatal(err) } headers := form.File["file"] if len(headers) == 0 { t.Fatal("no file header") } headers[0].Header.Set("Content-Type", contentType) return headers[0] }