mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
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
88 lines
2.6 KiB
Python
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)
|