@@ -291,7 +291,9 @@ async def test_new_team_with_object_permission(mock_db_client, mock_admin_auth):
291
291
mock_db_client .db .litellm_teamtable = MagicMock ()
292
292
mock_db_client .db .litellm_teamtable .create = mock_team_create
293
293
mock_db_client .db .litellm_teamtable .count = mock_team_count
294
- mock_db_client .db .litellm_teamtable .update = AsyncMock (return_value = team_create_result )
294
+ mock_db_client .db .litellm_teamtable .update = AsyncMock (
295
+ return_value = team_create_result
296
+ )
295
297
296
298
# 4. Mock user table update behaviour (called for each member)
297
299
mock_db_client .db .litellm_usertable = MagicMock ()
@@ -1013,3 +1015,160 @@ def test_add_new_models_to_team_with_existing_models():
1013
1015
)
1014
1016
1015
1017
assert updated_models .sort () == ["model1" , "model2" , "model3" , "model4" ].sort ()
1018
+
1019
+
1020
+ @pytest .mark .asyncio
1021
+ async def test_update_team_team_member_budget_not_passed_to_db ():
1022
+ """
1023
+ Test that 'team_member_budget' is never passed to prisma_client.db.litellm_teamtable.update
1024
+ regardless of whether the value is set or None.
1025
+
1026
+ This ensures that team_member_budget is properly handled via the separate budget table
1027
+ and not accidentally passed to the team table update operation.
1028
+ """
1029
+ from unittest .mock import AsyncMock , MagicMock , Mock , patch
1030
+
1031
+ from fastapi import Request
1032
+
1033
+ from litellm .proxy ._types import LitellmUserRoles , UpdateTeamRequest , UserAPIKeyAuth
1034
+ from litellm .proxy .management_endpoints .team_endpoints import update_team
1035
+
1036
+ # Mock dependencies
1037
+ mock_request = Mock (spec = Request )
1038
+ mock_user_api_key_dict = UserAPIKeyAuth (
1039
+ user_role = LitellmUserRoles .PROXY_ADMIN , user_id = "test_user_id"
1040
+ )
1041
+
1042
+ with patch ("litellm.proxy.proxy_server.prisma_client" ) as mock_prisma_client , patch (
1043
+ "litellm.proxy.proxy_server.llm_router"
1044
+ ) as mock_llm_router , patch (
1045
+ "litellm.proxy.proxy_server.user_api_key_cache"
1046
+ ) as mock_cache , patch (
1047
+ "litellm.proxy.proxy_server.proxy_logging_obj"
1048
+ ) as mock_logging , patch (
1049
+ "litellm.proxy.proxy_server.litellm_proxy_admin_name" , "admin"
1050
+ ), patch (
1051
+ "litellm.proxy.auth.auth_checks._cache_team_object"
1052
+ ) as mock_cache_team , patch (
1053
+ "litellm.proxy.management_endpoints.team_endpoints._upsert_team_member_budget_table"
1054
+ ) as mock_upsert_budget :
1055
+
1056
+ # Setup mock prisma client
1057
+ mock_existing_team = MagicMock ()
1058
+ mock_existing_team .model_dump .return_value = {
1059
+ "team_id" : "test_team_id" ,
1060
+ "team_alias" : "test_team" ,
1061
+ "metadata" : {"team_member_budget_id" : "budget_123" },
1062
+ }
1063
+ mock_prisma_client .db .litellm_teamtable .find_unique = AsyncMock (
1064
+ return_value = mock_existing_team
1065
+ )
1066
+
1067
+ # Mock the update return value
1068
+ mock_updated_team = MagicMock ()
1069
+ mock_updated_team .team_id = "test_team_id"
1070
+ mock_updated_team .model_dump .return_value = {"team_id" : "test_team_id" }
1071
+ mock_prisma_client .db .litellm_teamtable .update = AsyncMock (
1072
+ return_value = mock_updated_team
1073
+ )
1074
+ mock_prisma_client .jsonify_team_object = MagicMock (
1075
+ side_effect = lambda db_data : db_data
1076
+ )
1077
+
1078
+ # Mock budget upsert to return updated_kv without team_member_budget
1079
+ def mock_upsert_side_effect (
1080
+ team_table , updated_kv , team_member_budget , user_api_key_dict
1081
+ ):
1082
+ # Remove team_member_budget from updated_kv as the real function does
1083
+ result_kv = updated_kv .copy ()
1084
+ result_kv .pop ("team_member_budget" , None )
1085
+ return result_kv
1086
+
1087
+ mock_upsert_budget .side_effect = mock_upsert_side_effect
1088
+
1089
+ # Test Case 1: team_member_budget is set (not None)
1090
+ update_request_with_budget = UpdateTeamRequest (
1091
+ team_id = "test_team_id" , team_member_budget = 100.0 , team_alias = "updated_alias"
1092
+ )
1093
+
1094
+ result = await update_team (
1095
+ data = update_request_with_budget ,
1096
+ http_request = mock_request ,
1097
+ user_api_key_dict = mock_user_api_key_dict ,
1098
+ )
1099
+
1100
+ # Verify update was called
1101
+ assert mock_prisma_client .db .litellm_teamtable .update .called
1102
+
1103
+ # Get the call arguments
1104
+ call_args = mock_prisma_client .db .litellm_teamtable .update .call_args
1105
+ update_data = call_args [1 ]["data" ] # data parameter from the update call
1106
+
1107
+ # Verify team_member_budget is NOT in the update data
1108
+ assert (
1109
+ "team_member_budget" not in update_data
1110
+ ), f"team_member_budget should not be in update data, but found: { update_data } "
1111
+
1112
+ # Verify other fields are present (team_alias should be there)
1113
+ assert "team_alias" in update_data or "team_id" in str (
1114
+ call_args
1115
+ ), "Expected team update fields should be present"
1116
+
1117
+ # Reset mock for second test
1118
+ mock_prisma_client .db .litellm_teamtable .update .reset_mock ()
1119
+
1120
+ # Test Case 2: team_member_budget is None
1121
+ update_request_without_budget = UpdateTeamRequest (
1122
+ team_id = "test_team_id" ,
1123
+ team_member_budget = None ,
1124
+ team_alias = "updated_alias_2" ,
1125
+ )
1126
+
1127
+ result = await update_team (
1128
+ data = update_request_without_budget ,
1129
+ http_request = mock_request ,
1130
+ user_api_key_dict = mock_user_api_key_dict ,
1131
+ )
1132
+
1133
+ # Verify update was called again
1134
+ assert mock_prisma_client .db .litellm_teamtable .update .called
1135
+
1136
+ # Get the call arguments for second call
1137
+ call_args = mock_prisma_client .db .litellm_teamtable .update .call_args
1138
+ update_data = call_args [1 ]["data" ] # data parameter from the update call
1139
+
1140
+ # Verify team_member_budget is NOT in the update data
1141
+ assert (
1142
+ "team_member_budget" not in update_data
1143
+ ), f"team_member_budget should not be in update data, but found: { update_data } "
1144
+
1145
+ # Test Case 3: No team_member_budget field at all (excluded from request)
1146
+ mock_prisma_client .db .litellm_teamtable .update .reset_mock ()
1147
+
1148
+ update_request_no_budget_field = UpdateTeamRequest (
1149
+ team_id = "test_team_id" ,
1150
+ team_alias = "updated_alias_3" ,
1151
+ # team_member_budget not specified at all
1152
+ )
1153
+
1154
+ result = await update_team (
1155
+ data = update_request_no_budget_field ,
1156
+ http_request = mock_request ,
1157
+ user_api_key_dict = mock_user_api_key_dict ,
1158
+ )
1159
+
1160
+ # Verify update was called again
1161
+ assert mock_prisma_client .db .litellm_teamtable .update .called
1162
+
1163
+ # Get the call arguments for third call
1164
+ call_args = mock_prisma_client .db .litellm_teamtable .update .call_args
1165
+ update_data = call_args [1 ]["data" ] # data parameter from the update call
1166
+
1167
+ # Verify team_member_budget is NOT in the update data
1168
+ assert (
1169
+ "team_member_budget" not in update_data
1170
+ ), f"team_member_budget should not be in update data, but found: { update_data } "
1171
+
1172
+ print (
1173
+ "✅ All test cases passed: team_member_budget is properly excluded from database update operations"
1174
+ )
0 commit comments