Files
user-engine/tests/test_family_dataspace_onboarding.py

197 lines
7.4 KiB
Python

import unittest
from user_engine.adapters.local import InMemoryUserEngineStore
from user_engine.domain import (
AccountStatus,
FamilyDataspaceRequest,
FamilyMemberSpec,
FamilyRole,
InvitationStatus,
)
from user_engine.errors import AuthorizationDenied, ValidationError
from user_engine.service import UserEngineService
from user_engine.testing.fixtures import human_actor_claims
from user_engine.testing.scenarios import (
ScenarioAuthorizationHarness,
StrictFixtureIdentityClaimsAdapter,
)
class FamilyDataspaceOnboardingTests(unittest.TestCase):
def test_onboarding_creates_family_scope_dataspace_app_and_invitation(self):
service, store, authz = _service()
session = service.me(_owner_claims(), correlation_id="corr-owner")
onboarding = service.onboard_family_dataspace(
session.actor,
FamilyDataspaceRequest(
tenant="tenant:worsch-family",
family_scope_id="family:worsch",
family_display_name="Worsch Family",
application_id="app.personal-dataspace",
oidc_client_id="personal-dataspace-client",
protected_system_id="dataspace.personal.worsch",
member_specs=(
FamilyMemberSpec(
primary_email="child@example.test",
display_name="Child Member",
role=FamilyRole.CHILD,
),
),
),
correlation_id="corr-family-onboard",
)
self.assertEqual(onboarding.tenant, "tenant:worsch-family")
self.assertEqual(onboarding.binding.oidc_client_id, "personal-dataspace-client")
self.assertEqual(onboarding.catalog.namespace, "dataspace")
self.assertEqual(onboarding.owner_membership.kind, FamilyRole.OWNER.value)
self.assertEqual(onboarding.invitations[0].invitation.status, InvitationStatus.PENDING)
self.assertEqual(onboarding.invitations[0].tenant_account.status, AccountStatus.INVITED)
self.assertEqual(onboarding.identity_context.entity_refs["family:family:worsch"].concept, "Group")
self.assertEqual(
onboarding.claims_projection.values["dataspace.family_display_name"],
"Worsch Family",
)
self.assertIn("family_dataspace.onboarded", _event_types(service))
self.assertIn("family_member.invited", _event_types(service))
self.assertIn("family_dataspace.onboard", [request.action for request in authz.requests])
self.assertEqual(len(store.family_invitations), 1)
def test_member_acceptance_links_sso_identity_and_returns_dataspace_context(self):
service, _, _ = _service()
owner = service.me(_owner_claims(), correlation_id="corr-owner")
onboarding = _onboard_family(service, owner.actor)
invitation = onboarding.invitations[0].invitation
acceptance = service.accept_family_invitation(
_member_claims(subject="child-sso"),
invitation.invitation_id,
correlation_id="corr-accept",
)
self.assertEqual(acceptance.invitation.status, InvitationStatus.ACCEPTED)
self.assertEqual(acceptance.session.user.user_id, invitation.user_id)
self.assertEqual(acceptance.session.account.status, AccountStatus.ACTIVE)
self.assertEqual(
service.store.tenant_account("tenant:worsch-family", invitation.user_id).status,
AccountStatus.ACTIVE,
)
self.assertEqual(
service.store.find_identity(
"https://issuer.example.test",
"child-sso",
).user_id,
invitation.user_id,
)
self.assertEqual(
acceptance.claims_projection.values["dataspace.member_display_name"],
"Child Member",
)
self.assertEqual(acceptance.identity_context.memberships[0].kind, FamilyRole.CHILD.value)
self.assertIn("family_invitation.accepted", _event_types(service))
def test_revoked_invitation_cannot_be_accepted(self):
service, _, _ = _service()
owner = service.me(_owner_claims(), correlation_id="corr-owner")
onboarding = _onboard_family(service, owner.actor)
invitation = onboarding.invitations[0].invitation
resent = service.resend_family_invitation(
owner.actor,
invitation.invitation_id,
correlation_id="corr-resend",
)
revoked = service.revoke_family_invitation(
owner.actor,
invitation.invitation_id,
correlation_id="corr-revoke",
)
self.assertEqual(resent.resend_count, 1)
self.assertEqual(revoked.status, InvitationStatus.REVOKED)
self.assertEqual(
service.store.tenant_account("tenant:worsch-family", invitation.user_id).status,
AccountStatus.DISABLED,
)
with self.assertRaises(ValidationError):
service.accept_family_invitation(
_member_claims(subject="revoked-child"),
invitation.invitation_id,
correlation_id="corr-revoked-accept",
)
def test_cross_tenant_invitation_acceptance_is_denied(self):
service, _, _ = _service()
owner = service.me(_owner_claims(), correlation_id="corr-owner")
onboarding = _onboard_family(service, owner.actor)
with self.assertRaises(AuthorizationDenied):
service.accept_family_invitation(
_member_claims(subject="wrong-tenant", tenant="tenant:other-family"),
onboarding.invitations[0].invitation.invitation_id,
correlation_id="corr-wrong-tenant",
)
def _service():
store = InMemoryUserEngineStore()
service = UserEngineService(
store=store,
identity_adapter=StrictFixtureIdentityClaimsAdapter(),
authorization=ScenarioAuthorizationHarness(),
)
return service, store, service.authorization
def _onboard_family(service: UserEngineService, actor):
return service.onboard_family_dataspace(
actor,
FamilyDataspaceRequest(
tenant="tenant:worsch-family",
family_scope_id="family:worsch",
family_display_name="Worsch Family",
application_id="app.personal-dataspace",
oidc_client_id="personal-dataspace-client",
protected_system_id="dataspace.personal.worsch",
member_specs=(
FamilyMemberSpec(
primary_email="child@example.test",
display_name="Child Member",
role=FamilyRole.CHILD,
),
),
),
correlation_id="corr-family-onboard",
)
def _owner_claims() -> dict[str, object]:
claims = human_actor_claims(
subject="family-owner",
tenant="tenant:worsch-family",
)
claims["roles"] = ["tenant-admin"]
claims["preferred_username"] = "family.owner"
claims["email"] = "owner@example.test"
return claims
def _member_claims(
*,
subject: str,
tenant: str = "tenant:worsch-family",
) -> dict[str, object]:
claims = human_actor_claims(subject=subject, tenant=tenant)
claims["preferred_username"] = subject
claims["email"] = f"{subject}@example.test"
return claims
def _event_types(service: UserEngineService) -> list[str]:
return [event.event_type for event in service.outbox_events()]
if __name__ == "__main__":
unittest.main()