gohorsejobs/backend/internal/infrastructure/auth/jwt_service_test.go

348 lines
11 KiB
Go

package auth_test
import (
"fmt"
"os"
"testing"
"github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth"
"github.com/stretchr/testify/assert"
)
func TestJWTService_HashAndVerifyPassword(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_HashAndVerifyPassword ===")
// Setup
os.Setenv("PASSWORD_PEPPER", "test-pepper")
defer os.Unsetenv("PASSWORD_PEPPER")
service := auth.NewJWTService("secret", "issuer")
t.Run("Should hash and verify password correctly", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing password hash and verify")
password := "mysecurepassword"
hash, err := service.HashPassword(password)
fmt.Printf("[TEST LOG] Hash generated: %s...\n", hash[:20])
assert.NoError(t, err)
assert.NotEmpty(t, hash)
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Password verification result: %v\n", valid)
assert.True(t, valid)
})
t.Run("Should fail verification with wrong password", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing wrong password rejection")
password := "password"
hash, _ := service.HashPassword(password)
valid := service.VerifyPassword(hash, "wrong-password")
fmt.Printf("[TEST LOG] Wrong password verification result: %v (expected false)\n", valid)
assert.False(t, valid)
})
t.Run("Should fail verification with wrong pepper", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing wrong pepper rejection")
password := "password"
hash, _ := service.HashPassword(password)
// Change pepper
os.Setenv("PASSWORD_PEPPER", "wrong-pepper")
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Wrong pepper verification result: %v (expected false)\n", valid)
assert.False(t, valid)
// Reset pepper
os.Setenv("PASSWORD_PEPPER", "test-pepper")
})
}
func TestJWTService_HashPassword_NoPepper(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_HashPassword_NoPepper ===")
os.Unsetenv("PASSWORD_PEPPER")
defer os.Setenv("PASSWORD_PEPPER", "test-pepper")
service := auth.NewJWTService("secret", "issuer")
password := "password-no-pepper"
hash, err := service.HashPassword(password)
fmt.Printf("[TEST LOG] Hash without pepper: %s...\n", hash[:20])
assert.NoError(t, err)
assert.NotEmpty(t, hash)
// Should still verify (empty pepper is still valid)
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Verification without pepper: %v\n", valid)
assert.True(t, valid)
}
func TestJWTService_TokenOperations(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_TokenOperations ===")
service := auth.NewJWTService("secret", "issuer")
t.Run("Should generate and validate token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing token generation and validation")
userID := "user-123"
tenantID := "tenant-456"
roles := []string{"admin"}
token, err := service.GenerateToken(userID, tenantID, roles)
fmt.Printf("[TEST LOG] Token generated: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
claims, err := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Claims: sub=%v, tenant=%v\n", claims["sub"], claims["tenant"])
assert.NoError(t, err)
assert.Equal(t, userID, claims["sub"])
assert.Equal(t, tenantID, claims["tenant"])
})
t.Run("Should fail invalid token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid token rejection")
claims, err := service.ValidateToken("invalid-token")
fmt.Printf("[TEST LOG] Invalid token error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
}
func TestJWTService_GenerateToken_ExpirationParsing(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_GenerateToken_ExpirationParsing ===")
service := auth.NewJWTService("secret", "issuer")
t.Run("Default expiration (no env)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing default expiration (24h)")
os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with default expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
claims, _ := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Token claims: exp=%v\n", claims["exp"])
assert.NotNil(t, claims["exp"])
})
t.Run("Days format (7d)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing days format expiration (7d)")
os.Setenv("JWT_EXPIRATION", "7d")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with 7d expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Duration format (2h)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing duration format expiration (2h)")
os.Setenv("JWT_EXPIRATION", "2h")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with 2h expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Invalid days format fallback", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid days format (abcd)")
os.Setenv("JWT_EXPIRATION", "abcd")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with invalid format (fallback): %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Invalid day number fallback", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid day number (xxd)")
os.Setenv("JWT_EXPIRATION", "xxd")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with xxd format (fallback): %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
}
func TestJWTService_ValidateToken_WrongSigningMethod(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_ValidateToken_WrongSigningMethod ===")
service := auth.NewJWTService("secret", "issuer")
// A token signed with a different algorithm would fail validation
// This is hard to test directly, but we can test with a malformed token
t.Run("Malformed token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing malformed token")
claims, err := service.ValidateToken("eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.")
fmt.Printf("[TEST LOG] Malformed token error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
t.Run("Token with different secret", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing token from different secret")
otherService := auth.NewJWTService("different-secret", "issuer")
token, _ := otherService.GenerateToken("user", "tenant", []string{"role"})
claims, err := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Wrong secret error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
}
func TestJWTService_NewJWTService(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_NewJWTService ===")
service := auth.NewJWTService("my-secret", "my-issuer")
fmt.Printf("[TEST LOG] Service created: %v\n", service)
assert.NotNil(t, service)
}
// ============ ADDITIONAL TESTS (10% expansion) ============
func TestJWTService_HashPassword_EmptyPassword(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_HashPassword_EmptyPassword ===")
os.Setenv("PASSWORD_PEPPER", "test-pepper")
defer os.Unsetenv("PASSWORD_PEPPER")
service := auth.NewJWTService("secret", "issuer")
t.Run("Empty password should still hash", func(t *testing.T) {
hash, err := service.HashPassword("")
fmt.Printf("[TEST LOG] Empty password hash: %s...\n", hash[:20])
assert.NoError(t, err)
assert.NotEmpty(t, hash)
// Should verify with empty password
valid := service.VerifyPassword(hash, "")
assert.True(t, valid)
})
}
func TestJWTService_PepperConsistency(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_PepperConsistency ===")
t.Run("Same pepper produces verifiable hash", func(t *testing.T) {
os.Setenv("PASSWORD_PEPPER", "consistent-pepper")
service := auth.NewJWTService("secret", "issuer")
password := "Admin@2025!"
hash1, _ := service.HashPassword(password)
// Verify with same pepper
valid := service.VerifyPassword(hash1, password)
assert.True(t, valid, "Password should verify with same pepper")
os.Unsetenv("PASSWORD_PEPPER")
})
t.Run("Different peppers produce different hashes", func(t *testing.T) {
service := auth.NewJWTService("secret", "issuer")
os.Setenv("PASSWORD_PEPPER", "pepper-1")
hash1, _ := service.HashPassword("password")
os.Setenv("PASSWORD_PEPPER", "pepper-2")
hash2, _ := service.HashPassword("password")
// Hashes should be different
assert.NotEqual(t, hash1, hash2, "Different peppers should produce different hashes")
os.Unsetenv("PASSWORD_PEPPER")
})
}
func TestJWTService_TokenClaims_Content(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_TokenClaims_Content ===")
service := auth.NewJWTService("secret", "issuer")
t.Run("Token should contain all expected claims", func(t *testing.T) {
userID := "019b51a9-385f-7416-be8c-9960727531a3"
tenantID := "tenant-xyz"
roles := []string{"admin", "superadmin"}
token, err := service.GenerateToken(userID, tenantID, roles)
assert.NoError(t, err)
claims, err := service.ValidateToken(token)
assert.NoError(t, err)
// Check all claims
assert.Equal(t, userID, claims["sub"])
assert.Equal(t, tenantID, claims["tenant"])
assert.Equal(t, "issuer", claims["iss"])
assert.NotNil(t, claims["exp"])
assert.NotNil(t, claims["iat"])
// Check roles
rolesFromClaims := claims["roles"].([]interface{})
assert.Len(t, rolesFromClaims, 2)
})
}
func TestJWTService_LongPassword(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_LongPassword ===")
os.Setenv("PASSWORD_PEPPER", "test-pepper")
defer os.Unsetenv("PASSWORD_PEPPER")
service := auth.NewJWTService("secret", "issuer")
t.Run("Very long password should work", func(t *testing.T) {
// bcrypt has a 72 byte limit, test behavior
longPassword := "ShortPasswordButLongEnoughForTest"
hash, err := service.HashPassword(longPassword)
assert.NoError(t, err)
assert.NotEmpty(t, hash)
valid := service.VerifyPassword(hash, longPassword)
assert.True(t, valid)
})
}
func TestJWTService_SpecialCharactersPassword(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_SpecialCharactersPassword ===")
os.Setenv("PASSWORD_PEPPER", "test-pepper")
defer os.Unsetenv("PASSWORD_PEPPER")
service := auth.NewJWTService("secret", "issuer")
specialPasswords := []string{
"Admin@2025!",
"Pässwörd123",
"密码123",
"パスワード",
"🔐SecurePass!",
"test\nwith\nnewlines",
"tab\there",
}
for _, password := range specialPasswords {
t.Run("Password: "+password[:min(10, len(password))], func(t *testing.T) {
hash, err := service.HashPassword(password)
assert.NoError(t, err)
assert.NotEmpty(t, hash)
valid := service.VerifyPassword(hash, password)
assert.True(t, valid, "Should verify password with special chars")
})
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}