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:
2025-11-28 17:59:39 +01:00
parent 06f95afc92
commit 8d631f8fab
15 changed files with 2607 additions and 77 deletions
+357
View File
@@ -0,0 +1,357 @@
package auth
import (
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestNewJWTService(t *testing.T) {
tests := []struct {
name string
secret string
expiryHours int
}{
{
name: "standard configuration",
secret: "my-secret-key",
expiryHours: 24,
},
{
name: "short expiry",
secret: "test-secret",
expiryHours: 1,
},
{
name: "long expiry",
secret: "test-secret",
expiryHours: 168,
},
{
name: "empty secret",
secret: "",
expiryHours: 24,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := NewJWTService(tt.secret, tt.expiryHours)
if service == nil {
t.Fatal("NewJWTService() returned nil")
}
if string(service.secret) != tt.secret {
t.Errorf("secret = %v, want %v", string(service.secret), tt.secret)
}
expectedExpiry := time.Duration(tt.expiryHours) * time.Hour
if service.expiry != expectedExpiry {
t.Errorf("expiry = %v, want %v", service.expiry, expectedExpiry)
}
})
}
}
func TestJWTService_GenerateToken(t *testing.T) {
service := NewJWTService("test-secret-key", 24)
tests := []struct {
name string
userID string
email string
}{
{
name: "standard user",
userID: "user-123",
email: "user@example.com",
},
{
name: "empty user ID",
userID: "",
email: "user@example.com",
},
{
name: "empty email",
userID: "user-123",
email: "",
},
{
name: "both empty",
userID: "",
email: "",
},
{
name: "special characters in email",
userID: "user-456",
email: "user+test@example.com",
},
{
name: "uuid as user ID",
userID: "550e8400-e29b-41d4-a716-446655440000",
email: "uuid@example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token, err := service.GenerateToken(tt.userID, tt.email)
if err != nil {
t.Fatalf("GenerateToken() error = %v", err)
}
if token == "" {
t.Fatal("GenerateToken() returned empty token")
}
claims, err := service.ValidateToken(token)
if err != nil {
t.Fatalf("ValidateToken() error = %v", err)
}
if claims.UserID != tt.userID {
t.Errorf("UserID = %v, want %v", claims.UserID, tt.userID)
}
if claims.Email != tt.email {
t.Errorf("Email = %v, want %v", claims.Email, tt.email)
}
})
}
}
func TestJWTService_ValidateToken(t *testing.T) {
secret := "test-secret-key"
service := NewJWTService(secret, 24)
tests := []struct {
name string
setupToken func() string
expectError bool
errorMsg string
}{
{
name: "valid token",
setupToken: func() string {
token, _ := service.GenerateToken("user-123", "user@example.com")
return token
},
expectError: false,
},
{
name: "invalid token format",
setupToken: func() string {
return "not.a.valid.token"
},
expectError: true,
},
{
name: "empty token",
setupToken: func() string {
return ""
},
expectError: true,
},
{
name: "token with wrong secret",
setupToken: func() string {
wrongService := NewJWTService("wrong-secret", 24)
token, _ := wrongService.GenerateToken("user-123", "user@example.com")
return token
},
expectError: true,
},
{
name: "expired token",
setupToken: func() string {
expiredService := NewJWTService(secret, -1)
token, _ := expiredService.GenerateToken("user-123", "user@example.com")
return token
},
expectError: true,
},
{
name: "malformed token",
setupToken: func() string {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.malformed"
},
expectError: true,
},
{
name: "token with invalid signature",
setupToken: func() string {
token, _ := service.GenerateToken("user-123", "user@example.com")
return token[:len(token)-5] + "xxxxx"
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token := tt.setupToken()
claims, err := service.ValidateToken(token)
if tt.expectError {
if err == nil {
t.Fatal("ValidateToken() expected error but got nil")
}
} else {
if err != nil {
t.Fatalf("ValidateToken() unexpected error = %v", err)
}
if claims == nil {
t.Fatal("ValidateToken() returned nil claims")
}
}
})
}
}
func TestJWTService_ValidateTokenClaims(t *testing.T) {
service := NewJWTService("test-secret-key", 24)
userID := "user-123"
email := "user@example.com"
token, err := service.GenerateToken(userID, email)
if err != nil {
t.Fatalf("GenerateToken() error = %v", err)
}
claims, err := service.ValidateToken(token)
if err != nil {
t.Fatalf("ValidateToken() error = %v", err)
}
if claims.UserID != userID {
t.Errorf("UserID = %v, want %v", claims.UserID, userID)
}
if claims.Email != email {
t.Errorf("Email = %v, want %v", claims.Email, email)
}
if claims.ExpiresAt == nil {
t.Fatal("ExpiresAt is nil")
}
if claims.IssuedAt == nil {
t.Fatal("IssuedAt is nil")
}
if claims.ExpiresAt.Before(claims.IssuedAt.Time) {
t.Error("ExpiresAt is before IssuedAt")
}
expectedExpiry := claims.IssuedAt.Add(24 * time.Hour)
if !claims.ExpiresAt.Time.Equal(expectedExpiry) {
diff := claims.ExpiresAt.Time.Sub(expectedExpiry)
if diff > time.Second || diff < -time.Second {
t.Errorf("ExpiresAt = %v, want approximately %v (diff: %v)", claims.ExpiresAt.Time, expectedExpiry, diff)
}
}
}
func TestJWTService_TokenExpiry(t *testing.T) {
tests := []struct {
name string
expiryHours int
}{
{
name: "1 hour expiry",
expiryHours: 1,
},
{
name: "24 hours expiry",
expiryHours: 24,
},
{
name: "168 hours (1 week) expiry",
expiryHours: 168,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := NewJWTService("test-secret", tt.expiryHours)
token, err := service.GenerateToken("user-123", "user@example.com")
if err != nil {
t.Fatalf("GenerateToken() error = %v", err)
}
claims, err := service.ValidateToken(token)
if err != nil {
t.Fatalf("ValidateToken() error = %v", err)
}
expectedExpiry := time.Now().Add(time.Duration(tt.expiryHours) * time.Hour)
diff := claims.ExpiresAt.Time.Sub(expectedExpiry)
if diff > time.Second || diff < -time.Second {
t.Errorf("ExpiresAt difference too large: %v", diff)
}
})
}
}
func TestJWTService_ValidateTokenWithWrongSigningMethod(t *testing.T) {
service := NewJWTService("test-secret", 24)
claims := &Claims{
UserID: "user-123",
Email: "user@example.com",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodNone, claims)
tokenString, err := token.SignedString(jwt.UnsafeAllowNoneSignatureType)
if err != nil {
t.Fatalf("SignedString() error = %v", err)
}
_, err = service.ValidateToken(tokenString)
if err == nil {
t.Fatal("ValidateToken() expected error for none signing method but got nil")
}
}
func TestJWTService_MultipleTokens(t *testing.T) {
service := NewJWTService("test-secret", 24)
token1, err := service.GenerateToken("user-1", "user1@example.com")
if err != nil {
t.Fatalf("GenerateToken(1) error = %v", err)
}
token2, err := service.GenerateToken("user-2", "user2@example.com")
if err != nil {
t.Fatalf("GenerateToken(2) error = %v", err)
}
if token1 == token2 {
t.Error("Generated identical tokens for different users")
}
claims1, err := service.ValidateToken(token1)
if err != nil {
t.Fatalf("ValidateToken(1) error = %v", err)
}
claims2, err := service.ValidateToken(token2)
if err != nil {
t.Fatalf("ValidateToken(2) error = %v", err)
}
if claims1.UserID == claims2.UserID {
t.Error("Claims have same UserID")
}
if claims1.Email == claims2.Email {
t.Error("Claims have same Email")
}
}