README
¶
Entity Resolution Service Testing Guide
This guide covers the testing infrastructure for the OpenTDF Entity Resolution Service (ERS).
Overview
The ERS supports multiple backends for entity resolution:
- Claims: JWT token processing and claims extraction
- Keycloak: Identity provider integration with Admin API
- Multi-Strategy: Configurable multi-provider entity resolution (supports LDAP, SQL, and JWT claims providers)
Running Tests
All Tests
go test ./integration -v
Backend-Specific Tests
# Claims tests (fast)
go test ./integration -run TestClaims -v
# Multi-Strategy tests (fast, uses JWT claims provider)
go test ./integration -run TestMultiStrategy -v
# Keycloak tests (requires Docker, longer startup time)
go test ./integration -run TestKeycloak -v -timeout=10m
Skip Container-Based Tests
go test ./integration -v -short
Test Architecture
Contract Testing Framework
All ERS implementations use a unified contract testing framework that ensures consistent behavior across backends.
Key Components:
internal/contract_tests.go- Common test scenariosinternal/test_data.go- Standardized test datasetsinternal/contract_framework_test.go- Framework validation
Test Adapters
Each backend implements the ERSTestAdapter interface:
type ERSTestAdapter interface {
GetScopeName() string
SetupTestData(ctx context.Context, testDataSet *ContractTestDataSet) error
CreateERSService(ctx context.Context) (ERSImplementation, error)
TeardownTestData(ctx context.Context) error
}
Backend-Specific Testing
Claims Testing
File: claims_test.go
Features:
- JWT signing and validation infrastructure
- Claims entity processing
- Malformed/expired token handling
- No external dependencies
Configuration:
// Automatically generates RSA keys for JWT signing
adapter := NewClaimsTestAdapter()
Multi-Strategy Testing
File: multistrategy_test.go
Features:
- Configurable multi-provider entity resolution
- Strategy-based JWT claims processing
- Failure strategy testing (fail-fast vs continue)
- Entity chain creation and validation
- No external dependencies (uses JWT claims provider)
Configuration:
// Uses JWT claims provider for testing
adapter := NewMultiStrategyTestAdapter()
Supported Providers:
- JWT Claims Provider (used in tests)
- SQL Provider (SQLite/PostgreSQL support)
- LDAP Provider (enterprise directory integration)
Strategy Testing:
- Multiple mapping strategies with conditions
- JWT claim matching and processing
- Provider failover and fallback mechanisms
Keycloak Testing
File: keycloak_test.go
Features:
- Keycloak container with Admin API integration
- Real authentication flows
- User/client lifecycle management
Configuration:
config := &KeycloakTestConfig{
URL: "http://localhost:8080",
Realm: "opentdf",
ClientID: "test-client",
ClientSecret: "test-secret",
AdminUser: "admin",
AdminPass: "admin_password",
}
Container Management
Generic Container Helpers
File: internal/container_helpers.go
Provides generic container lifecycle management infrastructure:
// Each adapter creates its own container configuration
adapter := NewPostgreSQLTestAdapter()
config := adapter.createPostgreSQLContainerConfig()
manager := NewContainerManager(config)
// Start container
ctx := context.Background()
err := manager.Start(ctx)
// Get mapped port
port, err := manager.GetMappedPort(ctx, "5432")
// Cleanup
defer manager.Stop(ctx)
Container Test Suite
For managing multiple containers:
suite := NewContainerTestSuite()
// Each adapter provides its own configuration
postgresAdapter := NewPostgreSQLTestAdapter()
keycloakAdapter := NewKeycloakTestAdapter()
suite.AddContainer("postgres", postgresAdapter.createPostgreSQLContainerConfig())
suite.AddContainer("keycloak", keycloakAdapter.createKeycloakContainerConfig())
// Start all containers
err := suite.StartAll(ctx)
defer suite.StopAll(ctx)
Test Data
Standard Test Dataset
File: internal/test_data.go
Provides consistent test data across all backends:
type ContractTestDataSet struct {
Users []TestUser
Clients []TestClient
}
// Standard users: alice, bob, charlie
// Standard clients: test-client-1, test-client-2
Test Data Injection
Each adapter handles backend-specific data injection:
- Claims: JWT key generation and token creation
- Multi-Strategy: JWT claims extraction and strategy matching
- Keycloak: User/client creation via Admin API
Performance and Reliability
Test Execution Times
- Claims: ~0.1s (no external dependencies)
- Multi-Strategy: ~0.4s (JWT claims provider, no external dependencies)
- Keycloak: ~30s (includes container startup and configuration)
Reliability Features
- Automatic container cleanup on test completion
- Retry logic for container readiness checks
- Graceful handling of container lifecycle errors
- Parallel test execution safety
Troubleshooting
Common Issues
Docker/Podman Setup:
# For Podman users
export TESTCONTAINERS_PODMAN=true
export TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true
# For Colima users
export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock"
export TESTCONTAINERS_RYUK_DISABLED=true
Port Conflicts:
- Tests use random ports assigned by testcontainers
- No manual port configuration needed
Container Startup Timeouts:
# Increase timeout for slow systems
go test ./integration -timeout=15m
Debug Container Issues:
# Enable container logs
TESTCONTAINERS_DEBUG=true go test ./integration -v
Test Isolation
- Each test run uses fresh containers
- In-memory databases for SQLite tests
- Separate realms/databases per test execution
- No shared state between test runs
Integration with CI/CD
GitHub Actions
- name: Run Integration Tests
run: |
# Start Docker daemon
# Run tests with appropriate timeout
go test ./integration -v -timeout=15m
Local Development
# Quick tests during development
go test ./integration -run TestMultiStrategy -v
# Full test suite before commits
make test-integration
Developer Guide: Adding ERS Implementations and Tests
Quick Start: Adding a New ERS Implementation
To integrate a new Entity Resolution Service backend with the testing framework:
1. Create Your Test Adapter
Implement the ERSTestAdapter interface in a new file (e.g., mybackend_test.go):
type MyBackendTestAdapter struct {
config MyBackendConfig
// ... backend-specific fields
}
func NewMyBackendTestAdapter() *MyBackendTestAdapter {
return &MyBackendTestAdapter{
config: getMyBackendConfig(),
}
}
func (a *MyBackendTestAdapter) GetScopeName() string {
return "MyBackend"
}
func (a *MyBackendTestAdapter) SetupTestData(ctx context.Context, testDataSet *internal.ContractTestDataSet) error {
// Inject test users and clients into your backend
// Use testDataSet.Users and testDataSet.Clients
return nil
}
func (a *MyBackendTestAdapter) CreateERSService(ctx context.Context) (internal.ERSImplementation, error) {
// Return your ERS implementation
return mybackend.NewMyBackendERS(a.config), nil
}
func (a *MyBackendTestAdapter) TeardownTestData(ctx context.Context) error {
// Clean up any resources
return nil
}
2. Add Your Tests to the Unified Suite
func TestMyBackend_ContractTests(t *testing.T) {
adapter := NewMyBackendTestAdapter()
internal.RunContractTestSuite(t, adapter)
}
func TestMyBackend_EntityChainTests(t *testing.T) {
adapter := NewMyBackendTestAdapter()
internal.RunEntityChainContractTests(t, adapter)
}
That's it! Your backend will automatically run through all contract tests.
Best Practices for ERS Integration
✅ Use Dynamic Test Data
Good: Use the parameterized test data generators
func (a *MyBackendTestAdapter) SetupTestData(ctx context.Context, testDataSet *internal.ContractTestDataSet) error {
// Use provided test data
for _, user := range testDataSet.Users {
err := a.backend.CreateUser(user.Username, user.Email, user.DisplayName)
if err != nil {
return err
}
}
return nil
}
Avoid: Hardcoding test data
// Don't do this - hardcoded and inflexible
users := []User{
{Username: "alice", Email: "alice@test.com"},
{Username: "bob", Email: "bob@test.com"},
}
✅ Use Flexible Test Expectations
Good: Use range-based validation
// Test your specific business logic
expectation := internal.FlexibleEntityChainExpectation{
EphemeralID: "test-token-1",
MinEntityCount: 1, // At least 1 entity
MaxEntityCount: 5, // At most 5 entities
RequiredClaims: []string{"username"}, // Must have username
AllowImplementationGaps: true, // Accept variations
}
Avoid: Exact count expectations
// Don't do this - brittle
assert.Equal(t, 2, len(chain.Entities))
✅ Use Environment-Aware Configuration
Good: Leverage the configuration system
func getMyBackendConfig() MyBackendConfig {
config := internal.GetTestConfig()
return MyBackendConfig{
DatabaseURL: config.PostgresConnectionString("mydb", "user", "pass"),
Timeout: config.ContainerStartupTimeout,
// ... other configurable settings
}
}
Avoid: Hardcoded configuration
// Don't do this - not configurable
config := MyBackendConfig{
DatabaseURL: "postgres://localhost:5432/mydb",
Timeout: 2 * time.Minute,
}
✅ Generate Realistic JWTs for Testing
Good: Use dynamic JWT generation with multi-strategy routing support
// Basic JWT generation with current timestamps
aliceToken := internal.CreateTestJWT("web-client", "alice", "alice@company.com")
// Multi-strategy routing scenarios (uses different audiences for routing decisions)
internalToken := internal.CreateInternalJWT("web-client", "alice", "alice@company.com") // Routes to JWT claims
externalToken := internal.CreateExternalJWT("partner-app", "bob", "bob@partner.org", "ext_456") // Routes to DB lookup
customerToken := internal.CreateCustomerJWT("customer-app", "charlie", "charlie@customer.com") // Routes to customer DB
// Custom claims for specific test scenarios
customToken := internal.CreateTestJWTWithClaims("test-client", "testuser", "test@example.com", map[string]interface{}{
"aud": []string{"custom-audience"},
"iss": "custom-issuer",
"role": "admin",
})
// Complete test set for comprehensive multi-strategy testing
testSet := internal.CreateMultiStrategyTestSet()
// Returns map with keys: "internal-user", "external-partner", "customer-portal", "device-context", "fallback-email"
Avoid: Static test tokens
// Don't do this - same token everywhere, breaks multi-strategy routing tests
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Advanced Integration Patterns
Container-Based Backends
If your backend requires containers (databases, external services):
func (a *MyBackendTestAdapter) SetupTestData(ctx context.Context, testDataSet *internal.ContractTestDataSet) error {
// Start your container
config := internal.GetTestConfig()
containerConfig := internal.ContainerConfig{
Image: config.GetBackendImage(), // Configurable image
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_DB": "testdb",
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
},
WaitStrategy: wait.ForListeningPort("5432/tcp").
WithStartupTimeout(config.ContainerStartupTimeout),
Timeout: config.ContainerRunTimeout,
}
manager := internal.NewContainerManager(containerConfig)
err := manager.Start(ctx)
if err != nil {
return err
}
// Store manager for cleanup
a.containerManager = manager
// Inject test data
return a.injectTestData(testDataSet)
}
Multi-Strategy Patterns
For complex routing/strategy-based backends, use the built-in multi-strategy JWT helpers:
func NewMyMultiStrategyTestAdapter() *MyMultiStrategyTestAdapter {
return &MyMultiStrategyTestAdapter{
config: createMultiStrategyConfig(),
}
}
func (a *MyMultiStrategyTestAdapter) SetupTestData(ctx context.Context, testDataSet *internal.ContractTestDataSet) error {
// Use multi-strategy test set to create tokens for different routing scenarios
testSet := internal.CreateMultiStrategyTestSet()
// Test internal routing (JWT claims provider)
internalToken := internal.CreateInternalJWT("web-client", "alice", "alice@company.com")
a.testTokens["internal"] = internalToken
// Test external routing (database lookup provider)
externalToken := internal.CreateExternalJWT("partner-app", "bob", "bob@partner.org", "ext_user_456")
a.testTokens["external"] = externalToken
// Test environment context routing
deviceToken := internal.CreateEnvironmentJWT("mobile-app", "192.168.1.100", "device-12345")
a.testTokens["environment"] = deviceToken
return nil
}
func (a *MyMultiStrategyTestAdapter) testRoutingScenarios(t *testing.T) {
// Test that different audiences route to different providers
for scenario, token := range a.testTokens {
t.Run(fmt.Sprintf("Route_%s", scenario), func(t *testing.T) {
response := a.callERS(token)
// Verify the response matches expected routing behavior
a.validateRoutingStrategy(t, scenario, response)
})
}
}
Multi-Strategy JWT Audience Patterns:
internal,opentdf-internal→ JWT claims provider (zero-latency)external,partner,customer→ Database lookup providerdevice-context→ Environment entity provider- No specific audience → Fallback email lookup
Example Multi-Strategy Configuration Testing:
func (a *MyMultiStrategyTestAdapter) testFailureStrategies(t *testing.T) {
// Test fail-fast vs continue strategies
testCases := []struct {
strategy types.FailureStrategy
token string
expectError bool
}{
{types.FailureStrategyFailFast, "invalid-token", true},
{types.FailureStrategyContinue, "partial-token", false}, // Should try fallback strategies
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("FailureStrategy_%s", tc.strategy), func(t *testing.T) {
adapter := a.withFailureStrategy(tc.strategy)
result, err := adapter.resolveToken(tc.token)
if tc.expectError && err == nil {
t.Errorf("Expected error with %s strategy", tc.strategy)
}
if !tc.expectError && err != nil {
t.Errorf("Expected success with %s strategy, got: %v", tc.strategy, err)
}
})
}
}
Testing Your Integration
1. Validate JWT Processing
# Test JWT generation works correctly
go test ./integration/internal -v -run TestCreateTestJWT
2. Run Contract Tests
# Test your backend implementation
go test ./integration -run TestMyBackend -v
3. Test Configuration
# Test with custom configuration
TEST_POSTGRES_PORT=5433 TEST_CONTAINER_STARTUP_TIMEOUT=3m go test ./integration -run TestMyBackend -v
Common Patterns
Test Data Injection
func (a *MyBackendTestAdapter) injectUsers(users []internal.TestUser) error {
for _, user := range users {
// Inject each user using your backend's API
err := a.backend.CreateUser(internal.UserRequest{
Username: user.Username,
Email: user.Email,
DisplayName: user.DisplayName,
Groups: user.Groups,
})
if err != nil {
return fmt.Errorf("failed to create user %s: %w", user.Username, err)
}
}
return nil
}
Flexible Validation
func validateMyBackendResults(t *testing.T, chains []*entity.EntityChain) {
rule := internal.FlexibleChainValidationRule{
Description: "MyBackend entity chain validation",
Expectations: []internal.FlexibleEntityChainExpectation{
internal.ExpectBasicUserChain("user-token-1"),
internal.ExpectClientChain("client-token-1"),
},
AllowPartialSuccess: false,
MinSuccessCount: 2,
}
err := internal.ValidateEntityChainFlexible(chains, rule)
assert.NoError(t, err)
}
Environment Variables
Configure your tests using environment variables:
TEST_POSTGRES_HOST- PostgreSQL host (default: localhost)TEST_POSTGRES_PORT- PostgreSQL port (default: 5432)TEST_KEYCLOAK_URL- Keycloak URL (default: http://localhost:8080)TEST_CONTAINER_STARTUP_TIMEOUT- Container startup timeout (default: 2m)TEST_CONTAINER_RUN_TIMEOUT- Container run timeout (default: 4m)TEST_JWT_VALIDITY- JWT validity duration (default: 1h)TEST_DATA_VARIATION- Enable varied test data (default: true)
Future Enhancements
Planned Improvements
- Container Configuration: Configurable image versions and timeouts
- Caching Layer: Query result caching for SQL/LDAP backends
- Performance Metrics: OpenTelemetry integration for monitoring
- Load Testing: Benchmark tests for performance validation
- Multi-Database: Support for MySQL and other SQL databases
- HA Testing: High availability and failover testing