from __future__ import annotations import asyncio import os from contextlib import asynccontextmanager from typing import Any from jose import jwt os.environ.setdefault("VELOCITY_JWT_SECRET", "test-secret") from backend.auth.dependencies import UserPrincipal from backend.auth import service as auth_service auth_service.verify_password = lambda plain, hashed: plain == hashed class AppState: def __init__(self, pool: Any) -> None: self.db_pool = pool self._auth_user_directory_schema_ready = False class AppStub: def __init__(self, pool: Any) -> None: self.state = AppState(pool) class RequestStub: def __init__(self, pool: Any) -> None: self.app = AppStub(pool) class FakeConn: def __init__(self) -> None: password_hash = "velocity-demo-password" self.users: dict[str, dict[str, Any]] = { "user-alpha": { "id": "00000000-0000-0000-0000-000000000001", "email": "alpha@example.com", "password_hash": password_hash, "role": "ADMIN", "tenant_id": "tenant_alpha", "full_name": "Alpha Operator", "avatar_url": "/assets/profile_avatars/alpha.png", "is_active": True, }, "user-beta": { "id": "00000000-0000-0000-0000-000000000002", "email": "beta@example.com", "password_hash": password_hash, "role": "ADMIN", "tenant_id": "tenant_beta", "full_name": "Beta Operator", "avatar_url": "/assets/profile_avatars/beta.png", "is_active": True, }, "user-legacy": { "id": "00000000-0000-0000-0000-000000000003", "email": "legacy@example.com", "password_hash": password_hash, "role": "SENIOR_BROKER", "tenant_id": "", "full_name": "Legacy Tenant User", "avatar_url": None, "is_active": True, }, } self.schema_ready = False async def execute(self, query: str, *args): normalized = " ".join(query.strip().split()) if normalized.startswith("ALTER TABLE users_and_roles ADD COLUMN IF NOT EXISTS tenant_id TEXT"): for user in self.users.values(): user.setdefault("tenant_id", "") return "ALTER TABLE" if normalized.startswith("UPDATE users_and_roles SET tenant_id = $1 WHERE tenant_id IS NULL OR tenant_id = ''"): for user in self.users.values(): if not user.get("tenant_id"): user["tenant_id"] = args[0] return "UPDATE" if normalized.startswith("ALTER TABLE users_and_roles ALTER COLUMN tenant_id SET DEFAULT"): return "ALTER TABLE" if normalized.startswith("ALTER TABLE users_and_roles ALTER COLUMN tenant_id SET NOT NULL"): self.schema_ready = True return "ALTER TABLE" if normalized.startswith("CREATE INDEX IF NOT EXISTS idx_users_tenant_active ON users_and_roles (tenant_id, is_active)"): return "CREATE INDEX" if normalized.startswith("UPDATE users_and_roles SET avatar_url = $2 WHERE id = $1::uuid AND tenant_id = $3"): for user in self.users.values(): if user["id"] == args[0] and user["tenant_id"] == args[2]: user["avatar_url"] = args[1] return "UPDATE 1" return "UPDATE 0" raise AssertionError(f"Unexpected execute query: {query}") async def fetchrow(self, query: str, *args): normalized = " ".join(query.strip().split()) if "FROM users_and_roles" not in normalized: raise AssertionError(f"Unexpected fetchrow query: {query}") if "password_hash" in normalized: email, tenant_fallback = args for user in self.users.values(): if user["email"] == email and user["is_active"]: return { "id": user["id"], "role": user["role"], "password_hash": user["password_hash"], "tenant_id": user["tenant_id"] or tenant_fallback, } return None if "WHERE id = $1::uuid AND COALESCE(NULLIF(tenant_id, ''), $2) = $2" in normalized: user_id, tenant_id = args for user in self.users.values(): resolved_tenant = user["tenant_id"] or tenant_id if user["id"] == user_id and resolved_tenant == tenant_id: return { "full_name": user["full_name"], "email": user["email"], "avatar_url": user["avatar_url"], "tenant_id": resolved_tenant, } return None raise AssertionError(f"Unexpected fetchrow query: {query}") async def fetch(self, query: str, *args): normalized = " ".join(query.strip().split()) if "FROM users_and_roles" not in normalized: raise AssertionError(f"Unexpected fetch query: {query}") tenant_fallback, tenant_id = args rows = [] for user in self.users.values(): resolved_tenant = user["tenant_id"] or tenant_fallback if user["is_active"] and resolved_tenant == tenant_id: rows.append( { "user_id": user["id"], "role": user["role"], "tenant_id": resolved_tenant, "full_name": user["full_name"], "email": user["email"], "avatar_url": user["avatar_url"], } ) rows.sort(key=lambda row: (row["full_name"] or row["email"] or row["user_id"])) return rows class FakePool: def __init__(self) -> None: self.conn = FakeConn() @asynccontextmanager async def acquire(self): yield self.conn def _build_request() -> tuple[RequestStub, FakePool]: pool = FakePool() return RequestStub(pool), pool def test_login_mints_token_with_user_tenant_id() -> None: request, pool = _build_request() response = asyncio.run( auth_service.login_with_directory( app=request.app, email="alpha@example.com", password="velocity-demo-password", ) ) payload = jwt.get_unverified_claims(response["access_token"]) assert payload["tenant_id"] == "tenant_alpha" assert pool.conn.schema_ready is True def test_login_backfills_legacy_users_to_default_tenant_before_minting() -> None: request, pool = _build_request() response = asyncio.run( auth_service.login_with_directory( app=request.app, email="legacy@example.com", password="velocity-demo-password", ) ) payload = jwt.get_unverified_claims(response["access_token"]) assert payload["tenant_id"] == "tenant_velocity" assert pool.conn.users["user-legacy"]["tenant_id"] == "tenant_velocity" def test_auth_me_returns_profile_for_authenticated_tenant() -> None: request, _pool = _build_request() user = UserPrincipal("00000000-0000-0000-0000-000000000001", "ADMIN", "tenant_alpha") response = asyncio.run(auth_service.read_authenticated_user_profile(app=request.app, user=user)) assert response["tenant_id"] == "tenant_alpha" assert response["email"] == "alpha@example.com" def test_auth_users_are_scoped_to_authenticated_tenant() -> None: request, _pool = _build_request() user = UserPrincipal("00000000-0000-0000-0000-000000000001", "ADMIN", "tenant_alpha") users = asyncio.run(auth_service.list_tenant_users(app=request.app, user=user)) assert [user["email"] for user in users] == ["alpha@example.com"] assert all(user["tenant_id"] == "tenant_alpha" for user in users)