litellm/tests/proxy_behavior/management/test_team_info.py
yuneng-jiang 67e6e5e1df
test(proxy): behavior-pinning matrix for team management endpoints (#28441)
* test(proxy): behavior-pinning matrix for team management endpoints

PR2 (Team Tier-1) of the management-endpoint behavior-pinning effort.
Extends the tests/proxy_behavior/management/ harness PR1 built and adds
the actor x target-resource authz matrix for the 7 team endpoints:
/team/new, /team/info, /team/list, /team/update, /team/member_add,
/team/member_delete, /team/member_update.

Tests-only, no production code changes.

Harness extensions:
- actors.py: ORG_B_ADMIN actor (org admin of ORG_B) and TEAM_GAMMA (an
  ORG_A team with no actor members), so team-targeting endpoints get a
  clean own / same-org-other / cross-org target axis.
- conftest.py: create_scratch_team() raw-seeds target teams without
  /team/new side effects; the scratch teardown now also strips dangling
  scratch-team refs from LiteLLM_UserTable.teams.

156 new scenarios; status codes pinned to observed handler behavior.

* test(proxy): record mutmut run blockers in PR2 triage doc

Attempted a scoped local mutmut run for G5; it did not complete. Record
the three concrete blockers in mutmut_triage/pr2-team-tier1.md so the next
attempt has a head start:

1. mutmut's mutants/ sandbox is import-shadowed by the worktree source.
2. the legacy mock suite and the real-DB behavior suite cannot share a
   pytest session (mock suite globally patches prisma_client).
3. the CI mutation-test.yml workflow starts no Postgres, so its stats
   phase now aborts on the behavior-suite tests PR1 added to tests_dir.

mutmut stays a deferred follow-up (as in PR1); the binding pre-merge
signal remains the behavior matrix (G1) and the G4 regression-replay.

* test(proxy): drop suite README + triage doc, trim test comments

Remove the two prose docs from the behavior suite (README.md and
mutmut_triage/pr2-team-tier1.md) and tighten the comment blocks on the
team test files + harness down to the load-bearing parts (the gate each
matrix pins, plus genuinely surprising results). No behavior change —
all 286 scenarios still pass.

* test(proxy): remove mutmut tests_dir comment
2026-05-21 16:57:25 -07:00

70 lines
2.9 KiB
Python

import pytest
from .actors import Actor
pytestmark = pytest.mark.asyncio(loop_scope="session")
# GET /team/info — actor x team-target authz matrix, pinned against
# validate_membership(): a team is readable by a proxy admin, a key whose
# own team_id matches, a listed member, or an org admin of the team's org;
# everything else is 403. TEAM_GAMMA has no members, so only PROXY_ADMIN
# and ORG_A's org admin can read it.
_SCENARIOS = [
("alpha/proxy_admin", Actor.PROXY_ADMIN, "alpha", 200),
("alpha/org_admin", Actor.ORG_ADMIN, "alpha", 200),
("alpha/team_admin", Actor.TEAM_ADMIN, "alpha", 200),
("alpha/internal_user", Actor.INTERNAL_USER, "alpha", 200),
("alpha/owner", Actor.OWNER, "alpha", 200),
("alpha/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "alpha", 200),
("alpha/cross_org_user", Actor.CROSS_ORG_USER, "alpha", 403),
("alpha/service_account", Actor.SERVICE_ACCOUNT, "alpha", 200),
("alpha/org_b_admin", Actor.ORG_B_ADMIN, "alpha", 403),
("gamma/proxy_admin", Actor.PROXY_ADMIN, "gamma", 200),
("gamma/org_admin", Actor.ORG_ADMIN, "gamma", 200),
("gamma/team_admin", Actor.TEAM_ADMIN, "gamma", 403),
("gamma/internal_user", Actor.INTERNAL_USER, "gamma", 403),
("gamma/owner", Actor.OWNER, "gamma", 403),
("gamma/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "gamma", 403),
("gamma/cross_org_user", Actor.CROSS_ORG_USER, "gamma", 403),
("gamma/service_account", Actor.SERVICE_ACCOUNT, "gamma", 403),
("gamma/org_b_admin", Actor.ORG_B_ADMIN, "gamma", 403),
("beta/proxy_admin", Actor.PROXY_ADMIN, "beta", 200),
("beta/org_admin", Actor.ORG_ADMIN, "beta", 403),
("beta/team_admin", Actor.TEAM_ADMIN, "beta", 403),
("beta/internal_user", Actor.INTERNAL_USER, "beta", 403),
("beta/owner", Actor.OWNER, "beta", 403),
("beta/unrelated_same_org", Actor.UNRELATED_SAME_ORG, "beta", 403),
("beta/cross_org_user", Actor.CROSS_ORG_USER, "beta", 200),
("beta/service_account", Actor.SERVICE_ACCOUNT, "beta", 403),
("beta/org_b_admin", Actor.ORG_B_ADMIN, "beta", 200),
]
@pytest.mark.parametrize(
"actor,target,expected_status",
[(a, t, s) for (_id, a, t, s) in _SCENARIOS],
ids=[s[0] for s in _SCENARIOS],
)
async def test_team_info_authz_matrix(
actor: Actor, target: str, expected_status: int, proxy_client, world
):
caller = world.keys[actor]
target_team_id = {
"alpha": world.team_alpha_id,
"gamma": world.team_gamma_id,
"beta": world.team_beta_id,
}[target]
resp = await proxy_client.get(
f"/team/info?team_id={target_team_id}",
headers={"Authorization": f"Bearer {caller.cleartext}"},
)
assert (
resp.status_code == expected_status
), f"{actor.value} -> {target}: {resp.status_code} {resp.text}"
if expected_status == 200:
body = resp.json()
assert body["team_id"] == target_team_id
assert body["team_info"]["team_id"] == target_team_id