litellm/tests/proxy_behavior/management/conftest.py
Yuneng Jiang 4c3879a88e
test(proxy_behavior): seed 8-actor read-world for the authz matrix
Slice 4 of the management-endpoints behavior-pinning effort. New
``actors.py`` defines the actor enum + seeds an immutable world (2 orgs,
2 teams, 8 users, 8 verification tokens) under the ``behavior-pin-``
prefix so the rows are identifiable in psql and ``_wipe_world`` is
targeted.

Each actor key is created with its cleartext form generated locally and
its hashed form (via ``litellm.proxy.utils.hash_token``) stored in
``LiteLLM_VerificationToken`` — so the real ``user_api_key_auth`` accepts
the cleartext bearer token. Roles, ``team_id``, ``organization_id``, and
the service-account metadata flag are all set on the seeded rows so the
auth layer resolves the same scopes a real proxy would.

The session-scoped ``world`` fixture re-seeds at session start (idempotent
via wipe-then-create), and the smoke test confirms each of the 8 actor
keys can call ``/key/info`` on itself and receive its own row back.

Plan: https://www.notion.so/36643b8acdab8128a581ced0f6a4744d
2026-05-19 21:31:38 -07:00

88 lines
2.6 KiB
Python

"""Session-scoped async ASGI client for behavior-pinning tests.
The proxy app is initialised once per pytest session against the real Postgres
pointed at by ``DATABASE_URL``. No mocks: auth runs, prisma runs, integrations
run. Tests assert at the HTTP boundary.
"""
import os
import tempfile
from typing import AsyncIterator
import httpx
import pytest_asyncio
import yaml
MASTER_KEY = "sk-1234"
def _write_minimal_proxy_config() -> str:
config = {
"general_settings": {"master_key": MASTER_KEY},
"litellm_settings": {},
}
database_url = os.environ.get("DATABASE_URL")
if database_url:
config["general_settings"]["database_url"] = database_url
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False)
yaml.dump(config, f)
f.close()
return f.name
@pytest_asyncio.fixture(scope="session")
async def proxy_app():
"""Boot the proxy app once per session with the real FastAPI lifespan.
httpx 0.28's ASGITransport does not run the lifespan handler, so we enter
``proxy_startup_event`` (the @asynccontextmanager registered as the app's
lifespan) directly. That handler is where ``prisma_client`` is connected
and the rest of the startup wiring runs.
"""
from litellm.proxy.proxy_server import (
app,
cleanup_router_config_variables,
initialize,
proxy_startup_event,
)
cleanup_router_config_variables()
config_path = _write_minimal_proxy_config()
await initialize(config=config_path)
async with proxy_startup_event(app):
yield app
@pytest_asyncio.fixture(scope="session")
async def proxy_client(proxy_app) -> AsyncIterator[httpx.AsyncClient]:
transport = httpx.ASGITransport(app=proxy_app)
async with httpx.AsyncClient(
transport=transport, base_url="http://testserver"
) as client:
yield client
@pytest_asyncio.fixture(scope="session")
async def prisma(proxy_app):
"""The connected PrismaClient the lifespan opened."""
from litellm.proxy import proxy_server
assert (
proxy_server.prisma_client is not None
), "FastAPI lifespan did not connect prisma — harness is wrong."
return proxy_server.prisma_client
@pytest_asyncio.fixture(scope="session")
async def world(prisma):
"""The immutable read-world seed.
Re-seeds at session start so each pytest invocation gets a clean world.
Tests must not mutate these rows; write tests use Slice 5's namespace +
teardown fixtures for scratch entities.
"""
from .actors import seed_world
return await seed_world(prisma)