package security_test import ( "context" "testing" "time" "github.com/AmaniQuery/amaniquery/internal/security" ) // TestJWTAuthenticator_GenerateAndValidate tests the full JWT lifecycle func TestJWTAuthenticator_GenerateAndValidate(t *testing.T) { cfg := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "test-issuer", Audience: "test-audience", TokenDuration: time.Hour, } auth := security.NewJWTAuthenticator(cfg) // Generate token token, err := auth.GenerateToken("user-123", "test@example.com", []string{"user", "admin"}, "tenant-1") if err != nil { t.Fatalf("GenerateToken failed: %v", err) } if token == "" { t.Error("Expected non-empty token") } // Validate token claims, err := auth.ValidateToken(token) if err != nil { t.Fatalf("ValidateToken failed: %v", err) } if claims.UserID != "user-123" { t.Errorf("Expected UserID 'user-123', got '%s'", claims.UserID) } if claims.Email != "test@example.com" { t.Errorf("Expected Email 'test@example.com', got '%s'", claims.Email) } if len(claims.Roles) != 2 { t.Errorf("Expected 2 roles, got %d", len(claims.Roles)) } if claims.TenantID != "tenant-1" { t.Errorf("Expected TenantID 'tenant-1', got '%s'", claims.TenantID) } } // TestJWTAuthenticator_InvalidToken tests handling of invalid tokens func TestJWTAuthenticator_InvalidToken(t *testing.T) { cfg := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "test-issuer", TokenDuration: time.Hour, } auth := security.NewJWTAuthenticator(cfg) testCases := []struct { name string token string }{ {"empty token", ""}, {"garbage token", "not-a-valid-token"}, {"tampered token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { _, err := auth.ValidateToken(tc.token) if err == nil { t.Error("Expected error for invalid token") } }) } } // TestJWTAuthenticator_ExpiredToken tests handling of expired tokens func TestJWTAuthenticator_ExpiredToken(t *testing.T) { cfg := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "test-issuer", TokenDuration: -time.Hour, // Already expired } auth := security.NewJWTAuthenticator(cfg) token, err := auth.GenerateToken("user-123", "test@example.com", []string{"user"}, "") if err != nil { t.Fatalf("GenerateToken failed: %v", err) } // Validation should fail due to expiration _, err = auth.ValidateToken(token) if err == nil { t.Error("Expected error for expired token") } } // TestJWTAuthenticator_DifferentSecrets tests that different secrets fail validation func TestJWTAuthenticator_DifferentSecrets(t *testing.T) { cfg1 := security.JWTConfig{ Secret: []byte("first-secret-key-at-least-32-chars!"), Issuer: "test-issuer", TokenDuration: time.Hour, } cfg2 := security.JWTConfig{ Secret: []byte("second-secret-key-at-least-32-chars"), Issuer: "test-issuer", TokenDuration: time.Hour, } auth1 := security.NewJWTAuthenticator(cfg1) auth2 := security.NewJWTAuthenticator(cfg2) token, err := auth1.GenerateToken("user-123", "test@example.com", []string{"user"}, "") if err != nil { t.Fatalf("GenerateToken failed: %v", err) } // Should fail with different secret _, err = auth2.ValidateToken(token) if err == nil { t.Error("Expected error when validating with different secret") } } // TestJWTAuthenticator_IssuerValidation tests issuer validation func TestJWTAuthenticator_IssuerValidation(t *testing.T) { cfg1 := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "issuer-1", TokenDuration: time.Hour, } cfg2 := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "issuer-2", TokenDuration: time.Hour, } auth1 := security.NewJWTAuthenticator(cfg1) auth2 := security.NewJWTAuthenticator(cfg2) token, err := auth1.GenerateToken("user-123", "test@example.com", []string{"user"}, "") if err != nil { t.Fatalf("GenerateToken failed: %v", err) } // Should fail with different issuer _, err = auth2.ValidateToken(token) if err == nil { t.Error("Expected error for mismatched issuer") } } // TestContextWithClaims tests claims context operations func TestContextWithClaims(t *testing.T) { claims := &security.Claims{ UserID: "user-123", Email: "test@example.com", Roles: []string{"admin"}, } ctx := security.ContextWithClaims(context.Background(), claims) extracted, ok := security.ClaimsFromContext(ctx) if !ok { t.Fatal("Expected to extract claims from context") } if extracted.UserID != claims.UserID { t.Errorf("Expected UserID %s, got %s", claims.UserID, extracted.UserID) } } // TestClaimsFromContext_NoClaims tests extracting from context without claims func TestClaimsFromContext_NoClaims(t *testing.T) { ctx := context.Background() _, ok := security.ClaimsFromContext(ctx) if ok { t.Error("Expected no claims in empty context") } } // TestRequireRole tests role requirement checking func TestRequireRole(t *testing.T) { claims := &security.Claims{ UserID: "user-123", Roles: []string{"user", "editor"}, } ctx := security.ContextWithClaims(context.Background(), claims) // Should pass for existing role err := security.RequireRole(ctx, "user") if err != nil { t.Errorf("Expected no error for existing role: %v", err) } // Should fail for missing role err = security.RequireRole(ctx, "superadmin") if err == nil { t.Error("Expected error for missing role") } } // TestRequireRole_AdminBypass tests admin role bypass func TestRequireRole_AdminBypass(t *testing.T) { claims := &security.Claims{ UserID: "user-123", Roles: []string{"admin"}, } ctx := security.ContextWithClaims(context.Background(), claims) // Admin should have access to any role err := security.RequireRole(ctx, "any-role") if err != nil { t.Errorf("Expected admin to bypass role check: %v", err) } } // TestRequireAnyRole tests multiple role checking func TestRequireAnyRole(t *testing.T) { claims := &security.Claims{ UserID: "user-123", Roles: []string{"viewer"}, } ctx := security.ContextWithClaims(context.Background(), claims) // Should pass if user has any of the roles err := security.RequireAnyRole(ctx, "editor", "viewer", "admin") if err != nil { t.Errorf("Expected no error when user has one of the roles: %v", err) } // Should fail if user has none of the roles err = security.RequireAnyRole(ctx, "editor", "admin") if err == nil { t.Error("Expected error when user has none of the roles") } } // BenchmarkJWT_Generate benchmarks token generation func BenchmarkJWT_Generate(b *testing.B) { cfg := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "test-issuer", TokenDuration: time.Hour, } auth := security.NewJWTAuthenticator(cfg) b.ResetTimer() for i := 0; i < b.N; i++ { auth.GenerateToken("user-123", "test@example.com", []string{"admin"}, "tenant-1") } } // BenchmarkJWT_Validate benchmarks token validation func BenchmarkJWT_Validate(b *testing.B) { cfg := security.JWTConfig{ Secret: []byte("test-secret-key-at-least-32-chars!!"), Issuer: "test-issuer", TokenDuration: time.Hour, } auth := security.NewJWTAuthenticator(cfg) token, _ := auth.GenerateToken("user-123", "test@example.com", []string{"admin"}, "") b.ResetTimer() for i := 0; i < b.N; i++ { auth.ValidateToken(token) } }