Files
Project_Velocity/backend/tests/test_auth_tenant_contract.py
2026-04-28 11:32:56 +05:30

213 lines
7.9 KiB
Python

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)