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,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