e881d37de0
Every public method on these five services now takes ctx context.Context as the first arg and routes its repo calls through .WithContext(ctx). With TaskService and ResidenceService already migrated, this means every in-process service that touches Postgres now produces a flame graph in Jaeger where the SQL spans nest under the parent HTTP request span. Endpoints now fully traced (HTTP → service → SQL): - /api/auth/login, /register, /logout, /me, /verify-email, /resend-verification - /api/auth/forgot-password, /verify-reset, /reset-password, /update-profile - /api/contractors/* (CRUD + favorite + by-residence + tasks) - /api/documents/* (CRUD + activate/deactivate + image upload/delete) - /api/notifications/* (list, count, mark-read, prefs, devices) - /api/subscription/* (status, purchase, cancel, triggers, promotions) - All previously-migrated /api/tasks/* and /api/residences/* paths Internal helpers also threaded: - TaskService.sendTaskCompletedNotification → forwards ctx - TaskService.UpdateUserTimezone → forwards ctx to NotificationService - ResidenceService.CreateResidence → forwards ctx to SubscriptionService.CheckLimit - NotificationService.registerAPNSDevice / registerGCMDevice → both take ctx ~75 method signatures, ~120 handler/test call sites updated. Tests pass green; the only failure is the pre-existing flaky TaskHandler_QuickComplete SQLite race that fails ~60% of runs on master. Step 3 of the observability plan is now genuinely complete: every API endpoint backed by a Go service emits a per-request flame graph with HTTP → service → SQL spans, plus B2/APNs/FCM/asynq spans where applicable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
802 lines
23 KiB
Go
802 lines
23 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), user.ID, "123456")
|
|
require.NoError(t, err)
|
|
|
|
// Verify again — should get already verified error
|
|
err = service.VerifyEmail(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), req)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("new@test.com")
|
|
require.NoError(t, err)
|
|
|
|
code, err := service.ResendVerificationCode(context.Background(), 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(context.Background(), req)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("new@test.com")
|
|
require.NoError(t, err)
|
|
|
|
err = service.VerifyEmail(context.Background(), user.ID, "123456")
|
|
require.NoError(t, err)
|
|
|
|
_, err = service.ResendVerificationCode(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
code, user, err := service.ForgotPassword(context.Background(), "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(context.Background(), "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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
// Forgot password
|
|
_, _, err = service.ForgotPassword(context.Background(), "test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
// Verify reset code to get the token
|
|
resetToken, err := service.VerifyResetCode(context.Background(), "test@test.com", "123456")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resetToken)
|
|
|
|
// Reset password
|
|
err = service.ResetPassword(context.Background(), resetToken, "NewPassword123")
|
|
require.NoError(t, err)
|
|
|
|
// Login with new password
|
|
loginReq := &requests.LoginRequest{
|
|
Username: "testuser",
|
|
Password: "NewPassword123",
|
|
}
|
|
loginResp, err := service.Login(context.Background(), loginReq)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, loginResp.Token)
|
|
}
|
|
|
|
func TestAuthService_ResetPassword_InvalidToken(t *testing.T) {
|
|
service, _ := setupAuthService(t)
|
|
|
|
err := service.ResetPassword(context.Background(), "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(context.Background(), loginReq)
|
|
require.NoError(t, err)
|
|
|
|
// Logout
|
|
err = service.Logout(context.Background(), loginResp.Token)
|
|
require.NoError(t, err)
|
|
|
|
// Token should be deleted — refreshing should fail
|
|
_, err = service.RefreshToken(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
password := "Password123"
|
|
_, err = service.DeleteAccount(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
wrongPassword := "WrongPassword1"
|
|
_, err = service.DeleteAccount(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
_, err = service.DeleteAccount(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
// Make max allowed reset requests (3 based on setup)
|
|
for i := 0; i < 3; i++ {
|
|
_, _, err := service.ForgotPassword(context.Background(), "test@test.com")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// The 4th should be rate limited
|
|
_, _, err = service.ForgotPassword(context.Background(), "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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = service.ForgotPassword(context.Background(), "test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
// Wrong code but with debug mode, "123456" works, "000000" should fail
|
|
_, err = service.VerifyResetCode(context.Background(), "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(context.Background(), "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(context.Background(), 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(context.Background(), registerReq)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.userRepo.FindByEmail("test@test.com")
|
|
require.NoError(t, err)
|
|
|
|
emptyPw := ""
|
|
_, err = service.DeleteAccount(context.Background(), 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(context.Background(), 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)
|
|
}
|