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 := "ThisIsAVeryLongPasswordThatExceeds72BytesWhichIsTheMaxForBcryptSoItWillBeTruncated123" 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 }