MEMO-022: Prismctl OIDC Integration Testing Requirements
Context
Prismctl's authentication system (cli/prismctl/auth.py
) has 40% code coverage with unit tests. The uncovered 60% consists of OIDC integration flows that require a live identity provider:
- Device code flow (recommended for CLI)
- Password flow (local dev only)
- Token refresh flow
- Userinfo endpoint calls
- OIDC endpoint discovery
Current State:
✅ Unit tests: 6/6 passing
✅ Token storage: Secure (600 permissions)
✅ Expiry detection: Working
✅ CLI commands: Functional
❌ OIDC flows: Untested (40% coverage)
Why Integration Tests Matter:
- Security: OIDC is our primary authentication mechanism
- User Experience: Login flow is first interaction with prismctl
- Reliability: Token refresh must work seamlessly
- Compatibility: Must work with Dex (local) and production IdPs
Integration Testing Strategy
Test Infrastructure
Local Dex Server (from RFC-016):
# tests/integration/docker-compose.dex.yml
services:
dex:
image: ghcr.io/dexidp/dex:v2.37.0
container_name: prismctl-test-dex
ports:
- "5556:5556" # HTTP
volumes:
- ./dex-config.yaml:/etc/dex/config.yaml:ro
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5556/healthz"]
interval: 2s
timeout: 2s
retries: 10
Dex Test Configuration:
# tests/integration/dex-config.yaml
issuer: http://localhost:5556/dex
storage:
type: memory # In-memory for tests
web:
http: 0.0.0.0:5556
staticClients:
- id: prismctl-test
name: "Prismctl Test Client"
redirectURIs:
- http://localhost:8080/callback
secret: test-secret
connectors:
- type: mockCallback
id: mock
name: Mock
enablePasswordDB: true
staticPasswords:
- email: "test@prism.local"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # "password"
username: "test"
userID: "test-user-id"
- email: "admin@prism.local"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # "password"
username: "admin"
userID: "admin-user-id"
Test Scenarios
1. Device Code Flow (Priority: HIGH)
Test: test_device_code_flow_success
def test_device_code_flow_success():
"""Test successful device code authentication."""
# Start local Dex server
with DexTestServer() as dex:
config = OIDCConfig(
issuer=dex.issuer_url,
client_id="prismctl-test",
client_secret="test-secret",
)
authenticator = OIDCAuthenticator(config)
# Mock browser interaction (auto-approve)
with mock_device_approval(dex):
token = authenticator.login_device_code(open_browser=False)
# Assertions
assert token.access_token is not None
assert token.refresh_token is not None
assert not token.is_expired()
# Verify token works for userinfo
userinfo = authenticator.get_userinfo(token)
assert userinfo["email"] == "test@prism.local"
Test: test_device_code_flow_timeout
def test_device_code_flow_timeout():
"""Test device code flow timeout without approval."""
with DexTestServer() as dex:
authenticator = OIDCAuthenticator(config)
# Don't approve - should timeout
with pytest.raises(TimeoutError, match="timed out"):
authenticator.login_device_code(open_browser=False)
Test: test_device_code_flow_denied
def test_device_code_flow_denied():
"""Test device code flow when user denies."""
with DexTestServer() as dex:
with mock_device_denial(dex):
with pytest.raises(ValueError, match="denied by user"):
authenticator.login_device_code(open_browser=False)
2. Password Flow (Priority: MEDIUM)
Test: test_password_flow_success
def test_password_flow_success():
"""Test successful password authentication."""
with DexTestServer() as dex:
authenticator = OIDCAuthenticator(config)
token = authenticator.login_password(
username="test@prism.local",
password="password"
)
assert token.access_token is not None
assert not token.is_expired()
Test: test_password_flow_invalid_credentials
def test_password_flow_invalid_credentials():
"""Test password flow with wrong credentials."""
with DexTestServer() as dex:
authenticator = OIDCAuthenticator(config)
with pytest.raises(requests.HTTPError):
authenticator.login_password(
username="test@prism.local",
password="wrong"
)