//go:build permitio package cognitoauth import ( "log/slog" "os" "strings" "testing" "time" "github.com/joho/godotenv" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( // Test configuration for local PDP testPDPURL = "http://localhost:7766" testUserID = "test-user-1" testResource = "document" testAction = "read" ) func init() { // Load .env file if it exists (for tests) // Try multiple possible locations _ = godotenv.Load("../../.env") _ = godotenv.Load(".env") } // getTestAPIKey returns the test API key from environment - fails if not set func getTestAPIKey() string { key := os.Getenv("PERMIT_IO_API_KEY") if key == "" { panic("PERMIT_IO_API_KEY environment variable must be set to run Permit.io tests") } return key } // TestPermitIOClient_NewPermitIOClient verifies that a new Permit.io client can be created successfully func TestPermitIOClient_NewPermitIOClient(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) assert.NotNil(t, client) assert.NotNil(t, client.client) assert.Equal(t, logger, client.logger) } // TestPermitIOClient_CheckPermission tests the CheckPermission method with a real local PDP // This test requires the local PDP to be running as described in the demo.sh script func TestPermitIOClient_CheckPermission(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Test successful permission check t.Run("ValidPermissionCheck", func(t *testing.T) { permitted, err := client.CheckPermission(testUserID, testAction, testResource) // Even if permission is denied, the call should succeed without error assert.NoError(t, err) assert.IsType(t, bool(false), permitted) // Log the result for debugging t.Logf("Permission check result: user=%s, action=%s, resource=%s, permitted=%v", testUserID, testAction, testResource, permitted) }) // Test with different actions t.Run("DifferentActions", func(t *testing.T) { actions := []string{"GET", "POST", "PUT", "DELETE"} for _, action := range actions { t.Run(action, func(t *testing.T) { permitted, err := client.CheckPermission(testUserID, action, testResource) assert.NoError(t, err) assert.IsType(t, bool(false), permitted) t.Logf("Action %s result: permitted=%v", action, permitted) }) } }) // Test with different resources t.Run("DifferentResources", func(t *testing.T) { resources := []string{"document", "query-endpoint", "queryAPI"} for _, resource := range resources { t.Run(resource, func(t *testing.T) { permitted, err := client.CheckPermission(testUserID, testAction, resource) assert.NoError(t, err) assert.IsType(t, bool(false), permitted) t.Logf("Resource %s result: permitted=%v", resource, permitted) }) } }) } // TestPermitIOClient_CheckPermission_ErrorHandling tests error scenarios func TestPermitIOClient_CheckPermission_ErrorHandling(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) t.Run("InvalidPDPURL", func(t *testing.T) { // Use an invalid URL to test error handling client := NewPermitIOClient(getTestAPIKey(), "http://invalid-url:9999", logger) permitted, err := client.CheckPermission(testUserID, testAction, testResource) // Should return an error and false for permission assert.Error(t, err) assert.False(t, permitted) }) t.Run("EmptyUserID", func(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Test with empty user ID permitted, err := client.CheckPermission("", testAction, testResource) // This might succeed or fail depending on Permit.io's handling of empty user IDs // The important thing is that we handle it gracefully t.Logf("Empty user ID result: permitted=%v, error=%v", permitted, err) assert.IsType(t, bool(false), permitted) }) } // TestPermitIOClient_CheckPermission_Timeout tests that the client handles timeouts appropriately func TestPermitIOClient_CheckPermission_Timeout(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Test that the client completes within a reasonable time start := time.Now() _, err := client.CheckPermission(testUserID, testAction, testResource) elapsed := time.Since(start) // Should complete within 30 seconds (generous timeout for testing) assert.True(t, elapsed < 30*time.Second, "Permission check took too long: %v", elapsed) // Log results even if there's an error (might be expected if PDP is not running) t.Logf("Permission check completed in %v, error: %v", elapsed, err) } // TestPermitIOClient_Integration is a comprehensive integration test // This test requires the local PDP to be running func TestPermitIOClient_Integration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Test scenarios that mirror the actual API endpoints testCases := []struct { name string userID string resource string action string }{ {"DocumentRead", "test-user-1", "document", "read"}, {"DocumentCreate", "test-user-1", "document", "create"}, {"QueryRead", "test-user-2", "query-endpoint", "read"}, {"QueryAPIRead", "test-user-3", "queryAPI", "read"}, {"AdminStuffRead", "admin-user", "admin-stuff", "read"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { permitted, err := client.CheckPermission(tc.userID, tc.action, tc.resource) // Log all results for debugging purposes t.Logf("Test case %s: user=%s, resource=%s, action=%s, permitted=%v, error=%v", tc.name, tc.userID, tc.resource, tc.action, permitted, err) // We don't assert specific permission results since the test environment // may not have specific user/role configurations set up // The important thing is that the calls complete without panicking assert.IsType(t, bool(false), permitted) }) } } // TestPermitIOClient_ConfiguredUsers tests with actual users configured in Permit.io // This test uses the users and roles defined in permitio_test_config.go func TestPermitIOClient_ConfiguredUsers(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Test all configured users with all actions on document resource for _, user := range TestUsers { for _, action := range TestResources["document"].AvailableActions { t.Run(user.Name+"_"+action+"_document", func(t *testing.T) { permitted, err := client.CheckPermission(user.Key, action, "document") // Calculate expected result based on test configuration expectedPermitted := HasPermission(user.Key, "document", action) t.Logf("User: %s (%s), Action: %s, Resource: document", user.Name, user.Key, action) t.Logf("User Roles: %v", user.Roles) t.Logf("Expected: %v, Actual: %v, Error: %v", expectedPermitted, permitted, err) // Assert no error occurred assert.NoError(t, err, "Permission check should not fail") // Assert that the actual result matches expectations if expectedPermitted != permitted { t.Logf("MISMATCH: Expected %v but got %v for user %s (%s) doing %s on document", expectedPermitted, permitted, user.Name, user.Key, action) t.Logf("User roles: %v", user.Roles) for role, actions := range TestResources["document"].RolePermissions { t.Logf("Role %s can do: %v", role, actions) } // FAIL the test when expectations don't match actual results t.Errorf("Permission expectation failed: expected %v, got %v", expectedPermitted, permitted) } else { t.Logf("MATCH: Permission result matches expectation") } }) } } } // TestPermitIOClient_SpecificUserPermissions tests specific permission scenarios func TestPermitIOClient_SpecificUserPermissions(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) testUser, exists := GetUserByName("test user") require.True(t, exists, "Test user should exist in configuration") johnSmith, exists := GetUserByName("john smith") require.True(t, exists, "John Smith should exist in configuration") testCases := []struct { name string user TestUser action string resource string expected bool }{ // Test user (viewer + editor roles) {"TestUser_CanRead", testUser, "read", "document", true}, // both viewer and editor can read {"TestUser_CanCreate", testUser, "create", "document", true}, // editor can create {"TestUser_CanUpdate", testUser, "update", "document", true}, // editor can update {"TestUser_CannotDelete", testUser, "delete", "document", false}, // editor cannot delete // John Smith (editor + viewer roles) {"JohnSmith_CanRead", johnSmith, "read", "document", true}, // both viewer and editor can read {"JohnSmith_CanCreate", johnSmith, "create", "document", true}, // editor can create {"JohnSmith_CanUpdate", johnSmith, "update", "document", true}, // editor can update {"JohnSmith_CannotDelete", johnSmith, "delete", "document", false}, // editor cannot delete } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { permitted, err := client.CheckPermission(tc.user.Key, tc.action, tc.resource) assert.NoError(t, err, "Permission check should not fail") t.Logf("User: %s (%s), Action: %s, Resource: %s", tc.user.Name, tc.user.Key, tc.action, tc.resource) t.Logf("User Roles: %v", tc.user.Roles) t.Logf("Expected: %v, Actual: %v", tc.expected, permitted) if tc.expected != permitted { t.Logf("EXPECTATION MISMATCH: Expected %v but got %v", tc.expected, permitted) // Log role permissions for debugging for role, actions := range TestResources[tc.resource].RolePermissions { t.Logf("Role %s permissions on %s: %v", role, tc.resource, actions) } // FAIL the test when expectations don't match actual results t.Errorf("Permission expectation failed: expected %v, got %v", tc.expected, permitted) } else { t.Logf("MATCH: Permission result matches expectation") } }) } } // BenchmarkPermitIOClient_CheckPermission benchmarks the permission check performance func BenchmarkPermitIOClient_CheckPermission(b *testing.B) { if testing.Short() { b.Skip("Skipping benchmark in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError})) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = client.CheckPermission(testUserID, testAction, testResource) } } // TestAPIKeyValidation tests API key validation scenarios for startup validation func TestAPIKeyValidation(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) t.Run("ValidAPIKey", func(t *testing.T) { // Test with the correct API key - should succeed (no error, regardless of permission result) client := NewPermitIOClient(getTestAPIKey(), testPDPURL, logger) // Use dummy test data for validation testUser := "startup-validation-user-12345" testAction := "read" testResource := "startup-validation-resource" permitted, err := client.CheckPermission(testUser, testAction, testResource) // Should not have an error (even if permission is denied) assert.NoError(t, err, "Valid API key should not produce errors") assert.IsType(t, bool(false), permitted) t.Logf("Valid API key test: permitted=%v, error=%v", permitted, err) }) t.Run("InvalidAPIKey", func(t *testing.T) { // Modify the API key by changing the last character validKey := getTestAPIKey() require.NotEmpty(t, validKey, "Test API key must not be empty") // Change the last character to make it invalid invalidKey := validKey[:len(validKey)-1] + "X" if validKey[len(validKey)-1] == 'X' { invalidKey = validKey[:len(validKey)-1] + "Y" } client := NewPermitIOClient(invalidKey, testPDPURL, logger) // Use dummy test data for validation testUser := "startup-validation-user-12345" testAction := "read" testResource := "startup-validation-resource" permitted, err := client.CheckPermission(testUser, testAction, testResource) // Should have an error due to invalid API key assert.Error(t, err, "Invalid API key should produce an error") assert.False(t, permitted, "Invalid API key should result in denied permission") // Check that it's an authentication/API key error, not a connectivity error errStr := err.Error() t.Logf("Invalid API key error: %v", err) // Should be an API-related error, not a connectivity error assert.True(t, strings.Contains(errStr, "UnprocessableEntityError") || strings.Contains(errStr, "api_error") || strings.Contains(errStr, "Unauthorized") || strings.Contains(errStr, "authentication"), "Error should indicate API key/authentication issue, got: %s", errStr) }) }