Skip to content

Commit eae002b

Browse files
committed
added test for _upsert_budget_membership func
1 parent fc59d46 commit eae002b

File tree

2 files changed

+170
-2
lines changed

2 files changed

+170
-2
lines changed

litellm/proxy/management_endpoints/common_utils.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ def _set_object_metadata_field(
4848
object_data.metadata[field_name] = value
4949

5050

51-
5251
async def _upsert_budget_and_membership(
5352
tx,
5453
*,
@@ -119,4 +118,3 @@ async def _upsert_budget_and_membership(
119118
},
120119
},
121120
)
122-
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# tests/litellm/proxy/common_utils/test_upsert_budget_membership.py
2+
import types
3+
import pytest
4+
from unittest.mock import AsyncMock, MagicMock
5+
6+
from litellm.proxy.management_endpoints.common_utils import (
7+
_upsert_budget_and_membership,
8+
)
9+
10+
11+
# ---------------------------------------------------------------------------
12+
# Fixtures: a fake Prisma transaction and a fake UserAPIKeyAuth object
13+
# ---------------------------------------------------------------------------
14+
15+
@pytest.fixture
16+
def mock_tx():
17+
"""
18+
Builds an object that looks just enough like the Prisma tx you use
19+
inside _upsert_budget_and_membership.
20+
"""
21+
# membership “table”
22+
membership = MagicMock()
23+
membership.update = AsyncMock()
24+
membership.upsert = AsyncMock()
25+
26+
# budget “table”
27+
budget = MagicMock()
28+
budget.update = AsyncMock()
29+
# budget.create returns a fake row that has .budget_id
30+
budget.create = AsyncMock(
31+
return_value=types.SimpleNamespace(budget_id="new-budget-123")
32+
)
33+
34+
tx = MagicMock()
35+
tx.litellm_teammembership = membership
36+
tx.litellm_budgettable = budget
37+
return tx
38+
39+
40+
@pytest.fixture
41+
def fake_user():
42+
"""Cheap stand-in for UserAPIKeyAuth."""
43+
return types.SimpleNamespace(user_id="[email protected]")
44+
45+
# TEST: max_budget is None, disconnect only
46+
@pytest.mark.asyncio
47+
async def test_upsert_disconnect(mock_tx, fake_user):
48+
await _upsert_budget_and_membership(
49+
mock_tx,
50+
team_id="team-1",
51+
user_id="user-1",
52+
max_budget=None,
53+
existing_budget_id=None,
54+
user_api_key_dict=fake_user,
55+
)
56+
57+
mock_tx.litellm_teammembership.update.assert_awaited_once_with(
58+
where={"user_id_team_id": {"user_id": "user-1", "team_id": "team-1"}},
59+
data={"litellm_budget_table": {"disconnect": True}},
60+
)
61+
mock_tx.litellm_budgettable.update.assert_not_called()
62+
mock_tx.litellm_budgettable.create.assert_not_called()
63+
mock_tx.litellm_teammembership.upsert.assert_not_called()
64+
65+
66+
# TEST: existing budget id, update only
67+
@pytest.mark.asyncio
68+
async def test_upsert_update_existing(mock_tx, fake_user):
69+
await _upsert_budget_and_membership(
70+
mock_tx,
71+
team_id="team-2",
72+
user_id="user-2",
73+
max_budget=42.0,
74+
existing_budget_id="bud-999",
75+
user_api_key_dict=fake_user,
76+
)
77+
78+
mock_tx.litellm_budgettable.update.assert_awaited_once_with(
79+
where={"budget_id": "bud-999"},
80+
data={"max_budget": 42.0},
81+
)
82+
mock_tx.litellm_teammembership.update.assert_not_called()
83+
mock_tx.litellm_budgettable.create.assert_not_called()
84+
mock_tx.litellm_teammembership.upsert.assert_not_called()
85+
86+
87+
# TEST: create new budget and link membership
88+
@pytest.mark.asyncio
89+
async def test_upsert_create_and_link(mock_tx, fake_user):
90+
await _upsert_budget_and_membership(
91+
mock_tx,
92+
team_id="team-3",
93+
user_id="user-3",
94+
max_budget=99.9,
95+
existing_budget_id=None,
96+
user_api_key_dict=fake_user,
97+
)
98+
99+
mock_tx.litellm_budgettable.create.assert_awaited_once_with(
100+
data={
101+
"max_budget": 99.9,
102+
"created_by": fake_user.user_id,
103+
"updated_by": fake_user.user_id,
104+
},
105+
include={"team_membership": True},
106+
)
107+
108+
# Budget ID returned by the mocked create()
109+
bid = mock_tx.litellm_budgettable.create.return_value.budget_id
110+
111+
mock_tx.litellm_teammembership.upsert.assert_awaited_once_with(
112+
where={"user_id_team_id": {"user_id": "user-3", "team_id": "team-3"}},
113+
data={
114+
"create": {
115+
"user_id": "user-3",
116+
"team_id": "team-3",
117+
"litellm_budget_table": {"connect": {"budget_id": bid}},
118+
},
119+
"update": {
120+
"litellm_budget_table": {"connect": {"budget_id": bid}},
121+
},
122+
},
123+
)
124+
125+
mock_tx.litellm_teammembership.update.assert_not_called()
126+
mock_tx.litellm_budgettable.update.assert_not_called()
127+
128+
129+
# TEST: create new budget and link membership, then update
130+
@pytest.mark.asyncio
131+
async def test_upsert_create_then_update(mock_tx, fake_user):
132+
# FIRST CALL – create new budget and link membership
133+
await _upsert_budget_and_membership(
134+
mock_tx,
135+
team_id="team-42",
136+
user_id="user-42",
137+
max_budget=10.0,
138+
existing_budget_id=None,
139+
user_api_key_dict=fake_user,
140+
)
141+
142+
# capture the budget id that create() returned
143+
created_bid = mock_tx.litellm_budgettable.create.return_value.budget_id
144+
145+
# sanity: we really did the create + upsert path
146+
mock_tx.litellm_budgettable.create.assert_awaited_once()
147+
mock_tx.litellm_teammembership.upsert.assert_awaited_once()
148+
149+
# SECOND CALL – pretend the same membership already exists, and
150+
# reset call history so the next assertions are clear
151+
mock_tx.litellm_budgettable.create.reset_mock()
152+
mock_tx.litellm_teammembership.upsert.reset_mock()
153+
mock_tx.litellm_budgettable.update.reset_mock()
154+
155+
await _upsert_budget_and_membership(
156+
mock_tx,
157+
team_id="team-42",
158+
user_id="user-42",
159+
max_budget=25.0, # new limit
160+
existing_budget_id=created_bid, # now we say it exists
161+
user_api_key_dict=fake_user,
162+
)
163+
164+
# Now we expect ONLY an update to fire
165+
mock_tx.litellm_budgettable.update.assert_awaited_once_with(
166+
where={"budget_id": created_bid},
167+
data={"max_budget": 25.0},
168+
)
169+
mock_tx.litellm_budgettable.create.assert_not_called()
170+
mock_tx.litellm_teammembership.upsert.assert_not_called()

0 commit comments

Comments
 (0)