"""Test data factories (PostgreSQL required).""" from __future__ import annotations import uuid from auth import hash_password from db import get_connection from services.actors import create_actor def create_tenant(*, slug: str | None = None, name: str = "Test Tenant") -> str: slug = slug or f"t-{uuid.uuid4().hex[:10]}" conn = get_connection() try: with conn.cursor() as cur: cur.execute( "INSERT INTO tenants (slug, name) VALUES (%s, %s) RETURNING id", (slug, name), ) tenant_id = str(cur.fetchone()[0]) conn.commit() return tenant_id finally: conn.close() def create_user( *, email: str | None = None, password: str = "test-password-123", display_name: str = "Test User", portal_role: str = "user", ) -> dict: email = email or f"user-{uuid.uuid4().hex[:8]}@example.com" conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO users (email, password_hash, display_name, portal_role) VALUES (%s, %s, %s, %s) RETURNING id, email, display_name, portal_role """, (email, hash_password(password), display_name, portal_role), ) row = cur.fetchone() conn.commit() return { "id": str(row[0]), "email": row[1], "display_name": row[2], "portal_role": row[3], "password": password, } finally: conn.close() def add_membership(*, tenant_id: str, user_id: str, tenant_role: str = "member") -> None: conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO tenant_memberships (tenant_id, user_id, tenant_role) VALUES (%s, %s, %s) ON CONFLICT (tenant_id, user_id) DO UPDATE SET tenant_role = EXCLUDED.tenant_role """, (tenant_id, user_id, tenant_role), ) conn.commit() finally: conn.close() def tenant_context_from_user(user: dict): """Build TenantContext for direct service/kernel calls in tests.""" from psycopg2.extras import RealDictCursor from rights_registry import load_grants_for_roles from tenant_context import TenantContext conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT t.id, t.slug, t.name, tm.tenant_role FROM tenant_memberships tm JOIN tenants t ON t.id = tm.tenant_id WHERE tm.user_id = %s AND tm.tenant_id = %s """, (user["id"], user["tenant_id"]), ) membership = cur.fetchone() if not membership: raise ValueError("Test user has no tenant membership") finally: conn.close() tenant_role = membership["tenant_role"] caps = frozenset( load_grants_for_roles( portal_role=user["portal_role"], tenant_role=tenant_role, ) ) return TenantContext( user_id=user["id"], email=user["email"], display_name=user["display_name"], portal_role=user["portal_role"], tenant_id=str(membership["id"]), tenant_slug=membership["slug"], tenant_name=membership["name"], tenant_role=tenant_role, actor_id=user.get("actor_id"), actor_type="human" if user.get("actor_id") else None, session_token="test-session", capabilities=caps, ) def provision_user_in_tenant( *, tenant_role: str = "member", portal_role: str = "user", ) -> dict: tenant_id = create_tenant() user = create_user(portal_role=portal_role) add_membership(tenant_id=tenant_id, user_id=user["id"], tenant_role=tenant_role) actor = create_actor( tenant_id=tenant_id, actor_type="human", name=user["display_name"], user_id=user["id"], ) return {**user, "tenant_id": tenant_id, "actor_id": actor["id"]}