2ff6e05ffc
Permit integration - part 1 * WIP permit integration * slim down build * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/permit-integration-1 * omit swagger from auth * clean build * comment * part 1 completed this is the initial permitio parts and tests without integration. Also testing.md since we have a new test target `task test:permitio` * feature flag for no jwt validation * permit integration * fix ci unit tests * ci fix * build fix * fix home handler * fix redirect * test fix * update docs for auth
382 lines
14 KiB
Go
382 lines
14 KiB
Go
//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)
|
|
})
|
|
}
|