Files
user-engine/tests/test_registration_access_ui.py

287 lines
10 KiB
Python

import unittest
from user_engine.adapters.local import InMemoryUserEngineStore, LocalAuthorizationCheckPort
from user_engine.domain import (
AccessMembershipRequirement,
AccessProfile,
AccessScopeType,
FactorVerification,
IdentityFactorType,
OnboardingTriggerType,
PreparedEntitlement,
PreparedEntitlementKind,
PreparedFactorRequirement,
WelcomeProtocol,
WelcomeProtocolStep,
)
from user_engine.service import UserEngineService
from user_engine.testing.fixtures import FixtureIdentityClaimsAdapter, human_actor_claims
from user_engine.ui import RegistrationAccessManagementUi, UiViewport
class RegistrationAccessUiTests(unittest.TestCase):
def test_information_architecture_and_api_contract_expose_expected_routes(self):
ui, _, _ = _ui()
ia = ui.information_architecture()
contract = ui.api_contract()
route_ids = {route.route_id for route in contract.routes}
self.assertIn("registration", ia.primary_navigation)
self.assertIn("prepared_account.review", route_ids)
self.assertIn("access_profile.select_hat", route_ids)
self.assertIn("admin.dashboard", route_ids)
self.assertIn("authorization decisions", contract.adapter_boundaries)
self.assertEqual(ia.breakpoints["mobile"]["columns"], 1)
self.assertEqual(ia.breakpoints["desktop"]["columns"], 2)
def test_self_service_registration_flow_requires_terms_and_redacts_factor_values(self):
ui, service, _ = _ui()
actor = _actor()
started = ui.start_registration(
actor,
required_factor_types=(IdentityFactorType.EMAIL,),
viewport=UiViewport.MOBILE,
correlation_id="corr-ui-start",
)
ui.attach_factor(
actor,
started.session.registration_id,
_verified_email(),
viewport=UiViewport.MOBILE,
correlation_id="corr-ui-factor",
)
blocked = ui.complete_registration(
actor,
started.session.registration_id,
terms_accepted=False,
viewport=UiViewport.MOBILE,
correlation_id="corr-ui-blocked",
)
completed = ui.complete_registration(
actor,
started.session.registration_id,
terms_accepted=True,
viewport=UiViewport.MOBILE,
correlation_id="corr-ui-complete",
)
html = ui.render_html(completed.screen)
self.assertIn("Terms and consent", blocked.screen.alerts[0])
self.assertEqual(completed.completion.netkingdom_id, completed.completion.user.user_id)
self.assertEqual(completed.screen.layout["min_touch_target"], 44)
self.assertIn("role='main'", html)
self.assertIn("data-viewport='mobile'", html)
self.assertNotIn("sample.user@example.test", html)
self.assertNotIn(
"sample.user@example.test",
repr([event.payload for event in service.outbox_events()]),
)
def test_prepared_rights_can_be_reviewed_accepted_or_dismissed(self):
ui, service, _ = _ui()
actor = _actor()
prepared = service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(
PreparedFactorRequirement(
factor_type=IdentityFactorType.EMAIL,
normalized_value="sample.user@example.test",
),
),
entitlements=(
PreparedEntitlement(
kind=PreparedEntitlementKind.MEMBERSHIP,
tenant="tenant:coulomb",
scope_type="realm",
scope_id="realm:citadel",
role="member",
),
),
display_name="Prepared Member",
primary_email="sample.user@example.test",
correlation_id="corr-ui-prepare",
)
registration = _complete_registration(service, actor)
review = ui.prepared_rights_review(
actor,
registration.session.registration_id,
viewport=UiViewport.DESKTOP,
)
dismissed = ui.deny_prepared_claim(
prepared.prepared_account_id,
viewport=UiViewport.DESKTOP,
)
accepted = ui.accept_prepared_claim(
actor,
registration.session.registration_id,
prepared.prepared_account_id,
viewport=UiViewport.DESKTOP,
correlation_id="corr-ui-claim",
)
html = ui.render_html(review)
self.assertIn("denied_by_user", repr(dismissed))
self.assertEqual(accepted.claim.prepared_account.claimed_by_user_id, accepted.claim.user.user_id)
self.assertIn("Prepared Member", html)
self.assertIn("<redacted>", html)
self.assertNotIn("sample.user@example.test", html)
def test_hat_selection_view_selects_active_context_without_policy_details(self):
ui, service, store = _ui()
actor = _actor()
registration = _complete_registration(service, actor)
service.add_membership(
actor,
registration.user.user_id,
tenant="tenant:coulomb",
scope_type="realm",
scope_id="realm:citadel",
kind="operator",
correlation_id="corr-ui-membership",
)
profile = service.register_access_profile(
actor,
AccessProfile(
tenant="tenant:coulomb",
display_name="Realm Operator",
hat="operator",
scope_type=AccessScopeType.REALM,
scope_id="realm:citadel",
realm_id="realm:citadel",
service_id="app.demo",
membership_requirements=(
AccessMembershipRequirement(
scope_type="realm",
scope_id="realm:citadel",
kind="operator",
),
),
required_factor_types=(IdentityFactorType.EMAIL,),
claims={"internal_policy_hint": "do-not-render"},
),
correlation_id="corr-ui-profile",
)
before = ui.hat_selection_view(
actor,
registration.user.user_id,
tenant="tenant:coulomb",
viewport=UiViewport.DESKTOP,
)
selected = ui.select_hat(
actor,
registration.user.user_id,
profile.access_profile_id,
viewport=UiViewport.DESKTOP,
correlation_id="corr-ui-select-hat",
)
active = store.active_access_context(registration.user.user_id, "tenant:coulomb")
html = ui.render_html(selected)
self.assertIn("none", ui.render_html(before))
self.assertEqual(active.hat, "operator")
self.assertIn("Realm Operator", html)
self.assertNotIn("do-not-render", html)
def test_admin_dashboard_redacts_sensitive_setup_details(self):
ui, service, _ = _ui()
actor = _actor()
session = service.me(human_actor_claims(), correlation_id="corr-ui-me")
service.register_welcome_protocol(
session.actor,
WelcomeProtocol(
tenant="tenant:coulomb",
name="Blocked Welcome",
trigger_type=OnboardingTriggerType.MANUAL,
steps=(
WelcomeProtocolStep(
step_key="external",
title="External",
subsystem="crm",
requires_subsystem_callback=True,
),
),
),
correlation_id="corr-ui-protocol",
)
service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(
PreparedFactorRequirement(
factor_type=IdentityFactorType.EMAIL,
normalized_value="sample.user@example.test",
),
),
entitlements=(
PreparedEntitlement(
kind=PreparedEntitlementKind.ONBOARDING_JOURNEY,
tenant="tenant:coulomb",
onboarding_journey="welcome-demo",
),
),
primary_email="sample.user@example.test",
correlation_id="corr-ui-prepare",
)
dashboard = ui.admin_dashboard(
actor,
tenant="tenant:coulomb",
viewport=UiViewport.DESKTOP,
)
html = ui.render_html(dashboard)
self.assertEqual(dashboard.layout["columns"], 2)
self.assertIn("Prepared Accounts", html)
self.assertIn("Onboarding", html)
self.assertNotIn("sample.user@example.test", html)
self.assertIn("role='navigation'", html)
self.assertIn("aria-label='Sections'", html)
def _ui():
store = InMemoryUserEngineStore()
service = UserEngineService(
store=store,
identity_adapter=FixtureIdentityClaimsAdapter(),
authorization=LocalAuthorizationCheckPort(),
)
return RegistrationAccessManagementUi(service), service, store
def _actor():
return FixtureIdentityClaimsAdapter().normalize(
human_actor_claims(subject="sample-user", tenant="tenant:coulomb")
)
def _verified_email() -> FactorVerification:
return FactorVerification(
factor_type=IdentityFactorType.EMAIL,
normalized_value="sample.user@example.test",
display_value="sample.user@example.test",
source_system="fixture-email",
)
def _complete_registration(service: UserEngineService, actor):
session = service.start_registration(actor, correlation_id="corr-ui-reg-start")
service.attach_registration_factor(
actor,
session.registration_id,
_verified_email(),
correlation_id="corr-ui-reg-factor",
)
return service.complete_registration(
actor,
session.registration_id,
correlation_id="corr-ui-reg-complete",
)
if __name__ == "__main__":
unittest.main()