mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
* test: migrate tests/proxy_behavior into integration and unit tiers (partial) * test(integration): run settings-dependent management tests on an owned proxy * test(integration): assert stored values in the rewritten management rows * test(integration): pin sibling team allocations against the org guaranteed throughput cap * test(integration): keep Prisma clients out of the shared integration conftest import path --------- Co-authored-by: yuneng <yuneng@berri.ai>
137 lines
5.8 KiB
Python
137 lines
5.8 KiB
Python
import pytest
|
|
|
|
from tests.integration._support.actors import Actor
|
|
from tests.integration._support.actors import create_scratch_team, create_scratch_user
|
|
|
|
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
|
|
_URL = "/management/v1/users/bulk_delete"
|
|
|
|
# (id, actor, victims' org, expected status, whether the victims are gone afterwards)
|
|
_MATRIX = [
|
|
("org_a/proxy_admin", Actor.PROXY_ADMIN, "a", 200, True),
|
|
("org_a/org_admin", Actor.ORG_ADMIN, "a", 200, True),
|
|
("org_a/org_b_admin", Actor.ORG_B_ADMIN, "a", 200, False),
|
|
("org_a/team_admin", Actor.TEAM_ADMIN, "a", 403, False),
|
|
("org_a/internal_user", Actor.INTERNAL_USER, "a", 403, False),
|
|
("org_a/owner", Actor.OWNER, "a", 403, False),
|
|
("org_a/service_account", Actor.SERVICE_ACCOUNT, "a", 403, False),
|
|
("no_org/proxy_admin", Actor.PROXY_ADMIN, None, 200, True),
|
|
("no_org/org_admin", Actor.ORG_ADMIN, None, 200, False),
|
|
]
|
|
|
|
|
|
def _member_ids(row) -> list:
|
|
return [m["user_id"] for m in (row.members_with_roles or [])]
|
|
|
|
|
|
async def _seed_team_members(db, resources, actors, member_ids: list, org_id) -> None:
|
|
"""Leave behind what /team/member_add would: roster entry, `teams` array, and org membership."""
|
|
await create_scratch_team(db, resources.prefix, organization_id=actors.org_a_id, member_user_ids=member_ids)
|
|
await db.db.litellm_usertable.update_many(
|
|
where={"user_id": {"in": member_ids}}, data={"teams": {"set": [resources.prefix]}}
|
|
)
|
|
if org_id is None:
|
|
return
|
|
for uid in member_ids:
|
|
await db.db.litellm_organizationmembership.create(
|
|
data={"user_id": uid, "organization_id": org_id, "user_role": "internal_user"}
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"actor,org,expected_status,expect_deleted",
|
|
[(a, o, s, d) for (_id, a, o, s, d) in _MATRIX],
|
|
ids=[s[0] for s in _MATRIX],
|
|
)
|
|
async def test_users_bulk_delete_authz_matrix(
|
|
actor: Actor,
|
|
org,
|
|
expected_status: int,
|
|
expect_deleted: bool,
|
|
gateway_client,
|
|
db,
|
|
resources,
|
|
actors,
|
|
):
|
|
victims = [await create_scratch_user(db, resources.prefix, suffix=s) for s in ("v1", "v2")]
|
|
keep = await create_scratch_user(db, resources.prefix, suffix="keep")
|
|
await _seed_team_members(db, resources, actors, victims + [keep], actors.org_a_id if org == "a" else None)
|
|
|
|
resp = await gateway_client.post(
|
|
_URL,
|
|
headers={"Authorization": f"Bearer {actors.keys[actor].cleartext}"},
|
|
json={"user_ids": victims},
|
|
)
|
|
assert resp.status_code == expected_status, f"{actor.value}: {resp.status_code} {resp.text}"
|
|
|
|
team = await db.db.litellm_teamtable.find_unique(where={"team_id": resources.prefix})
|
|
assert team is not None and keep in _member_ids(team), "unrelated member removed"
|
|
remaining = {u.user_id for u in await db.db.litellm_usertable.find_many(where={"user_id": {"in": victims}})}
|
|
if expected_status == 403:
|
|
assert resp.headers["content-type"] == "application/problem+json"
|
|
assert resp.json()["type"] == "urn:litellm:error:forbidden"
|
|
assert remaining == set(victims), "denied but users deleted"
|
|
assert set(victims) <= set(_member_ids(team)), "denied but members removed"
|
|
return
|
|
|
|
body = resp.json()
|
|
assert set(body) == {"data"}
|
|
rows = [(r["user_id"], r["success"], r["teams_removed"]) for r in body["data"]]
|
|
if expect_deleted:
|
|
assert rows == [(v, True, [resources.prefix]) for v in victims]
|
|
assert remaining == set()
|
|
assert not set(victims) & set(_member_ids(team))
|
|
return
|
|
assert rows == [(v, False, []) for v in victims]
|
|
assert all("not within your admin scope" in r["error"] for r in body["data"])
|
|
assert remaining == set(victims), "out-of-scope rows reported failed but users deleted"
|
|
assert set(victims) <= set(_member_ids(team))
|
|
|
|
|
|
async def test_users_bulk_delete_reports_each_row_in_order(gateway_client, db, resources, actors):
|
|
victim = await create_scratch_user(db, resources.prefix, suffix="victim")
|
|
ghost = resources.tag("ghost")
|
|
|
|
resp = await gateway_client.post(
|
|
_URL,
|
|
headers={"Authorization": f"Bearer {actors.keys[Actor.PROXY_ADMIN].cleartext}"},
|
|
json={"user_ids": [ghost, victim, victim]},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
assert [(r["user_id"], r["success"]) for r in resp.json()["data"]] == [
|
|
(ghost, False),
|
|
(victim, True),
|
|
(victim, False),
|
|
]
|
|
assert await db.db.litellm_usertable.find_unique(where={"user_id": victim}) is None
|
|
|
|
|
|
async def test_users_bulk_delete_unknown_query_param_is_400_problem(gateway_client, db, resources, actors):
|
|
victim = await create_scratch_user(db, resources.prefix, suffix="victim")
|
|
|
|
resp = await gateway_client.post(
|
|
f"{_URL}?dry_run=1",
|
|
headers={"Authorization": f"Bearer {actors.keys[Actor.PROXY_ADMIN].cleartext}"},
|
|
json={"user_ids": [victim]},
|
|
)
|
|
assert resp.status_code == 400, resp.text
|
|
assert resp.headers["content-type"] == "application/problem+json"
|
|
assert resp.json()["type"] == "urn:litellm:error:unknown-query-parameter"
|
|
assert "dry_run" in resp.json()["detail"]
|
|
assert await db.db.litellm_usertable.find_unique(where={"user_id": victim}) is not None
|
|
|
|
|
|
async def test_users_bulk_delete_unknown_body_field_is_422_problem(gateway_client, db, resources, actors):
|
|
victim = await create_scratch_user(db, resources.prefix, suffix="victim")
|
|
|
|
resp = await gateway_client.post(
|
|
_URL,
|
|
headers={"Authorization": f"Bearer {actors.keys[Actor.PROXY_ADMIN].cleartext}"},
|
|
json={"user_ids": [victim], "dry_run": True},
|
|
)
|
|
assert resp.status_code == 422, resp.text
|
|
assert resp.headers["content-type"] == "application/problem+json"
|
|
assert resp.json()["type"] == "urn:litellm:error:invalid-request-body"
|
|
assert "dry_run" in resp.json()["detail"]
|
|
assert await db.db.litellm_usertable.find_unique(where={"user_id": victim}) is not None
|