generated from coulomb/repo-seed
feat: add durable store conformance harness
This commit is contained in:
@@ -1 +1,11 @@
|
||||
"""Testing helpers and local fixtures for user-engine."""
|
||||
|
||||
from user_engine.testing.store_conformance import (
|
||||
assert_user_engine_migration_contract,
|
||||
assert_user_engine_store_conformance,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"assert_user_engine_migration_contract",
|
||||
"assert_user_engine_store_conformance",
|
||||
]
|
||||
|
||||
522
src/user_engine/testing/store_conformance.py
Normal file
522
src/user_engine/testing/store_conformance.py
Normal file
@@ -0,0 +1,522 @@
|
||||
"""Reusable conformance checks for user-engine store adapters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from unittest import TestCase
|
||||
|
||||
from user_engine.domain import (
|
||||
Account,
|
||||
AccountStatus,
|
||||
AccessMembershipRequirement,
|
||||
AccessProfile,
|
||||
AccessScopeType,
|
||||
ActiveAccessContext,
|
||||
Actor,
|
||||
Application,
|
||||
ApplicationBinding,
|
||||
AttributeDefinition,
|
||||
AuditRecord,
|
||||
Catalog,
|
||||
CatalogLifecycle,
|
||||
ExternalIdentity,
|
||||
FamilyInvitation,
|
||||
IdentityFactor,
|
||||
IdentityFactorType,
|
||||
InvitationStatus,
|
||||
Membership,
|
||||
Mutability,
|
||||
OnboardingJourney,
|
||||
OnboardingStep,
|
||||
OnboardingTriggerType,
|
||||
OutboxEvent,
|
||||
PreparedAccount,
|
||||
PreparedEntitlement,
|
||||
PreparedEntitlementKind,
|
||||
PreparedFactorRequirement,
|
||||
PrincipalType,
|
||||
ProfileScope,
|
||||
ProfileValue,
|
||||
ProjectionType,
|
||||
RegistrationSession,
|
||||
RegistrationStatus,
|
||||
Sensitivity,
|
||||
TenantAccount,
|
||||
User,
|
||||
Visibility,
|
||||
WelcomeProtocol,
|
||||
WelcomeProtocolStep,
|
||||
)
|
||||
from user_engine.migrations import (
|
||||
LATEST_SCHEMA_VERSION,
|
||||
USER_ENGINE_RECORD_COUNT_KEYS,
|
||||
migration_manifest,
|
||||
validate_migration_manifest,
|
||||
)
|
||||
from user_engine.ports import UserEngineStore
|
||||
|
||||
StoreFactory = Callable[[], UserEngineStore]
|
||||
|
||||
TENANT = "tenant:store-conformance"
|
||||
USER_ID = "usr_store_conformance"
|
||||
RAW_FACTOR_VALUE = "store.user@example.test"
|
||||
PROFILE_SECRET_VALUE = "quiet-secret-profile-value"
|
||||
|
||||
|
||||
def assert_user_engine_migration_contract(testcase: TestCase) -> None:
|
||||
"""Assert the migration manifest is ordered and provider-safe."""
|
||||
testcase.assertEqual(validate_migration_manifest(), ())
|
||||
manifest = migration_manifest()
|
||||
testcase.assertGreaterEqual(len(manifest), 1)
|
||||
testcase.assertEqual(manifest[-1].version, LATEST_SCHEMA_VERSION)
|
||||
testcase.assertEqual(
|
||||
manifest[0].sql_path,
|
||||
"migrations/postgres/0001_user_engine_store.sql",
|
||||
)
|
||||
|
||||
|
||||
def assert_user_engine_store_conformance(
|
||||
testcase: TestCase,
|
||||
store_factory: StoreFactory,
|
||||
) -> None:
|
||||
"""Run the core durable-store behavior contract for one store factory."""
|
||||
assert_user_engine_migration_contract(testcase)
|
||||
_assert_readiness_contract(testcase, store_factory)
|
||||
_assert_save_read_and_query_contract(testcase, store_factory)
|
||||
_assert_transaction_rollback_contract(testcase, store_factory)
|
||||
_assert_outbox_ordering_contract(testcase, store_factory)
|
||||
_assert_diagnostics_contract(testcase, store_factory)
|
||||
|
||||
|
||||
def _assert_readiness_contract(testcase: TestCase, store_factory: StoreFactory) -> None:
|
||||
store = store_factory()
|
||||
if store.schema_version is None:
|
||||
testcase.assertFalse(store.ready)
|
||||
|
||||
store.migrate()
|
||||
|
||||
testcase.assertTrue(store.ready)
|
||||
testcase.assertEqual(store.schema_version, LATEST_SCHEMA_VERSION)
|
||||
store.migrate()
|
||||
testcase.assertTrue(store.ready)
|
||||
testcase.assertEqual(store.schema_version, LATEST_SCHEMA_VERSION)
|
||||
|
||||
|
||||
def _assert_save_read_and_query_contract(
|
||||
testcase: TestCase,
|
||||
store_factory: StoreFactory,
|
||||
) -> None:
|
||||
store = _migrated(store_factory)
|
||||
records = _write_reference_records(store)
|
||||
user = records["user"]
|
||||
account = records["account"]
|
||||
identity = records["identity"]
|
||||
tenant_account = records["tenant_account"]
|
||||
membership = records["membership"]
|
||||
application = records["application"]
|
||||
binding = records["binding"]
|
||||
catalog = records["catalog"]
|
||||
invitation = records["invitation"]
|
||||
registration = records["registration"]
|
||||
factor = records["factor"]
|
||||
prepared_account = records["prepared_account"]
|
||||
access_profile = records["access_profile"]
|
||||
access_context = records["access_context"]
|
||||
welcome_protocol = records["welcome_protocol"]
|
||||
onboarding_journey = records["onboarding_journey"]
|
||||
profile_value = records["profile_value"]
|
||||
|
||||
testcase.assertEqual(store.user(USER_ID), user)
|
||||
testcase.assertEqual(store.user_account(USER_ID), account)
|
||||
testcase.assertEqual(store.find_identity(identity.issuer, identity.subject), identity)
|
||||
testcase.assertEqual(store.identities_for_user(USER_ID), (identity,))
|
||||
testcase.assertEqual(store.tenant_account(TENANT, USER_ID), tenant_account)
|
||||
testcase.assertEqual(store.memberships_for_user(USER_ID), (membership,))
|
||||
testcase.assertEqual(store.memberships_for_user(USER_ID, tenant=TENANT), (membership,))
|
||||
testcase.assertEqual(store.memberships_for_tenant(TENANT), (membership,))
|
||||
testcase.assertEqual(store.application(application.application_id), application)
|
||||
testcase.assertEqual(store.binding(binding.application_id), binding)
|
||||
testcase.assertEqual(store.catalog(catalog.catalog_id), catalog)
|
||||
testcase.assertEqual(store.all_catalogs(), (catalog,))
|
||||
testcase.assertEqual(store.family_invitation(invitation.invitation_id), invitation)
|
||||
testcase.assertEqual(store.family_invitations_for_user(USER_ID), (invitation,))
|
||||
testcase.assertEqual(store.registration_session(registration.registration_id), registration)
|
||||
testcase.assertEqual(store.all_registration_sessions(), (registration,))
|
||||
testcase.assertEqual(store.identity_factor(factor.factor_id), factor)
|
||||
testcase.assertEqual(store.factors_for_registration(registration.registration_id), (factor,))
|
||||
testcase.assertEqual(store.factors_for_user(USER_ID), (factor,))
|
||||
testcase.assertEqual(
|
||||
store.prepared_account(prepared_account.prepared_account_id),
|
||||
prepared_account,
|
||||
)
|
||||
testcase.assertEqual(store.prepared_accounts_for_tenant(TENANT), (prepared_account,))
|
||||
testcase.assertEqual(store.access_profile(access_profile.access_profile_id), access_profile)
|
||||
testcase.assertEqual(store.access_profiles_for_tenant(TENANT), (access_profile,))
|
||||
testcase.assertEqual(store.active_access_context(USER_ID, TENANT), access_context)
|
||||
testcase.assertEqual(store.active_access_contexts_for_tenant(TENANT), (access_context,))
|
||||
testcase.assertEqual(store.welcome_protocol(welcome_protocol.protocol_id), welcome_protocol)
|
||||
testcase.assertEqual(store.welcome_protocols_for_tenant(TENANT), (welcome_protocol,))
|
||||
testcase.assertEqual(
|
||||
store.onboarding_journey(onboarding_journey.journey_id),
|
||||
onboarding_journey,
|
||||
)
|
||||
testcase.assertEqual(store.onboarding_journeys_for_user(USER_ID), (onboarding_journey,))
|
||||
testcase.assertEqual(
|
||||
store.onboarding_journeys_for_user(USER_ID, tenant=TENANT),
|
||||
(onboarding_journey,),
|
||||
)
|
||||
testcase.assertEqual(store.onboarding_journeys_for_tenant(TENANT), (onboarding_journey,))
|
||||
testcase.assertEqual(store.values_for_user(USER_ID), (profile_value,))
|
||||
|
||||
replacement = User(
|
||||
user_id=USER_ID,
|
||||
display_name="Replacement User",
|
||||
primary_email=RAW_FACTOR_VALUE,
|
||||
created_at=user.created_at,
|
||||
)
|
||||
store.save_user(replacement)
|
||||
testcase.assertEqual(store.user(USER_ID), replacement)
|
||||
|
||||
|
||||
def _assert_transaction_rollback_contract(
|
||||
testcase: TestCase,
|
||||
store_factory: StoreFactory,
|
||||
) -> None:
|
||||
store = _migrated(store_factory)
|
||||
actor = _actor()
|
||||
|
||||
with testcase.assertRaises(RuntimeError):
|
||||
with store.transaction():
|
||||
store.save_user(User(user_id="usr_rollback", display_name="Rollback"))
|
||||
store.append_audit(
|
||||
AuditRecord(
|
||||
audit_id="aud_rollback",
|
||||
actor=actor,
|
||||
action="store.write",
|
||||
subject="usr_rollback",
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-rollback",
|
||||
summary="rolled back audit",
|
||||
)
|
||||
)
|
||||
store.append_outbox(
|
||||
OutboxEvent(
|
||||
event_id="evt_rollback",
|
||||
event_type="store.rollback",
|
||||
aggregate_id="usr_rollback",
|
||||
payload={"result": "rollback"},
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-rollback",
|
||||
)
|
||||
)
|
||||
raise RuntimeError("force rollback")
|
||||
|
||||
testcase.assertIsNone(store.user("usr_rollback"))
|
||||
testcase.assertEqual(store.audit_log(), ())
|
||||
testcase.assertEqual(store.pending_outbox(), ())
|
||||
|
||||
|
||||
def _assert_outbox_ordering_contract(
|
||||
testcase: TestCase,
|
||||
store_factory: StoreFactory,
|
||||
) -> None:
|
||||
store = _migrated(store_factory)
|
||||
events = (
|
||||
OutboxEvent(
|
||||
event_id="evt_order_1",
|
||||
event_type="store.first",
|
||||
aggregate_id=USER_ID,
|
||||
payload={"sequence": 1},
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-order",
|
||||
),
|
||||
OutboxEvent(
|
||||
event_id="evt_order_2",
|
||||
event_type="store.second",
|
||||
aggregate_id=USER_ID,
|
||||
payload={"sequence": 2},
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-order",
|
||||
),
|
||||
)
|
||||
for event in events:
|
||||
store.append_outbox(event)
|
||||
|
||||
testcase.assertEqual(store.pending_outbox(), events)
|
||||
|
||||
|
||||
def _assert_diagnostics_contract(
|
||||
testcase: TestCase,
|
||||
store_factory: StoreFactory,
|
||||
) -> None:
|
||||
store = _migrated(store_factory)
|
||||
_write_reference_records(store)
|
||||
|
||||
counts = dict(store.record_counts())
|
||||
testcase.assertEqual(set(counts), set(USER_ENGINE_RECORD_COUNT_KEYS))
|
||||
testcase.assertTrue(all(isinstance(value, int) for value in counts.values()))
|
||||
testcase.assertEqual(counts["bindings"], 1)
|
||||
testcase.assertEqual(counts["pending_outbox_events"], 1)
|
||||
|
||||
diagnostics_text = repr(counts)
|
||||
testcase.assertNotIn(RAW_FACTOR_VALUE, diagnostics_text)
|
||||
testcase.assertNotIn(PROFILE_SECRET_VALUE, diagnostics_text)
|
||||
|
||||
|
||||
def _migrated(store_factory: StoreFactory) -> UserEngineStore:
|
||||
store = store_factory()
|
||||
store.migrate()
|
||||
return store
|
||||
|
||||
|
||||
def _write_reference_records(store: UserEngineStore) -> dict[str, Any]:
|
||||
actor = _actor()
|
||||
user = User(
|
||||
user_id=USER_ID,
|
||||
display_name="Store Conformance User",
|
||||
primary_email=RAW_FACTOR_VALUE,
|
||||
)
|
||||
account = Account(
|
||||
account_id="acct_store_conformance",
|
||||
user_id=USER_ID,
|
||||
status=AccountStatus.ACTIVE,
|
||||
)
|
||||
identity = ExternalIdentity(
|
||||
identity_id="eid_store_conformance",
|
||||
user_id=USER_ID,
|
||||
issuer="https://issuer.example.test",
|
||||
subject="store-conformance",
|
||||
provider="fixture",
|
||||
)
|
||||
tenant_account = TenantAccount(user_id=USER_ID, tenant=TENANT)
|
||||
membership = Membership(
|
||||
membership_id="mbr_store_conformance",
|
||||
user_id=USER_ID,
|
||||
tenant=TENANT,
|
||||
scope_type="team",
|
||||
scope_id="team:store-conformance",
|
||||
kind="member",
|
||||
)
|
||||
application = Application(
|
||||
application_id="app.store-conformance",
|
||||
display_name="Store Conformance",
|
||||
owner="team:store-conformance",
|
||||
allowed_profile_scopes=(ProfileScope.GLOBAL, ProfileScope.APPLICATION),
|
||||
allowed_projection_types=(ProjectionType.APPLICATION_RUNTIME,),
|
||||
)
|
||||
binding = ApplicationBinding(
|
||||
application_id=application.application_id,
|
||||
oidc_client_id="store-conformance-client",
|
||||
protected_system_id="store-conformance.service",
|
||||
catalog_namespaces=("store",),
|
||||
event_source="store-conformance",
|
||||
deployment_ref="local",
|
||||
)
|
||||
catalog = Catalog(
|
||||
catalog_id="cat_store_conformance",
|
||||
namespace="store",
|
||||
version="1.0.0",
|
||||
owning_application_id=application.application_id,
|
||||
lifecycle=CatalogLifecycle.ACTIVE,
|
||||
attributes=(
|
||||
AttributeDefinition(
|
||||
key="store.secret",
|
||||
value_type="string",
|
||||
scope=ProfileScope.GLOBAL,
|
||||
sensitivity=Sensitivity.SECRET,
|
||||
visibility=(Visibility.USER,),
|
||||
mutability=(Mutability.USER,),
|
||||
),
|
||||
),
|
||||
)
|
||||
invitation = FamilyInvitation(
|
||||
invitation_id="finv_store_conformance",
|
||||
tenant=TENANT,
|
||||
family_scope_id="family:store-conformance",
|
||||
application_id=application.application_id,
|
||||
user_id=USER_ID,
|
||||
primary_email=RAW_FACTOR_VALUE,
|
||||
role="adult",
|
||||
status=InvitationStatus.PENDING,
|
||||
invited_by=actor.subject,
|
||||
)
|
||||
registration = RegistrationSession(
|
||||
tenant=TENANT,
|
||||
registration_id="reg_store_conformance",
|
||||
status=RegistrationStatus.FACTOR_VERIFIED,
|
||||
required_factor_types=(IdentityFactorType.EMAIL,),
|
||||
verified_factor_ids=("fac_store_conformance",),
|
||||
user_id=USER_ID,
|
||||
netkingdom_id="nk-store-conformance",
|
||||
started_by_subject=actor.subject,
|
||||
correlation_id="corr-store",
|
||||
)
|
||||
factor = IdentityFactor(
|
||||
factor_id="fac_store_conformance",
|
||||
factor_type=IdentityFactorType.EMAIL,
|
||||
normalized_value=RAW_FACTOR_VALUE,
|
||||
registration_id=registration.registration_id,
|
||||
user_id=USER_ID,
|
||||
display_value="s***@example.test",
|
||||
)
|
||||
prepared_account = PreparedAccount(
|
||||
tenant=TENANT,
|
||||
prepared_account_id="pacct_store_conformance",
|
||||
required_factor_matches=(
|
||||
PreparedFactorRequirement(
|
||||
factor_type=IdentityFactorType.EMAIL,
|
||||
normalized_value=RAW_FACTOR_VALUE,
|
||||
),
|
||||
),
|
||||
entitlements=(
|
||||
PreparedEntitlement(
|
||||
kind=PreparedEntitlementKind.MEMBERSHIP,
|
||||
tenant=TENANT,
|
||||
scope_type="team",
|
||||
scope_id="team:store-conformance",
|
||||
role="member",
|
||||
),
|
||||
),
|
||||
display_name="Prepared Store User",
|
||||
primary_email=RAW_FACTOR_VALUE,
|
||||
prepared_by_subject=actor.subject,
|
||||
correlation_id="corr-store",
|
||||
)
|
||||
access_profile = AccessProfile(
|
||||
tenant=TENANT,
|
||||
display_name="Store Member",
|
||||
hat="member",
|
||||
access_profile_id="apf_store_conformance",
|
||||
scope_type=AccessScopeType.TENANT,
|
||||
scope_id=TENANT,
|
||||
membership_requirements=(
|
||||
AccessMembershipRequirement(
|
||||
scope_type="team",
|
||||
scope_id="team:store-conformance",
|
||||
kind="member",
|
||||
),
|
||||
),
|
||||
required_factor_types=(IdentityFactorType.EMAIL,),
|
||||
profile_defaults={"store.secret": PROFILE_SECRET_VALUE},
|
||||
claims={"role": "member"},
|
||||
)
|
||||
access_context = ActiveAccessContext(
|
||||
active_context_id="actx_store_conformance",
|
||||
user_id=USER_ID,
|
||||
tenant=TENANT,
|
||||
access_profile_id=access_profile.access_profile_id,
|
||||
hat=access_profile.hat,
|
||||
scope_type=AccessScopeType.TENANT,
|
||||
scope_id=TENANT,
|
||||
membership_ids=(membership.membership_id,),
|
||||
factor_ids=(factor.factor_id,),
|
||||
selected_by_subject=actor.subject,
|
||||
)
|
||||
welcome_protocol = WelcomeProtocol(
|
||||
protocol_id="wpro_store_conformance",
|
||||
tenant=TENANT,
|
||||
name="Store Welcome",
|
||||
trigger_type=OnboardingTriggerType.REGISTRATION,
|
||||
steps=(
|
||||
WelcomeProtocolStep(
|
||||
step_key="profile",
|
||||
title="Complete profile",
|
||||
subsystem="user-account",
|
||||
),
|
||||
),
|
||||
)
|
||||
onboarding_journey = OnboardingJourney(
|
||||
journey_id="ojrn_store_conformance",
|
||||
tenant=TENANT,
|
||||
user_id=USER_ID,
|
||||
protocol_id=welcome_protocol.protocol_id,
|
||||
trigger_type=OnboardingTriggerType.REGISTRATION,
|
||||
steps=(
|
||||
OnboardingStep(
|
||||
step_key="profile",
|
||||
title="Complete profile",
|
||||
subsystem="user-account",
|
||||
),
|
||||
),
|
||||
source_id=registration.registration_id,
|
||||
source_event_type="registration.completed",
|
||||
correlation_id="corr-store",
|
||||
)
|
||||
profile_value = ProfileValue(
|
||||
user_id=USER_ID,
|
||||
attribute_key="store.secret",
|
||||
value=PROFILE_SECRET_VALUE,
|
||||
scope=ProfileScope.GLOBAL,
|
||||
)
|
||||
audit_record = AuditRecord(
|
||||
audit_id="aud_store_conformance",
|
||||
actor=actor,
|
||||
action="store.write",
|
||||
subject=USER_ID,
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-store",
|
||||
summary="store conformance",
|
||||
)
|
||||
outbox_event = OutboxEvent(
|
||||
event_id="evt_store_conformance",
|
||||
event_type="store.changed",
|
||||
aggregate_id=USER_ID,
|
||||
payload={"kind": "store-conformance"},
|
||||
tenant=TENANT,
|
||||
correlation_id="corr-store",
|
||||
)
|
||||
|
||||
store.save_user(user)
|
||||
store.save_account(account)
|
||||
store.save_identity(identity)
|
||||
store.save_tenant_account(tenant_account)
|
||||
store.save_membership(membership)
|
||||
store.save_application(application)
|
||||
store.save_binding(binding)
|
||||
store.save_catalog(catalog)
|
||||
store.save_family_invitation(invitation)
|
||||
store.save_registration_session(registration)
|
||||
store.save_identity_factor(factor)
|
||||
store.save_prepared_account(prepared_account)
|
||||
store.save_access_profile(access_profile)
|
||||
store.save_active_access_context(access_context)
|
||||
store.save_welcome_protocol(welcome_protocol)
|
||||
store.save_onboarding_journey(onboarding_journey)
|
||||
store.save_profile_value(profile_value)
|
||||
store.append_audit(audit_record)
|
||||
store.append_outbox(outbox_event)
|
||||
|
||||
return {
|
||||
"user": user,
|
||||
"account": account,
|
||||
"identity": identity,
|
||||
"tenant_account": tenant_account,
|
||||
"membership": membership,
|
||||
"application": application,
|
||||
"binding": binding,
|
||||
"catalog": catalog,
|
||||
"invitation": invitation,
|
||||
"registration": registration,
|
||||
"factor": factor,
|
||||
"prepared_account": prepared_account,
|
||||
"access_profile": access_profile,
|
||||
"access_context": access_context,
|
||||
"welcome_protocol": welcome_protocol,
|
||||
"onboarding_journey": onboarding_journey,
|
||||
"profile_value": profile_value,
|
||||
"audit_record": audit_record,
|
||||
"outbox_event": outbox_event,
|
||||
}
|
||||
|
||||
|
||||
def _actor() -> Actor:
|
||||
return Actor(
|
||||
issuer="https://issuer.example.test",
|
||||
subject="store-conformance",
|
||||
tenant=TENANT,
|
||||
principal_type=PrincipalType.HUMAN,
|
||||
audience=("user-engine",),
|
||||
roles=("user",),
|
||||
scopes=("profile",),
|
||||
)
|
||||
Reference in New Issue
Block a user