Improve README SEO and increase test coverage to 50%
Optimized README for better discoverability with keywords: api, habits, self-hosted. Consolidated content to reduce redundancy while maintaining clarity. Added comprehensive test coverage across multiple layers: - Infrastructure: bcrypt hashing, JWT tokens, configuration validation - Application commands: user deletion, password reset, token revocation, email verification - Application queries: login, token refresh - Domain entities: refresh tokens, password reset tokens Coverage increased from 42.5% to 50.8% with meaningful business logic tests. Fixed integration test handler initialization with correct parameters.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type mockDeleteUserRepo struct {
|
||||
findByIDFunc func(ctx context.Context, id string) (*entities.User, error)
|
||||
deleteFunc func(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if m.findByIDFunc != nil {
|
||||
return m.findByIDFunc(ctx, id)
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) Create(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) Update(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDeleteUserRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.deleteFunc != nil {
|
||||
return m.deleteFunc(ctx, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDeleteUserHandler_Success(t *testing.T) {
|
||||
var deletedID string
|
||||
|
||||
repo := &mockDeleteUserRepo{
|
||||
findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) {
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = id
|
||||
return user, nil
|
||||
},
|
||||
deleteFunc: func(ctx context.Context, id string) error {
|
||||
deletedID = id
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDeleteUserHandler(repo)
|
||||
|
||||
cmd := DeleteUserCommand{
|
||||
UserID: "user-123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if deletedID != "user-123" {
|
||||
t.Errorf("deletedID = %v, want %v", deletedID, "user-123")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUserHandler_EmptyUserID(t *testing.T) {
|
||||
repo := &mockDeleteUserRepo{}
|
||||
handler := NewDeleteUserHandler(repo)
|
||||
|
||||
cmd := DeleteUserCommand{
|
||||
UserID: "",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUserHandler_UserNotFound(t *testing.T) {
|
||||
repo := &mockDeleteUserRepo{
|
||||
findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDeleteUserHandler(repo)
|
||||
|
||||
cmd := DeleteUserCommand{
|
||||
UserID: "non-existent-user",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrNotFound {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUserHandler_DeleteError(t *testing.T) {
|
||||
customError := errors.ErrNotFound
|
||||
|
||||
repo := &mockDeleteUserRepo{
|
||||
findByIDFunc: func(ctx context.Context, id string) (*entities.User, error) {
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = id
|
||||
return user, nil
|
||||
},
|
||||
deleteFunc: func(ctx context.Context, id string) error {
|
||||
return customError
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDeleteUserHandler(repo)
|
||||
|
||||
cmd := DeleteUserCommand{
|
||||
UserID: "user-123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != customError {
|
||||
t.Errorf("Handle() error = %v, want %v", err, customError)
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ type RequestPasswordResetCommand struct {
|
||||
}
|
||||
|
||||
type RequestPasswordResetHandler struct {
|
||||
userRepo repositories.UserRepository
|
||||
passwordResetTokenRepo repositories.PasswordResetTokenRepository
|
||||
emailService services.EmailService
|
||||
appURL string
|
||||
userRepo repositories.UserRepository
|
||||
passwordResetTokenRepo repositories.PasswordResetTokenRepository
|
||||
emailService services.EmailService
|
||||
appURL string
|
||||
}
|
||||
|
||||
func NewRequestPasswordResetHandler(
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type mockResetPasswordUserRepo struct {
|
||||
findByIDFunc func(ctx context.Context, id string) (*entities.User, error)
|
||||
updateFunc func(ctx context.Context, user *entities.User) error
|
||||
users map[string]*entities.User
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if m.findByIDFunc != nil {
|
||||
return m.findByIDFunc(ctx, id)
|
||||
}
|
||||
if user, ok := m.users[id]; ok {
|
||||
return user, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) Update(ctx context.Context, user *entities.User) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(ctx, user)
|
||||
}
|
||||
m.users[user.ID] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) Create(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordUserRepo) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockPasswordResetTokenRepo struct {
|
||||
findByTokenFunc func(ctx context.Context, token string) (*entities.PasswordResetToken, error)
|
||||
updateFunc func(ctx context.Context, token *entities.PasswordResetToken) error
|
||||
tokens map[string]*entities.PasswordResetToken
|
||||
}
|
||||
|
||||
func (m *mockPasswordResetTokenRepo) Create(ctx context.Context, token *entities.PasswordResetToken) error {
|
||||
m.tokens[token.Token] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPasswordResetTokenRepo) FindByToken(ctx context.Context, token string) (*entities.PasswordResetToken, error) {
|
||||
if m.findByTokenFunc != nil {
|
||||
return m.findByTokenFunc(ctx, token)
|
||||
}
|
||||
if t, ok := m.tokens[token]; ok {
|
||||
return t, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockPasswordResetTokenRepo) Update(ctx context.Context, token *entities.PasswordResetToken) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(ctx, token)
|
||||
}
|
||||
m.tokens[token.Token] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPasswordResetTokenRepo) DeleteExpired(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockResetPasswordHasher struct {
|
||||
hashFunc func(password string) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordHasher) Hash(password string) (string, error) {
|
||||
if m.hashFunc != nil {
|
||||
return m.hashFunc(password)
|
||||
}
|
||||
return "hashed_" + password, nil
|
||||
}
|
||||
|
||||
func (m *mockResetPasswordHasher) Compare(hashedPassword, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_Success(t *testing.T) {
|
||||
user := entities.NewUser("test@example.com", "old_hash", "UTC")
|
||||
user.ID = "user-123"
|
||||
|
||||
resetToken := entities.NewPasswordResetToken(
|
||||
user.ID,
|
||||
"reset-token",
|
||||
time.Now().Add(1*time.Hour),
|
||||
)
|
||||
|
||||
var updatedUser *entities.User
|
||||
var updatedToken *entities.PasswordResetToken
|
||||
|
||||
userRepo := &mockResetPasswordUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
user.ID: user,
|
||||
},
|
||||
updateFunc: func(ctx context.Context, u *entities.User) error {
|
||||
updatedUser = u
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
tokenRepo := &mockPasswordResetTokenRepo{
|
||||
tokens: map[string]*entities.PasswordResetToken{
|
||||
resetToken.Token: resetToken,
|
||||
},
|
||||
updateFunc: func(ctx context.Context, t *entities.PasswordResetToken) error {
|
||||
updatedToken = t
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockResetPasswordHasher{}
|
||||
|
||||
handler := NewResetPasswordHandler(userRepo, tokenRepo, hasher)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "reset-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if updatedUser == nil {
|
||||
t.Fatal("User was not updated")
|
||||
}
|
||||
|
||||
if updatedUser.PasswordHash != "hashed_NewP@ssw0rd123" {
|
||||
t.Errorf("PasswordHash = %v, want %v", updatedUser.PasswordHash, "hashed_NewP@ssw0rd123")
|
||||
}
|
||||
|
||||
if updatedToken == nil {
|
||||
t.Fatal("Token was not updated")
|
||||
}
|
||||
|
||||
if !updatedToken.IsUsed() {
|
||||
t.Error("Token should be marked as used")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_EmptyToken(t *testing.T) {
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
&mockPasswordResetTokenRepo{tokens: make(map[string]*entities.PasswordResetToken)},
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_EmptyPassword(t *testing.T) {
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
&mockPasswordResetTokenRepo{tokens: make(map[string]*entities.PasswordResetToken)},
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "reset-token",
|
||||
NewPassword: "",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_InvalidPassword(t *testing.T) {
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
&mockPasswordResetTokenRepo{tokens: make(map[string]*entities.PasswordResetToken)},
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
}{
|
||||
{"too short", "Short1!"},
|
||||
{"no uppercase", "password123!"},
|
||||
{"no lowercase", "PASSWORD123!"},
|
||||
{"no digit", "Password!"},
|
||||
{"no special char", "Password123"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "reset-token",
|
||||
NewPassword: tt.password,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_TokenNotFound(t *testing.T) {
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
&mockPasswordResetTokenRepo{tokens: make(map[string]*entities.PasswordResetToken)},
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "non-existent-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_ExpiredToken(t *testing.T) {
|
||||
resetToken := entities.NewPasswordResetToken(
|
||||
"user-123",
|
||||
"expired-token",
|
||||
time.Now().Add(-1*time.Hour),
|
||||
)
|
||||
|
||||
tokenRepo := &mockPasswordResetTokenRepo{
|
||||
tokens: map[string]*entities.PasswordResetToken{
|
||||
resetToken.Token: resetToken,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
tokenRepo,
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "expired-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_UsedToken(t *testing.T) {
|
||||
resetToken := entities.NewPasswordResetToken(
|
||||
"user-123",
|
||||
"used-token",
|
||||
time.Now().Add(1*time.Hour),
|
||||
)
|
||||
resetToken.MarkAsUsed()
|
||||
|
||||
tokenRepo := &mockPasswordResetTokenRepo{
|
||||
tokens: map[string]*entities.PasswordResetToken{
|
||||
resetToken.Token: resetToken,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewResetPasswordHandler(
|
||||
&mockResetPasswordUserRepo{users: make(map[string]*entities.User)},
|
||||
tokenRepo,
|
||||
&mockResetPasswordHasher{},
|
||||
)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "used-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_UserNotFound(t *testing.T) {
|
||||
resetToken := entities.NewPasswordResetToken(
|
||||
"non-existent-user",
|
||||
"reset-token",
|
||||
time.Now().Add(1*time.Hour),
|
||||
)
|
||||
|
||||
tokenRepo := &mockPasswordResetTokenRepo{
|
||||
tokens: map[string]*entities.PasswordResetToken{
|
||||
resetToken.Token: resetToken,
|
||||
},
|
||||
}
|
||||
|
||||
userRepo := &mockResetPasswordUserRepo{
|
||||
users: make(map[string]*entities.User),
|
||||
}
|
||||
|
||||
handler := NewResetPasswordHandler(userRepo, tokenRepo, &mockResetPasswordHasher{})
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "reset-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrNotFound {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordHandler_HashingError(t *testing.T) {
|
||||
user := entities.NewUser("test@example.com", "old_hash", "UTC")
|
||||
user.ID = "user-123"
|
||||
|
||||
resetToken := entities.NewPasswordResetToken(
|
||||
user.ID,
|
||||
"reset-token",
|
||||
time.Now().Add(1*time.Hour),
|
||||
)
|
||||
|
||||
userRepo := &mockResetPasswordUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
user.ID: user,
|
||||
},
|
||||
}
|
||||
|
||||
tokenRepo := &mockPasswordResetTokenRepo{
|
||||
tokens: map[string]*entities.PasswordResetToken{
|
||||
resetToken.Token: resetToken,
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockResetPasswordHasher{
|
||||
hashFunc: func(password string) (string, error) {
|
||||
return "", errors.ErrInvalidInput
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewResetPasswordHandler(userRepo, tokenRepo, hasher)
|
||||
|
||||
cmd := ResetPasswordCommand{
|
||||
Token: "reset-token",
|
||||
NewPassword: "NewP@ssw0rd123",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err == nil {
|
||||
t.Fatal("Handle() expected error but got nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type mockRefreshTokenRepo struct {
|
||||
tokens map[string]*entities.RefreshToken
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) Create(ctx context.Context, token *entities.RefreshToken) error {
|
||||
m.tokens[token.Token] = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) FindByToken(ctx context.Context, token string) (*entities.RefreshToken, error) {
|
||||
if t, ok := m.tokens[token]; ok {
|
||||
return t, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) FindByUserID(ctx context.Context, userID string) ([]*entities.RefreshToken, error) {
|
||||
var tokens []*entities.RefreshToken
|
||||
for _, t := range m.tokens {
|
||||
if t.UserID == userID {
|
||||
tokens = append(tokens, t)
|
||||
}
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) RevokeByToken(ctx context.Context, token string) error {
|
||||
if t, ok := m.tokens[token]; ok {
|
||||
t.Revoke()
|
||||
return nil
|
||||
}
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) RevokeAllByUserID(ctx context.Context, userID string) error {
|
||||
for _, t := range m.tokens {
|
||||
if t.UserID == userID {
|
||||
t.Revoke()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockRefreshTokenRepo) DeleteExpired(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRevokeTokenHandler_Handle(t *testing.T) {
|
||||
repo := &mockRefreshTokenRepo{
|
||||
tokens: make(map[string]*entities.RefreshToken),
|
||||
}
|
||||
|
||||
handler := NewRevokeTokenHandler(repo)
|
||||
|
||||
token := entities.NewRefreshToken("user-123", "valid-token", time.Now().Add(24*time.Hour))
|
||||
repo.Create(context.Background(), token)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd RevokeTokenCommand
|
||||
expectError bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "revoke valid token",
|
||||
cmd: RevokeTokenCommand{
|
||||
RefreshToken: "valid-token",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "revoke empty token",
|
||||
cmd: RevokeTokenCommand{
|
||||
RefreshToken: "",
|
||||
},
|
||||
expectError: true,
|
||||
expectedErr: errors.ErrInvalidInput,
|
||||
},
|
||||
{
|
||||
name: "revoke non-existent token",
|
||||
cmd: RevokeTokenCommand{
|
||||
RefreshToken: "non-existent-token",
|
||||
},
|
||||
expectError: true,
|
||||
expectedErr: errors.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := handler.Handle(context.Background(), tt.cmd)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("Handle() expected error but got nil")
|
||||
}
|
||||
if tt.expectedErr != nil && err != tt.expectedErr {
|
||||
t.Errorf("Handle() error = %v, want %v", err, tt.expectedErr)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeAllTokensHandler_Handle(t *testing.T) {
|
||||
repo := &mockRefreshTokenRepo{
|
||||
tokens: make(map[string]*entities.RefreshToken),
|
||||
}
|
||||
|
||||
handler := NewRevokeAllTokensHandler(repo)
|
||||
|
||||
token1 := entities.NewRefreshToken("user-123", "token-1", time.Now().Add(24*time.Hour))
|
||||
token2 := entities.NewRefreshToken("user-123", "token-2", time.Now().Add(24*time.Hour))
|
||||
token3 := entities.NewRefreshToken("user-456", "token-3", time.Now().Add(24*time.Hour))
|
||||
|
||||
repo.Create(context.Background(), token1)
|
||||
repo.Create(context.Background(), token2)
|
||||
repo.Create(context.Background(), token3)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd RevokeAllTokensCommand
|
||||
expectError bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "revoke all tokens for user",
|
||||
cmd: RevokeAllTokensCommand{
|
||||
UserID: "user-123",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "revoke with empty user ID",
|
||||
cmd: RevokeAllTokensCommand{
|
||||
UserID: "",
|
||||
},
|
||||
expectError: true,
|
||||
expectedErr: errors.ErrInvalidInput,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := handler.Handle(context.Background(), tt.cmd)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("Handle() expected error but got nil")
|
||||
}
|
||||
if tt.expectedErr != nil && err != tt.expectedErr {
|
||||
t.Errorf("Handle() error = %v, want %v", err, tt.expectedErr)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectError && tt.cmd.UserID == "user-123" {
|
||||
if repo.tokens["token-1"].RevokedAt == nil {
|
||||
t.Error("token-1 should be revoked")
|
||||
}
|
||||
if repo.tokens["token-2"].RevokedAt == nil {
|
||||
t.Error("token-2 should be revoked")
|
||||
}
|
||||
if repo.tokens["token-3"].RevokedAt != nil {
|
||||
t.Error("token-3 should not be revoked")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/domain/services"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type mockVerifyEmailUserRepo struct {
|
||||
findByVerificationTokenFunc func(ctx context.Context, token string) (*entities.User, error)
|
||||
updateFunc func(ctx context.Context, user *entities.User) error
|
||||
users map[string]*entities.User
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
|
||||
if m.findByVerificationTokenFunc != nil {
|
||||
return m.findByVerificationTokenFunc(ctx, token)
|
||||
}
|
||||
if user, ok := m.users[token]; ok {
|
||||
return user, nil
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) Update(ctx context.Context, user *entities.User) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(ctx, user)
|
||||
}
|
||||
m.users[user.Email] = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) Create(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockVerifyEmailUserRepo) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockEmailService struct {
|
||||
sendFunc func(message services.EmailMessage) error
|
||||
sentMessages []services.EmailMessage
|
||||
}
|
||||
|
||||
func (m *mockEmailService) Send(message services.EmailMessage) error {
|
||||
m.sentMessages = append(m.sentMessages, message)
|
||||
if m.sendFunc != nil {
|
||||
return m.sendFunc(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_Success(t *testing.T) {
|
||||
token := "valid-token"
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
var updatedUser *entities.User
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
updateFunc: func(ctx context.Context, u *entities.User) error {
|
||||
updatedUser = u
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if updatedUser == nil {
|
||||
t.Fatal("User was not updated")
|
||||
}
|
||||
|
||||
if !updatedUser.EmailVerified {
|
||||
t.Error("EmailVerified should be true")
|
||||
}
|
||||
|
||||
if updatedUser.EmailVerificationToken != nil {
|
||||
t.Error("EmailVerificationToken should be nil after verification")
|
||||
}
|
||||
|
||||
if updatedUser.EmailVerificationExpiry != nil {
|
||||
t.Error("EmailVerificationExpiry should be nil after verification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_EmptyToken(t *testing.T) {
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: make(map[string]*entities.User),
|
||||
}
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: "",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_TokenNotFound(t *testing.T) {
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: make(map[string]*entities.User),
|
||||
}
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: "non-existent-token",
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_AlreadyVerified(t *testing.T) {
|
||||
token := "valid-token"
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = true
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrAlreadyExists {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrAlreadyExists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_ExpiredToken(t *testing.T) {
|
||||
token := "expired-token"
|
||||
expiry := time.Now().Add(-1 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_NilExpiry(t *testing.T) {
|
||||
token := "valid-token"
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = nil
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_WithWelcomeEmail(t *testing.T) {
|
||||
token := "valid-token"
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
}
|
||||
|
||||
emailService := &mockEmailService{}
|
||||
handler := NewVerifyEmailHandler(repo, emailService, true)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if len(emailService.sentMessages) != 1 {
|
||||
t.Fatalf("Expected 1 email sent, got %d", len(emailService.sentMessages))
|
||||
}
|
||||
|
||||
sentEmail := emailService.sentMessages[0]
|
||||
if sentEmail.To != "test@example.com" {
|
||||
t.Errorf("Email To = %v, want %v", sentEmail.To, "test@example.com")
|
||||
}
|
||||
|
||||
if sentEmail.Subject != "Welcome to Apocapoc!" {
|
||||
t.Errorf("Email Subject = %v, want %v", sentEmail.Subject, "Welcome to Apocapoc!")
|
||||
}
|
||||
|
||||
if !sentEmail.IsHTML {
|
||||
t.Error("Email should be HTML")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_WithoutWelcomeEmail(t *testing.T) {
|
||||
token := "valid-token"
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
}
|
||||
|
||||
emailService := &mockEmailService{}
|
||||
handler := NewVerifyEmailHandler(repo, emailService, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if len(emailService.sentMessages) != 0 {
|
||||
t.Errorf("Expected 0 emails sent, got %d", len(emailService.sentMessages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmailHandler_UpdateError(t *testing.T) {
|
||||
token := "valid-token"
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
|
||||
user := entities.NewUser("test@example.com", "hashedPassword", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
user.EmailVerificationToken = &token
|
||||
user.EmailVerificationExpiry = &expiry
|
||||
|
||||
repo := &mockVerifyEmailUserRepo{
|
||||
users: map[string]*entities.User{
|
||||
token: user,
|
||||
},
|
||||
updateFunc: func(ctx context.Context, u *entities.User) error {
|
||||
return errors.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewVerifyEmailHandler(repo, nil, false)
|
||||
|
||||
cmd := VerifyEmailCommand{
|
||||
Token: token,
|
||||
}
|
||||
|
||||
err := handler.Handle(context.Background(), cmd)
|
||||
if err == nil {
|
||||
t.Fatal("Handle() expected error but got nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"apocapoc-api/internal/domain/entities"
|
||||
"apocapoc-api/internal/shared/errors"
|
||||
)
|
||||
|
||||
type mockLoginUserRepo struct {
|
||||
findByEmailFunc func(ctx context.Context, email string) (*entities.User, error)
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
if m.findByEmailFunc != nil {
|
||||
return m.findByEmailFunc(ctx, email)
|
||||
}
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) FindByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) FindByVerificationToken(ctx context.Context, token string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) Create(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) Update(ctx context.Context, user *entities.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLoginUserRepo) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockLoginPasswordHasher struct {
|
||||
compareFunc func(hashedPassword, password string) error
|
||||
}
|
||||
|
||||
func (m *mockLoginPasswordHasher) Hash(password string) (string, error) {
|
||||
return "hashed_" + password, nil
|
||||
}
|
||||
|
||||
func (m *mockLoginPasswordHasher) Compare(hashedPassword, password string) error {
|
||||
if m.compareFunc != nil {
|
||||
return m.compareFunc(hashedPassword, password)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_Success(t *testing.T) {
|
||||
user := entities.NewUser("test@example.com", "hashed_password", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = true
|
||||
|
||||
repo := &mockLoginUserRepo{
|
||||
findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) {
|
||||
return user, nil
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockLoginPasswordHasher{
|
||||
compareFunc: func(hashedPassword, password string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
result, err := handler.Handle(context.Background(), query)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if result.UserID != "user-123" {
|
||||
t.Errorf("UserID = %v, want %v", result.UserID, "user-123")
|
||||
}
|
||||
|
||||
if result.Email != "test@example.com" {
|
||||
t.Errorf("Email = %v, want %v", result.Email, "test@example.com")
|
||||
}
|
||||
|
||||
if result.Timezone != "UTC" {
|
||||
t.Errorf("Timezone = %v, want %v", result.Timezone, "UTC")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_EmptyEmail(t *testing.T) {
|
||||
repo := &mockLoginUserRepo{}
|
||||
hasher := &mockLoginPasswordHasher{}
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
_, err := handler.Handle(context.Background(), query)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_EmptyPassword(t *testing.T) {
|
||||
repo := &mockLoginUserRepo{}
|
||||
hasher := &mockLoginPasswordHasher{}
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "test@example.com",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
_, err := handler.Handle(context.Background(), query)
|
||||
if err != errors.ErrInvalidInput {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrInvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_UserNotFound(t *testing.T) {
|
||||
repo := &mockLoginUserRepo{
|
||||
findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) {
|
||||
return nil, errors.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockLoginPasswordHasher{}
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "nonexistent@example.com",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
_, err := handler.Handle(context.Background(), query)
|
||||
if err != errors.ErrNotFound {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_InvalidPassword(t *testing.T) {
|
||||
user := entities.NewUser("test@example.com", "hashed_password", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = true
|
||||
|
||||
repo := &mockLoginUserRepo{
|
||||
findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) {
|
||||
return user, nil
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockLoginPasswordHasher{
|
||||
compareFunc: func(hashedPassword, password string) error {
|
||||
return errors.ErrInvalidInput
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "test@example.com",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
|
||||
_, err := handler.Handle(context.Background(), query)
|
||||
if err != errors.ErrNotFound {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginUserHandler_EmailNotVerified(t *testing.T) {
|
||||
user := entities.NewUser("test@example.com", "hashed_password", "UTC")
|
||||
user.ID = "user-123"
|
||||
user.EmailVerified = false
|
||||
|
||||
repo := &mockLoginUserRepo{
|
||||
findByEmailFunc: func(ctx context.Context, email string) (*entities.User, error) {
|
||||
return user, nil
|
||||
},
|
||||
}
|
||||
|
||||
hasher := &mockLoginPasswordHasher{
|
||||
compareFunc: func(hashedPassword, password string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewLoginUserHandler(repo, hasher)
|
||||
|
||||
query := LoginUserQuery{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
_, err := handler.Handle(context.Background(), query)
|
||||
if err != errors.ErrEmailNotVerified {
|
||||
t.Errorf("Handle() error = %v, want %v", err, errors.ErrEmailNotVerified)
|
||||
}
|
||||
}
|
||||
@@ -182,3 +182,87 @@ func TestRefreshTokenHandler_EmptyToken(t *testing.T) {
|
||||
t.Errorf("Expected ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRefreshToken(t *testing.T) {
|
||||
token1, err := GenerateRefreshToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRefreshToken() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if token1 == "" {
|
||||
t.Fatal("GenerateRefreshToken() returned empty token")
|
||||
}
|
||||
|
||||
token2, err := GenerateRefreshToken()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateRefreshToken() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if token1 == token2 {
|
||||
t.Error("GenerateRefreshToken() generated identical tokens")
|
||||
}
|
||||
|
||||
if len(token1) < 20 {
|
||||
t.Errorf("GenerateRefreshToken() token too short: %d characters", len(token1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRefreshToken(t *testing.T) {
|
||||
userID := "user-123"
|
||||
expiryDuration := 7 * 24 * time.Hour
|
||||
|
||||
token, err := CreateRefreshToken(userID, expiryDuration)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRefreshToken() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
t.Fatal("CreateRefreshToken() returned nil")
|
||||
}
|
||||
|
||||
if token.UserID != userID {
|
||||
t.Errorf("UserID = %v, want %v", token.UserID, userID)
|
||||
}
|
||||
|
||||
if token.Token == "" {
|
||||
t.Error("Token is empty")
|
||||
}
|
||||
|
||||
if token.ExpiresAt.IsZero() {
|
||||
t.Error("ExpiresAt is zero")
|
||||
}
|
||||
|
||||
if token.CreatedAt.IsZero() {
|
||||
t.Error("CreatedAt is zero")
|
||||
}
|
||||
|
||||
expectedExpiry := time.Now().Add(expiryDuration)
|
||||
diff := token.ExpiresAt.Sub(expectedExpiry)
|
||||
if diff > time.Second || diff < -time.Second {
|
||||
t.Errorf("ExpiresAt difference too large: %v", diff)
|
||||
}
|
||||
|
||||
if !token.IsValid() {
|
||||
t.Error("Token should be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRefreshToken_MultipleCalls(t *testing.T) {
|
||||
token1, err := CreateRefreshToken("user-1", 24*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRefreshToken(1) unexpected error = %v", err)
|
||||
}
|
||||
|
||||
token2, err := CreateRefreshToken("user-2", 24*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRefreshToken(2) unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if token1.Token == token2.Token {
|
||||
t.Error("CreateRefreshToken() generated identical tokens for different users")
|
||||
}
|
||||
|
||||
if token1.UserID == token2.UserID {
|
||||
t.Error("UserIDs should be different")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user