test(auth_v2): cover DbIdentityStore group CRUD; sync dashboard API types

Regression tests for upsert_group/get_group/list_groups/delete_group against a
fake prisma that mirrors the one behavior they depend on (a Json-wrapped write
round-trips as its plain value), so a future drop of the prisma Json wrapper or
the typed read fails here rather than only against a live database.

Regenerate ui/litellm-dashboard/src/lib/http/schema.d.ts from the proxy
OpenAPI spec now that the /admin/teams routes are mounted.
This commit is contained in:
Claude 2026-06-13 18:26:56 +00:00
parent 8f401a8aaf
commit 86ac48701c
No known key found for this signature in database
2 changed files with 274 additions and 0 deletions

View file

@ -3,6 +3,9 @@ from __future__ import annotations
from typing import Dict, Optional
import pytest
from prisma import Json
from scim2_models import Group as ScimGroup
from scim2_models import GroupMember as ScimGroupMember
from litellm.proxy._types import LiteLLM_UserTable, UserAPIKeyAuth, hash_token
from litellm.proxy.auth_v2.authorization import Role
@ -153,3 +156,73 @@ async def test_mtls_credential_resolves_to_service_account():
assert principal.principal_type == PrincipalType.SERVICE_ACCOUNT
assert principal.user is None
assert principal.subject == "CN=svc-a,O=Co"
class _FakeTeamTable:
"""Minimal stand-in for the prisma team table.
Mirrors the one prisma behavior the group methods depend on: a Json-wrapped
write is stored as its plain Python value and read back the same way. Storing
the raw value would let a regression that forgets the Json wrapper pass here
while failing against a real database.
"""
def __init__(self) -> None:
self.rows: Dict[str, dict] = {}
@staticmethod
def _norm(data: dict) -> dict:
return {k: (v.data if isinstance(v, Json) else v) for k, v in data.items()}
async def find_unique(self, where):
return self.rows.get(where["team_id"])
async def create(self, data):
row = self._norm(data)
self.rows[row["team_id"]] = row
return row
async def update(self, where, data):
self.rows[where["team_id"]].update(self._norm(data))
return self.rows[where["team_id"]]
async def find_many(self, **kwargs):
return list(self.rows.values())
async def delete(self, where):
return self.rows.pop(where["team_id"], None)
class _FakeTeamPrisma:
def __init__(self) -> None:
self.db = type("_Db", (), {"litellm_teamtable": _FakeTeamTable()})()
async def test_group_upsert_round_trips_members_through_json():
store = DbIdentityStore(_FakeTeamPrisma(), _FakeCache())
created = await store.upsert_group(
ScimGroup(
display_name="eng",
members=[ScimGroupMember(value="u-1"), ScimGroupMember(value="u-2")],
)
)
assert created.id is not None
assert created.display_name == "eng"
assert [m.value for m in created.members] == ["u-1", "u-2"]
fetched = await store.get_group(created.id)
assert fetched is not None
assert [m.value for m in fetched.members] == ["u-1", "u-2"]
async def test_group_list_and_delete():
store = DbIdentityStore(_FakeTeamPrisma(), _FakeCache())
a = await store.upsert_group(ScimGroup(display_name="a"))
await store.upsert_group(ScimGroup(display_name="b"))
assert {g.display_name for g in await store.list_groups(None)} == {"a", "b"}
await store.delete_group(a.id)
assert await store.get_group(a.id) is None
assert {g.display_name for g in await store.list_groups(None)} == {"b"}

View file

@ -416,6 +416,43 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/teams": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List Teams */
get: operations["list_teams_admin_teams_get"];
put?: never;
/** Create Team */
post: operations["create_team_admin_teams_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/teams/{team_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Team */
get: operations["get_team_admin_teams__team_id__get"];
/** Upsert Team */
put: operations["upsert_team_admin_teams__team_id__put"];
post?: never;
/** Delete Team */
delete: operations["delete_team_admin_teams__team_id__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/agent/daily/activity": {
parameters: {
query?: never;
@ -30580,6 +30617,22 @@ export interface components {
/** Team Id */
team_id: string;
};
/** TeamUpsert */
TeamUpsert: {
/** Members */
members?: string[];
/** Name */
name: string;
};
/** TeamView */
TeamView: {
/** Id */
id: string;
/** Members */
members: string[];
/** Name */
name: string;
};
/**
* TestCustomCodeGuardrailRequest
* @description Request model for testing custom code guardrails.
@ -33107,6 +33160,154 @@ export interface operations {
};
};
};
list_teams_admin_teams_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TeamView"][];
};
};
};
};
create_team_admin_teams_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TeamUpsert"];
};
};
responses: {
/** @description Successful Response */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TeamView"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_team_admin_teams__team_id__get: {
parameters: {
query?: never;
header?: never;
path: {
team_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TeamView"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
upsert_team_admin_teams__team_id__put: {
parameters: {
query?: never;
header?: never;
path: {
team_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TeamUpsert"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TeamView"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_team_admin_teams__team_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
team_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_agent_daily_activity_agent_daily_activity_get: {
parameters: {
query?: {