348 lines
11 KiB
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
|
|
}
|