Files
query-orchestration/internal/cognitoauth/permitio_test.go
T
Jay Brown 2ff6e05ffc Merged in feature/permit-integration-1 (pull request #172)
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
2025-07-11 19:27:14 +00:00

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)
})
}