@@ -1793,3 +1793,162 @@ async def test_team_member_delete_cleans_membership(mock_db_client, mock_admin_a
17931793 mock_db_client .db .litellm_teammembership .delete_many .assert_awaited_with (
17941794 where = {"team_id" : test_team_id , "user_id" : test_user_id }
17951795 )
1796+
1797+
1798+ @pytest .mark .asyncio
1799+ async def test_new_team_max_budget_exceeds_user_max_budget ():
1800+ """
1801+ Test that /team/new raises ProxyException when max_budget exceeds user's end_user_max_budget.
1802+
1803+ This validates the budget enforcement logic where non-admin users cannot create teams
1804+ with budgets higher than their personal maximum budget limit.
1805+ """
1806+ from fastapi import Request
1807+
1808+ from litellm .proxy ._types import NewTeamRequest , ProxyException , UserAPIKeyAuth
1809+ from litellm .proxy .management_endpoints .team_endpoints import new_team
1810+
1811+ # Create non-admin user with end_user_max_budget set to 100.0
1812+ non_admin_user = UserAPIKeyAuth (
1813+ user_role = LitellmUserRoles .INTERNAL_USER ,
1814+ user_id = "non-admin-user-123" ,
1815+ end_user_max_budget = 100.0 ,
1816+ )
1817+
1818+ # Create team request with max_budget (200.0) exceeding user's limit (100.0)
1819+ team_request = NewTeamRequest (
1820+ team_alias = "high-budget-team" ,
1821+ max_budget = 200.0 , # Exceeds user's end_user_max_budget
1822+ )
1823+
1824+ dummy_request = MagicMock (spec = Request )
1825+
1826+ with patch ("litellm.proxy.proxy_server.prisma_client" ) as mock_prisma , patch (
1827+ "litellm.proxy.proxy_server._license_check"
1828+ ) as mock_license , patch (
1829+ "litellm.proxy.proxy_server.user_api_key_cache"
1830+ ) as mock_cache , patch (
1831+ "litellm.proxy.proxy_server.litellm_proxy_admin_name" , "admin"
1832+ ), patch (
1833+ "litellm.proxy.proxy_server.create_audit_log_for_update"
1834+ ) as mock_audit :
1835+ # Setup basic mocks
1836+ mock_prisma .db .litellm_teamtable .count = AsyncMock (return_value = 0 )
1837+ mock_license .is_team_count_over_limit .return_value = False
1838+ mock_prisma .get_data = AsyncMock (return_value = None )
1839+ mock_audit .return_value = AsyncMock (return_value = MagicMock ())
1840+
1841+ # The budget validation happens BEFORE trying to add members,
1842+ # so we shouldn't need to mock the user table operations
1843+
1844+ # Should raise ProxyException (HTTPException gets converted by handle_exception_on_proxy)
1845+ with pytest .raises (ProxyException ) as exc_info :
1846+ await new_team (
1847+ data = team_request ,
1848+ http_request = dummy_request ,
1849+ user_api_key_dict = non_admin_user ,
1850+ )
1851+
1852+ # Verify exception details
1853+ # ProxyException stores status_code in 'code' attribute
1854+ assert exc_info .value .code == '400'
1855+ assert "max budget higher than user max" in str (exc_info .value .message )
1856+ assert "100.0" in str (exc_info .value .message ) # User's max budget should be mentioned
1857+ assert LitellmUserRoles .INTERNAL_USER .value in str (exc_info .value .message )
1858+
1859+
1860+ @pytest .mark .asyncio
1861+ async def test_new_team_max_budget_within_user_limit ():
1862+ """
1863+ Test that /team/new succeeds when max_budget is within user's end_user_max_budget.
1864+
1865+ This ensures that users can create teams with budgets at or below their personal limit.
1866+ """
1867+ from fastapi import Request
1868+
1869+ from litellm .proxy ._types import NewTeamRequest , UserAPIKeyAuth
1870+ from litellm .proxy .management_endpoints .team_endpoints import new_team
1871+
1872+ # Create non-admin user with end_user_max_budget set to 100.0
1873+ non_admin_user = UserAPIKeyAuth (
1874+ user_role = LitellmUserRoles .INTERNAL_USER ,
1875+ user_id = "non-admin-user-456" ,
1876+ end_user_max_budget = 100.0 ,
1877+ models = [], # Empty models list to bypass model validation
1878+ )
1879+
1880+ # Create team request with max_budget (50.0) within user's limit (100.0)
1881+ team_request = NewTeamRequest (
1882+ team_alias = "within-budget-team" ,
1883+ max_budget = 50.0 , # Within user's end_user_max_budget
1884+ )
1885+
1886+ dummy_request = MagicMock (spec = Request )
1887+
1888+ with patch ("litellm.proxy.proxy_server.prisma_client" ) as mock_prisma , patch (
1889+ "litellm.proxy.proxy_server.user_api_key_cache"
1890+ ) as mock_cache , patch (
1891+ "litellm.proxy.proxy_server._license_check"
1892+ ) as mock_license , patch (
1893+ "litellm.proxy.proxy_server.litellm_proxy_admin_name" , "admin"
1894+ ), patch (
1895+ "litellm.proxy.proxy_server.create_audit_log_for_update"
1896+ ) as mock_audit :
1897+
1898+ # Setup mocks
1899+ mock_prisma .db .litellm_teamtable .count = AsyncMock (return_value = 0 )
1900+ mock_license .is_team_count_over_limit .return_value = False
1901+ mock_prisma .jsonify_team_object = lambda db_data : db_data
1902+ mock_prisma .get_data = AsyncMock (return_value = None )
1903+ mock_prisma .update_data = AsyncMock ()
1904+ mock_audit .return_value = AsyncMock (return_value = MagicMock ())
1905+
1906+ # Mock team creation
1907+ mock_created_team = MagicMock ()
1908+ mock_created_team .team_id = "team-within-budget-789"
1909+ mock_created_team .team_alias = "within-budget-team"
1910+ mock_created_team .max_budget = 50.0
1911+ mock_created_team .members_with_roles = []
1912+ mock_created_team .metadata = None
1913+ mock_created_team .model_dump .return_value = {
1914+ "team_id" : "team-within-budget-789" ,
1915+ "team_alias" : "within-budget-team" ,
1916+ "max_budget" : 50.0 ,
1917+ "members_with_roles" : [],
1918+ }
1919+ mock_prisma .db .litellm_teamtable .create = AsyncMock (return_value = mock_created_team )
1920+ mock_prisma .db .litellm_teamtable .update = AsyncMock (return_value = mock_created_team )
1921+
1922+ # Mock model table
1923+ mock_prisma .db .litellm_modeltable = MagicMock ()
1924+ mock_prisma .db .litellm_modeltable .create = AsyncMock (return_value = MagicMock (id = "model123" ))
1925+
1926+ # Mock user table operations for adding the creator as a member
1927+ mock_user = MagicMock ()
1928+ mock_user .user_id = "non-admin-user-456"
1929+ mock_user .model_dump .return_value = {"user_id" : "non-admin-user-456" , "teams" : ["team-within-budget-789" ]}
1930+ mock_prisma .db .litellm_usertable = MagicMock ()
1931+ mock_prisma .db .litellm_usertable .upsert = AsyncMock (return_value = mock_user )
1932+ mock_prisma .db .litellm_usertable .update = AsyncMock (return_value = mock_user )
1933+
1934+ # Mock team membership table
1935+ mock_membership = MagicMock ()
1936+ mock_membership .model_dump .return_value = {
1937+ "team_id" : "team-within-budget-789" ,
1938+ "user_id" : "non-admin-user-456" ,
1939+ "budget_id" : None ,
1940+ }
1941+ mock_prisma .db .litellm_teammembership = MagicMock ()
1942+ mock_prisma .db .litellm_teammembership .create = AsyncMock (return_value = mock_membership )
1943+
1944+ # Should NOT raise an exception
1945+ result = await new_team (
1946+ data = team_request ,
1947+ http_request = dummy_request ,
1948+ user_api_key_dict = non_admin_user ,
1949+ )
1950+
1951+ # Verify the team was created successfully
1952+ assert result is not None
1953+ assert result ["team_id" ] == "team-within-budget-789"
1954+ assert result ["max_budget" ] == 50.0
0 commit comments