forked from sagnik/Project_Velocity
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#41
213 lines
7.9 KiB
Python
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)
|