package app import ( "context" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "testing" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/tenant/repository" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" ) func TestAuthServiceLoginPasswordDecryptsEncryptedPassword(t *testing.T) { t.Parallel() key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) privatePEM := string(pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key), })) cipher, err := auth.NewPasswordCipher(privatePEM, "tenant-test-key") require.NoError(t, err) encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte("Test@123"), nil) require.NoError(t, err) svc := (&AuthService{}).WithPasswordCipher(cipher) got, err := svc.loginPassword(LoginRequest{ EncryptedPassword: base64.StdEncoding.EncodeToString(encrypted), PasswordKeyID: "tenant-test-key", }) require.NoError(t, err) require.Equal(t, "Test@123", got) } func TestAuthServiceLoginPasswordAllowsPlainPasswordCompatibility(t *testing.T) { t.Parallel() got, err := (&AuthService{}).loginPassword(LoginRequest{Password: "Test@123"}) require.NoError(t, err) require.Equal(t, "Test@123", got) } func TestAuthServiceLoginPasswordRejectsEncryptedWhenCipherMissing(t *testing.T) { t.Parallel() _, err := (&AuthService{}).loginPassword(LoginRequest{EncryptedPassword: "abc"}) require.Error(t, err) appErr, ok := err.(*response.AppError) require.True(t, ok) require.Equal(t, "password_cipher_unavailable", appErr.Message) } func TestAuthServiceChangePasswordUpdatesHash(t *testing.T) { t.Parallel() oldHash, err := bcrypt.GenerateFromPassword([]byte("OldPass@123"), bcrypt.MinCost) require.NoError(t, err) repo := &authRepoStub{ user: &repository.UserRecord{ ID: 12, Phone: "13800000000", PasswordHash: string(oldHash), Status: "active", }, } sessions := auth.NewSessionStore(nil) svc := &AuthService{repo: repo, sessions: sessions} err = svc.ChangePassword(context.Background(), auth.Actor{UserID: 12}, ChangePasswordRequest{ OldPassword: "OldPass@123", NewPassword: "NewPass@123", }) require.NoError(t, err) require.Equal(t, int64(12), repo.updatedUserID) require.NoError(t, bcrypt.CompareHashAndPassword([]byte(repo.updatedPasswordHash), []byte("NewPass@123"))) version, err := sessions.SessionVersion(context.Background(), 12) require.NoError(t, err) require.Equal(t, int64(1), version) } func TestAuthServiceChangePasswordRejectsInvalidOldPassword(t *testing.T) { t.Parallel() oldHash, err := bcrypt.GenerateFromPassword([]byte("OldPass@123"), bcrypt.MinCost) require.NoError(t, err) repo := &authRepoStub{ user: &repository.UserRecord{ ID: 12, Phone: "13800000000", PasswordHash: string(oldHash), Status: "active", }, } svc := &AuthService{repo: repo} err = svc.ChangePassword(context.Background(), auth.Actor{UserID: 12}, ChangePasswordRequest{ OldPassword: "wrong", NewPassword: "NewPass@123", }) require.Error(t, err) appErr, ok := err.(*response.AppError) require.True(t, ok) require.Equal(t, "invalid_old_password", appErr.Message) require.Empty(t, repo.updatedPasswordHash) } func TestAuthServiceChangePasswordRejectsWeakPassword(t *testing.T) { t.Parallel() repo := &authRepoStub{} svc := &AuthService{repo: repo} err := svc.ChangePassword(context.Background(), auth.Actor{UserID: 12}, ChangePasswordRequest{ OldPassword: "OldPass@123", NewPassword: "short", }) require.Error(t, err) appErr, ok := err.(*response.AppError) require.True(t, ok) require.Equal(t, "weak_password", appErr.Message) require.False(t, repo.getUserByIDCalled) } func TestAuthServiceChangePasswordDecryptsEncryptedPasswords(t *testing.T) { t.Parallel() key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) privatePEM := string(pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key), })) cipher, err := auth.NewPasswordCipher(privatePEM, "tenant-change-key") require.NoError(t, err) encryptedOld, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte("OldPass@123"), nil) require.NoError(t, err) encryptedNew, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &key.PublicKey, []byte("NewPass@123"), nil) require.NoError(t, err) svc := (&AuthService{}).WithPasswordCipher(cipher) oldPassword, newPassword, err := svc.changePasswordValues(ChangePasswordRequest{ EncryptedOldPassword: base64.StdEncoding.EncodeToString(encryptedOld), EncryptedNewPassword: base64.StdEncoding.EncodeToString(encryptedNew), PasswordKeyID: "tenant-change-key", }) require.NoError(t, err) require.Equal(t, "OldPass@123", oldPassword) require.Equal(t, "NewPass@123", newPassword) } type authRepoStub struct { user *repository.UserRecord getUserByIDCalled bool updatedUserID int64 updatedPasswordHash string } func (r *authRepoStub) GetUserByEmail(context.Context, string) (*repository.UserRecord, error) { return nil, errors.New("not implemented") } func (r *authRepoStub) GetUserByLoginIdentifier(context.Context, string) (*repository.UserRecord, error) { return nil, errors.New("not implemented") } func (r *authRepoStub) GetUserByID(context.Context, int64) (*repository.UserRecord, error) { r.getUserByIDCalled = true if r.user == nil { return nil, errors.New("not found") } return r.user, nil } func (r *authRepoStub) UpdateUserPassword(_ context.Context, id int64, passwordHash string) error { r.updatedUserID = id r.updatedPasswordHash = passwordHash return nil } func (r *authRepoStub) GetTenantMembership(context.Context, int64) (*repository.MembershipRecord, error) { return nil, errors.New("not implemented") } func (r *authRepoStub) GetTenantMembershipByTenantAndUser(context.Context, int64, int64) (*repository.MembershipRecord, error) { return nil, errors.New("not implemented") } var _ repository.AuthRepository = (*authRepoStub)(nil)