package services import ( "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/treytartt/honeydue-api/internal/config" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/repositories" "github.com/treytartt/honeydue-api/internal/testutil" ) func setupAuthService(t *testing.T) (*AuthService, *repositories.UserRepository) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) notifRepo := repositories.NewNotificationRepository(db) cfg := &config.Config{ Server: config.ServerConfig{ DebugFixedCodes: true, }, Security: config.SecurityConfig{ SecretKey: "test-secret", ConfirmationExpiry: 24 * time.Hour, PasswordResetExpiry: 15 * time.Minute, MaxPasswordResetRate: 3, TokenExpiryDays: 90, TokenRefreshDays: 60, }, } service := NewAuthService(userRepo, cfg) service.SetNotificationRepository(notifRepo) return service, userRepo } // === Login === func TestAuthService_Login(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") req := &requests.LoginRequest{ Username: "testuser", Password: "Password123", } resp, err := service.Login(req) require.NoError(t, err) assert.NotEmpty(t, resp.Token) assert.Equal(t, "testuser", resp.User.Username) } func TestAuthService_Login_ByEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") req := &requests.LoginRequest{ Email: "test@test.com", Password: "Password123", } resp, err := service.Login(req) require.NoError(t, err) assert.NotEmpty(t, resp.Token) } func TestAuthService_Login_InvalidCredentials(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") req := &requests.LoginRequest{ Username: "testuser", Password: "WrongPassword1", } _, err := service.Login(req) testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.invalid_credentials") } func TestAuthService_Login_UserNotFound(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) req := &requests.LoginRequest{ Username: "nonexistent", Password: "Password123", } _, err := service.Login(req) testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.invalid_credentials") } func TestAuthService_Login_InactiveUser(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "inactive", "inactive@test.com", "Password123") // Deactivate user.IsActive = false db.Save(user) req := &requests.LoginRequest{ Username: "inactive", Password: "Password123", } _, err := service.Login(req) testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.account_inactive") } // === Register === func TestAuthService_Register(t *testing.T) { service, _ := setupAuthService(t) req := &requests.RegisterRequest{ Username: "newuser", Email: "new@test.com", Password: "Password123", } resp, code, err := service.Register(req) require.NoError(t, err) assert.NotEmpty(t, resp.Token) assert.Equal(t, "newuser", resp.User.Username) assert.Equal(t, "123456", code) // DebugFixedCodes=true } func TestAuthService_Register_DuplicateUsername(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Server: config.ServerConfig{DebugFixedCodes: true}, Security: config.SecurityConfig{SecretKey: "test", ConfirmationExpiry: 24 * time.Hour}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "taken", "taken@test.com", "Password123") req := &requests.RegisterRequest{ Username: "taken", Email: "different@test.com", Password: "Password123", } _, _, err := service.Register(req) testutil.AssertAppError(t, err, http.StatusConflict, "error.username_taken") } func TestAuthService_Register_DuplicateEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Server: config.ServerConfig{DebugFixedCodes: true}, Security: config.SecurityConfig{SecretKey: "test", ConfirmationExpiry: 24 * time.Hour}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "existing", "taken@test.com", "Password123") req := &requests.RegisterRequest{ Username: "newuser", Email: "taken@test.com", Password: "Password123", } _, _, err := service.Register(req) testutil.AssertAppError(t, err, http.StatusConflict, "error.email_taken") } // === GetCurrentUser === func TestAuthService_GetCurrentUser(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") // Create profile userRepo.GetOrCreateProfile(user.ID) resp, err := service.GetCurrentUser(user.ID) require.NoError(t, err) assert.Equal(t, "testuser", resp.Username) assert.Equal(t, "test@test.com", resp.Email) assert.Equal(t, "email", resp.AuthProvider) // Default for no social auth } // === UpdateProfile === func TestAuthService_UpdateProfile(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) newFirst := "John" newLast := "Doe" req := &requests.UpdateProfileRequest{ FirstName: &newFirst, LastName: &newLast, } resp, err := service.UpdateProfile(user.ID, req) require.NoError(t, err) assert.Equal(t, "John", resp.FirstName) assert.Equal(t, "Doe", resp.LastName) } func TestAuthService_UpdateProfile_DuplicateEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "user1", "user1@test.com", "Password123") user2 := testutil.CreateTestUser(t, db, "user2", "user2@test.com", "Password123") userRepo.GetOrCreateProfile(user2.ID) takenEmail := "user1@test.com" req := &requests.UpdateProfileRequest{ Email: &takenEmail, } _, err := service.UpdateProfile(user2.ID, req) testutil.AssertAppError(t, err, http.StatusConflict, "error.email_already_taken") } func TestAuthService_UpdateProfile_SameEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) sameEmail := "test@test.com" req := &requests.UpdateProfileRequest{ Email: &sameEmail, } // Same email should not trigger duplicate error resp, err := service.UpdateProfile(user.ID, req) require.NoError(t, err) assert.Equal(t, "test@test.com", resp.Email) } // === VerifyEmail === func TestAuthService_VerifyEmail(t *testing.T) { service, _ := setupAuthService(t) // Register a user (creates confirmation code) req := &requests.RegisterRequest{ Username: "newuser", Email: "new@test.com", Password: "Password123", } _, _, err := service.Register(req) require.NoError(t, err) // Get the user ID user, err := service.userRepo.FindByEmail("new@test.com") require.NoError(t, err) // Verify with the debug code err = service.VerifyEmail(user.ID, "123456") require.NoError(t, err) // Verify again — should get already verified error err = service.VerifyEmail(user.ID, "123456") testutil.AssertAppError(t, err, http.StatusBadRequest, "error.email_already_verified") } func TestAuthService_VerifyEmail_InvalidCode(t *testing.T) { service, _ := setupAuthService(t) // Register req := &requests.RegisterRequest{ Username: "newuser", Email: "new@test.com", Password: "Password123", } _, _, err := service.Register(req) require.NoError(t, err) user, err := service.userRepo.FindByEmail("new@test.com") require.NoError(t, err) // Wrong code — with DebugFixedCodes enabled, "123456" bypasses normal lookup, // but a wrong code should use the normal path err = service.VerifyEmail(user.ID, "000000") assert.Error(t, err) } // === ResendVerificationCode === func TestAuthService_ResendVerificationCode(t *testing.T) { service, _ := setupAuthService(t) // Register req := &requests.RegisterRequest{ Username: "newuser", Email: "new@test.com", Password: "Password123", } _, _, err := service.Register(req) require.NoError(t, err) user, err := service.userRepo.FindByEmail("new@test.com") require.NoError(t, err) code, err := service.ResendVerificationCode(user.ID) require.NoError(t, err) assert.Equal(t, "123456", code) // DebugFixedCodes } func TestAuthService_ResendVerificationCode_AlreadyVerified(t *testing.T) { service, _ := setupAuthService(t) // Register and verify req := &requests.RegisterRequest{ Username: "newuser", Email: "new@test.com", Password: "Password123", } _, _, err := service.Register(req) require.NoError(t, err) user, err := service.userRepo.FindByEmail("new@test.com") require.NoError(t, err) err = service.VerifyEmail(user.ID, "123456") require.NoError(t, err) _, err = service.ResendVerificationCode(user.ID) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.email_already_verified") } // === ForgotPassword === func TestAuthService_ForgotPassword(t *testing.T) { service, _ := setupAuthService(t) // Register a user first registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) code, user, err := service.ForgotPassword("test@test.com") require.NoError(t, err) assert.Equal(t, "123456", code) // DebugFixedCodes assert.NotNil(t, user) assert.Equal(t, "test@test.com", user.Email) } func TestAuthService_ForgotPassword_NonexistentEmail(t *testing.T) { service, _ := setupAuthService(t) // Should not reveal that email doesn't exist code, user, err := service.ForgotPassword("nonexistent@test.com") require.NoError(t, err) assert.Empty(t, code) assert.Nil(t, user) } // === ResetPassword === func TestAuthService_ResetPassword(t *testing.T) { service, _ := setupAuthService(t) // Register registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) // Forgot password _, _, err = service.ForgotPassword("test@test.com") require.NoError(t, err) // Verify reset code to get the token resetToken, err := service.VerifyResetCode("test@test.com", "123456") require.NoError(t, err) assert.NotEmpty(t, resetToken) // Reset password err = service.ResetPassword(resetToken, "NewPassword123") require.NoError(t, err) // Login with new password loginReq := &requests.LoginRequest{ Username: "testuser", Password: "NewPassword123", } loginResp, err := service.Login(loginReq) require.NoError(t, err) assert.NotEmpty(t, loginResp.Token) } func TestAuthService_ResetPassword_InvalidToken(t *testing.T) { service, _ := setupAuthService(t) err := service.ResetPassword("invalid-token", "NewPassword123") testutil.AssertAppError(t, err, http.StatusBadRequest, "error.invalid_reset_token") } // === Logout === func TestAuthService_Logout(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") // Login first loginReq := &requests.LoginRequest{ Username: "testuser", Password: "Password123", } loginResp, err := service.Login(loginReq) require.NoError(t, err) // Logout err = service.Logout(loginResp.Token) require.NoError(t, err) // Token should be deleted — refreshing should fail _, err = service.RefreshToken(loginResp.Token, user.ID) assert.Error(t, err) } // === DeleteAccount === func TestAuthService_DeleteAccount_EmailAuth(t *testing.T) { service, _ := setupAuthService(t) // Register registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) user, err := service.userRepo.FindByEmail("test@test.com") require.NoError(t, err) password := "Password123" _, err = service.DeleteAccount(user.ID, &password, nil) require.NoError(t, err) } func TestAuthService_DeleteAccount_WrongPassword(t *testing.T) { service, _ := setupAuthService(t) registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) user, err := service.userRepo.FindByEmail("test@test.com") require.NoError(t, err) wrongPassword := "WrongPassword1" _, err = service.DeleteAccount(user.ID, &wrongPassword, nil) testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.invalid_credentials") } func TestAuthService_DeleteAccount_NoPassword(t *testing.T) { service, _ := setupAuthService(t) registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) user, err := service.userRepo.FindByEmail("test@test.com") require.NoError(t, err) _, err = service.DeleteAccount(user.ID, nil, nil) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.password_required") } func TestAuthService_DeleteAccount_UserNotFound(t *testing.T) { service, _ := setupAuthService(t) password := "Password123" _, err := service.DeleteAccount(99999, &password, nil) testutil.AssertAppError(t, err, http.StatusNotFound, "error.user_not_found") } // === Helper functions === func TestGenerateSixDigitCode(t *testing.T) { code := generateSixDigitCode() assert.Len(t, code, 6) // Should be numeric for _, c := range code { assert.True(t, c >= '0' && c <= '9', "code should contain only digits") } } func TestGenerateResetToken(t *testing.T) { token := generateResetToken() assert.NotEmpty(t, token) assert.Len(t, token, 64) // 32 bytes = 64 hex chars } func TestGetStringOrEmpty(t *testing.T) { s := "hello" assert.Equal(t, "hello", getStringOrEmpty(&s)) assert.Equal(t, "", getStringOrEmpty(nil)) } func TestIsPrivateRelayEmail(t *testing.T) { assert.True(t, isPrivateRelayEmail("abc@privaterelay.appleid.com")) assert.True(t, isPrivateRelayEmail("ABC@PRIVATERELAY.APPLEID.COM")) assert.False(t, isPrivateRelayEmail("user@gmail.com")) } func TestGetEmailFromRequest(t *testing.T) { email := "req@test.com" assert.Equal(t, "req@test.com", getEmailFromRequest(&email, "claims@test.com")) assert.Equal(t, "claims@test.com", getEmailFromRequest(nil, "claims@test.com")) empty := "" assert.Equal(t, "claims@test.com", getEmailFromRequest(&empty, "claims@test.com")) } // === getEmailOrDefault === func TestGetEmailOrDefault(t *testing.T) { // Non-empty email returns itself assert.Equal(t, "user@test.com", getEmailOrDefault("user@test.com")) // Empty email returns a generated placeholder result := getEmailOrDefault("") assert.Contains(t, result, "@privaterelay.appleid.com") assert.Contains(t, result, "apple_") } // === generateUniqueUsername === func TestGenerateUniqueUsername(t *testing.T) { // Normal email generates username from email prefix username := generateUniqueUsername("john@test.com", nil) assert.Contains(t, username, "john_") // Private relay email falls back to first name firstName := "Jane" username = generateUniqueUsername("abc@privaterelay.appleid.com", &firstName) assert.Contains(t, username, "jane_") // Private relay email and no first name — fallback username = generateUniqueUsername("abc@privaterelay.appleid.com", nil) assert.Contains(t, username, "user_") // Empty email with first name firstName2 := "Bob" username = generateUniqueUsername("", &firstName2) assert.Contains(t, username, "bob_") // Empty email and no first name username = generateUniqueUsername("", nil) assert.Contains(t, username, "user_") } // === generateGoogleUsername === func TestGenerateGoogleUsername(t *testing.T) { // Normal email username := generateGoogleUsername("john@gmail.com", "John") assert.Contains(t, username, "john_") // Empty email falls back to first name username = generateGoogleUsername("", "Alice") assert.Contains(t, username, "alice_") // Empty email and empty first name — fallback username = generateGoogleUsername("", "") assert.Contains(t, username, "google_") } // === Login with empty password === func TestAuthService_Login_EmptyPassword(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") req := &requests.LoginRequest{ Username: "testuser", Password: "", } _, err := service.Login(req) testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.invalid_credentials") } // === ForgotPassword rate limiting === func TestAuthService_ForgotPassword_RateLimit(t *testing.T) { service, _ := setupAuthService(t) registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) // Make max allowed reset requests (3 based on setup) for i := 0; i < 3; i++ { _, _, err := service.ForgotPassword("test@test.com") require.NoError(t, err) } // The 4th should be rate limited _, _, err = service.ForgotPassword("test@test.com") assert.Error(t, err) } // === VerifyResetCode with wrong code === func TestAuthService_VerifyResetCode_WrongCode(t *testing.T) { service, _ := setupAuthService(t) registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) _, _, err = service.ForgotPassword("test@test.com") require.NoError(t, err) // Wrong code but with debug mode, "123456" works, "000000" should fail _, err = service.VerifyResetCode("test@test.com", "000000") assert.Error(t, err) } // === VerifyResetCode with nonexistent email === func TestAuthService_VerifyResetCode_NonexistentEmail(t *testing.T) { service, _ := setupAuthService(t) _, err := service.VerifyResetCode("nonexistent@test.com", "123456") assert.Error(t, err) } // === UpdateProfile — change email to new email === func TestAuthService_UpdateProfile_ChangeEmail(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) user := testutil.CreateTestUser(t, db, "testuser", "test@test.com", "Password123") userRepo.GetOrCreateProfile(user.ID) newEmail := "newemail@test.com" req := &requests.UpdateProfileRequest{ Email: &newEmail, } resp, err := service.UpdateProfile(user.ID, req) require.NoError(t, err) assert.Equal(t, "newemail@test.com", resp.Email) } // === DeleteAccount — empty password string === func TestAuthService_DeleteAccount_EmptyPassword(t *testing.T) { service, _ := setupAuthService(t) registerReq := &requests.RegisterRequest{ Username: "testuser", Email: "test@test.com", Password: "Password123", } _, _, err := service.Register(registerReq) require.NoError(t, err) user, err := service.userRepo.FindByEmail("test@test.com") require.NoError(t, err) emptyPw := "" _, err = service.DeleteAccount(user.ID, &emptyPw, nil) testutil.AssertAppError(t, err, http.StatusBadRequest, "error.password_required") } // === SetNotificationRepository === func TestAuthService_SetNotificationRepository(t *testing.T) { db := testutil.SetupTestDB(t) userRepo := repositories.NewUserRepository(db) notifRepo := repositories.NewNotificationRepository(db) cfg := &config.Config{ Security: config.SecurityConfig{SecretKey: "test-secret"}, } service := NewAuthService(userRepo, cfg) assert.Nil(t, service.notificationRepo) service.SetNotificationRepository(notifRepo) assert.NotNil(t, service.notificationRepo) } // === Register creates profile and notification preferences === func TestAuthService_Register_CreatesProfile(t *testing.T) { service, userRepo := setupAuthService(t) req := &requests.RegisterRequest{ Username: "profileuser", Email: "profile@test.com", Password: "Password123", FirstName: "John", LastName: "Doe", } resp, _, err := service.Register(req) require.NoError(t, err) assert.Equal(t, "profileuser", resp.User.Username) // Profile should exist profile, err := userRepo.GetOrCreateProfile(resp.User.ID) require.NoError(t, err) assert.NotNil(t, profile) }