c77ff07ce9
Remediation of the 2026-05-12/13 audits (78 findings + cluster gaps), tracked in deploy-k3s/SECURITY.md, plus fixes from two independent post-remediation reviews. Auth & sessions: - SHA-256 hashed auth-token storage (C1); prior-token cache eviction on re-login (MEDIUM-1) - local Google JWKS verification, iss/aud/exp checks (C2/C3) - constant-time login + generic errors (L1/LIVE-L11/LIVE-L13) - per-account login lockout keyed on distinct source IPs (M5/MEDIUM-3) - verified-email gating, login rate limiting (LIVE-L19, H1-H3) IAP & webhooks: - Apple/Google cross-account replay protection (C5/C6/C10/C13, H5/H6) - migrations 000003-000006 (token hashing, IAP replay, audit_log + webhook_event_log table creation, append-only audit log) Authorization & races: - file-ownership owner-OR-member fix (C7), atomic share-code join (C9/H9), device-token reassignment (C8/LOW-3) Secrets & deploy: - secrets file-mounted at /etc/honeydue/secrets, not env (F8); Redis password out of the ConfigMap (HIGH-1); B2 keys reconciled - digest-pinned images, admin ingress hardening, CSP/HSTS, /metrics lockdown; kubeconfig 0600, etcd secrets-encryption, fail2ban + unattended-upgrades at provision; secret-rotation runbook Build, vet, and the full test suite (incl. -race) pass; the goose migration chain is verified against PostgreSQL 16. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
804 lines
23 KiB
Go
804 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, "")
|
|
// Audit L1: inactive accounts return the same generic error as bad
|
|
// credentials so login does not disclose which accounts exist.
|
|
testutil.AssertAppError(t, err, http.StatusUnauthorized, "error.invalid_credentials")
|
|
}
|
|
|
|
// === 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)
|
|
}
|