package test import ( "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "errors" "testing" "time" ) func TestResetPasswordUseCase(t *testing.T) { newRepo := func(expiration time.Time) *mockUserRepo { repo := newMockUserRepo() repo.users[3] = user.User{ ID: 3, Email: "alice@example.com", PasswordResetCode: "hashed:reset-code-1", PasswordResetExpiration: expiration, } return repo } t.Run("The use case itself sets the password and clears the code in one write, independently of any handler", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if err != nil { t.Fatalf("expected no error, got %v", err) } if repo.resetPasswordCalls != 1 { t.Fatalf("expected ResetPassword to be called exactly once, got %d", repo.resetPasswordCalls) } if repo.lastResetPasswordUserId != 3 { t.Errorf("expected userId 3, got %d", repo.lastResetPasswordUserId) } if repo.lastResetPasswordHash != "hashed:newpassword1" { t.Errorf("expected hash %q, got %q", "hashed:newpassword1", repo.lastResetPasswordHash) } }) t.Run("The use case itself advances TokenVersion by one on every successful reset, independently of any handler", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) repo.users[3] = user.User{ ID: 3, Email: "alice@example.com", TokenVersion: 5, PasswordResetCode: "hashed:reset-code-1", PasswordResetExpiration: time.Now().Add(time.Hour), } uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if err != nil { t.Fatalf("expected no error, got %v", err) } if repo.users[3].TokenVersion != 6 { t.Errorf("expected TokenVersion 6, got %d", repo.users[3].TokenVersion) } }) t.Run("The use case rejects a wrong code without touching the repository's ResetPassword method", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "wrong-code", "newpassword1") if !errors.Is(err, user.ErrInvalidPasswordResetCode) { t.Errorf("expected user.ErrInvalidPasswordResetCode, got %v", err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } }) t.Run("The use case rejects an expired code without touching the repository's ResetPassword method", func(t *testing.T) { repo := newRepo(time.Now().Add(-time.Hour)) uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if !errors.Is(err, user.ErrPasswordResetCodeExpired) { t.Errorf("expected user.ErrPasswordResetCodeExpired, got %v", err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } }) t.Run("The use case treats an unknown email the same as a wrong code", func(t *testing.T) { repo := newMockUserRepo() uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("nobody@example.com", "any-code", "newpassword1") if !errors.Is(err, user.ErrInvalidPasswordResetCode) { t.Errorf("expected user.ErrInvalidPasswordResetCode, got %v", err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } }) t.Run("The use case treats an empty stored code the same as a wrong code, never calling Compare with an empty hash", func(t *testing.T) { repo := newMockUserRepo() repo.users[3] = user.User{ID: 3, Email: "alice@example.com", PasswordResetCode: ""} hasher := &stubHasher{} uc := useCases.NewResetPasswordUseCase(repo, hasher, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "anything", "newpassword1") if !errors.Is(err, user.ErrInvalidPasswordResetCode) { t.Errorf("expected user.ErrInvalidPasswordResetCode, got %v", err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } if hasher.compareCalls != 0 { t.Errorf("expected Compare to never be called, got %d calls", hasher.compareCalls) } }) t.Run("The use case treats any GetByEmail failure, not just not-found, as an invalid code", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) repo.fail.failNextWith("GetByEmail", errors.New("db is down")) uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if !errors.Is(err, user.ErrInvalidPasswordResetCode) { t.Errorf("expected user.ErrInvalidPasswordResetCode, got %v", err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } }) t.Run("A hashing failure propagates and the repository's ResetPassword method is never called", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) hashErr := errors.New("hash blew up") uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{hashErr: hashErr}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if !errors.Is(err, hashErr) { t.Errorf("expected %v, got %v", hashErr, err) } if repo.resetPasswordCalls != 0 { t.Errorf("expected ResetPassword to never be called, got %d", repo.resetPasswordCalls) } }) t.Run("A failure in the repository's ResetPassword method propagates", func(t *testing.T) { repo := newRepo(time.Now().Add(time.Hour)) repo.fail.failNext("ResetPassword") uc := useCases.NewResetPasswordUseCase(repo, &stubHasher{}, newMockAPIKeyRepo(), time.Now) err := uc.ResetPassword("alice@example.com", "reset-code-1", "newpassword1") if !errors.Is(err, errRepositoryFailure) { t.Errorf("expected %v, got %v", errRepositoryFailure, err) } }) }