Added LiteLLM to the stack
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.proxy._types import LiteLLM_UserTable
|
||||
from litellm.proxy.management_endpoints.scim.scim_v2 import patch_user
|
||||
from litellm.types.proxy.management_endpoints.scim_v2 import (
|
||||
SCIMPatchOp,
|
||||
SCIMPatchOperation,
|
||||
SCIMUser,
|
||||
SCIMUserEmail,
|
||||
SCIMUserName,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_updates_fields():
|
||||
mock_user = LiteLLM_UserTable(
|
||||
user_id="user-1",
|
||||
user_email="test@example.com",
|
||||
user_alias="Old",
|
||||
teams=[],
|
||||
metadata={},
|
||||
)
|
||||
|
||||
# Create a proper copy to track updates
|
||||
updated_user = LiteLLM_UserTable(
|
||||
user_id="user-1",
|
||||
user_email="test@example.com",
|
||||
user_alias="New Name",
|
||||
teams=[],
|
||||
metadata={"scim_active": False, "scim_metadata": {}},
|
||||
)
|
||||
|
||||
async def mock_update(*, where, data):
|
||||
# Return the updated user object
|
||||
return updated_user
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_client.db = mock_db
|
||||
mock_db.litellm_usertable.find_unique = AsyncMock(return_value=mock_user)
|
||||
mock_db.litellm_usertable.update = AsyncMock(side_effect=mock_update)
|
||||
mock_db.litellm_teamtable.find_unique = AsyncMock(return_value=None)
|
||||
|
||||
# Mock the transformation function to return a proper SCIMUser
|
||||
mock_scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id="user-1",
|
||||
userName="user-1",
|
||||
displayName="New Name",
|
||||
name=SCIMUserName(familyName="Name", givenName="New"),
|
||||
emails=[SCIMUserEmail(value="test@example.com")],
|
||||
active=False,
|
||||
)
|
||||
|
||||
patch_ops = SCIMPatchOp(
|
||||
Operations=[
|
||||
SCIMPatchOperation(op="replace", path="displayName", value="New Name"),
|
||||
SCIMPatchOperation(op="replace", path="active", value="False"),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_client), \
|
||||
patch("litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=mock_scim_user)):
|
||||
result = await patch_user(user_id="user-1", patch_ops=patch_ops)
|
||||
|
||||
mock_db.litellm_usertable.update.assert_called_once()
|
||||
assert result.displayName == "New Name"
|
||||
assert result.active is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_manages_group_memberships():
|
||||
mock_user = LiteLLM_UserTable(
|
||||
user_id="user-2",
|
||||
user_email="test@example.com",
|
||||
user_alias="Old",
|
||||
teams=["old-team"],
|
||||
metadata={},
|
||||
)
|
||||
|
||||
# Create updated user with final teams
|
||||
updated_user = LiteLLM_UserTable(
|
||||
user_id="user-2",
|
||||
user_email="test@example.com",
|
||||
user_alias="Old",
|
||||
teams=["new-team"],
|
||||
metadata={"scim_metadata": {}},
|
||||
)
|
||||
|
||||
async def mock_update(*, where, data):
|
||||
# Return the updated user
|
||||
return updated_user
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_client.db = mock_db
|
||||
mock_db.litellm_usertable.find_unique = AsyncMock(return_value=mock_user)
|
||||
mock_db.litellm_usertable.update = AsyncMock(side_effect=mock_update)
|
||||
mock_db.litellm_teamtable.find_unique = AsyncMock(return_value=None)
|
||||
|
||||
# Mock the transformation function to return a proper SCIMUser
|
||||
mock_scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id="user-2",
|
||||
userName="user-2",
|
||||
displayName="Old",
|
||||
name=SCIMUserName(familyName="Family", givenName="Old"),
|
||||
emails=[SCIMUserEmail(value="test@example.com")],
|
||||
active=True,
|
||||
)
|
||||
|
||||
async def mock_add(data, user_api_key_dict):
|
||||
# Mock team member add
|
||||
pass
|
||||
|
||||
async def mock_delete(data, user_api_key_dict):
|
||||
# Mock team member delete
|
||||
pass
|
||||
|
||||
patch_ops = SCIMPatchOp(
|
||||
Operations=[
|
||||
SCIMPatchOperation(op="add", path="groups", value=[{"value": "new-team"}]),
|
||||
SCIMPatchOperation(op="remove", path="groups", value=[{"value": "old-team"}]),
|
||||
]
|
||||
)
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_client), \
|
||||
patch("litellm.proxy.management_endpoints.scim.scim_v2.team_member_add",
|
||||
AsyncMock(side_effect=mock_add)) as mock_add_fn, \
|
||||
patch("litellm.proxy.management_endpoints.scim.scim_v2.team_member_delete",
|
||||
AsyncMock(side_effect=mock_delete)) as mock_del_fn, \
|
||||
patch("litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=mock_scim_user)):
|
||||
result = await patch_user(user_id="user-2", patch_ops=patch_ops)
|
||||
|
||||
assert mock_add_fn.called
|
||||
assert mock_del_fn.called
|
||||
# Check that the database update was called with the correct teams
|
||||
call_args = mock_db.litellm_usertable.update.call_args
|
||||
assert "new-team" in call_args[1]["data"]["teams"]
|
||||
assert result == mock_scim_user
|
||||
|
@@ -0,0 +1,285 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Optional, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.proxy._types import LiteLLM_TeamTable, LiteLLM_UserTable, Member
|
||||
from litellm.proxy.management_endpoints.scim.scim_transformations import (
|
||||
ScimTransformations,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.scim_v2 import (
|
||||
SCIMGroup,
|
||||
SCIMPatchOp,
|
||||
SCIMPatchOperation,
|
||||
SCIMUser,
|
||||
)
|
||||
|
||||
|
||||
# Mock data
|
||||
@pytest.fixture
|
||||
def mock_user():
|
||||
return LiteLLM_UserTable(
|
||||
user_id="user-123",
|
||||
user_email="test@example.com",
|
||||
user_alias="Test User",
|
||||
teams=["team-1", "team-2"],
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_with_scim_metadata():
|
||||
return LiteLLM_UserTable(
|
||||
user_id="user-456",
|
||||
user_email="test2@example.com",
|
||||
user_alias="Test User 2",
|
||||
teams=["team-1"],
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
metadata={"scim_metadata": {"givenName": "Test", "familyName": "User"}},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_user_minimal():
|
||||
return LiteLLM_UserTable(
|
||||
user_id="user-789",
|
||||
user_email=None,
|
||||
user_alias=None,
|
||||
teams=[],
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_team():
|
||||
return LiteLLM_TeamTable(
|
||||
team_id="team-1",
|
||||
team_alias="Test Team",
|
||||
members_with_roles=[
|
||||
Member(user_id="user-123", user_email="test@example.com", role="admin"),
|
||||
Member(user_id="user-456", user_email="test2@example.com", role="user"),
|
||||
],
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_team_minimal():
|
||||
return LiteLLM_TeamTable(
|
||||
team_id="team-2",
|
||||
team_alias="Test Team 2",
|
||||
members_with_roles=[Member(user_id="user-789", user_email=None, role="user")],
|
||||
created_at=None,
|
||||
updated_at=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_prisma_client():
|
||||
mock_client = MagicMock()
|
||||
mock_db = MagicMock()
|
||||
mock_client.db = mock_db
|
||||
|
||||
mock_find_unique = AsyncMock()
|
||||
mock_db.litellm_teamtable.find_unique = mock_find_unique
|
||||
|
||||
return mock_client, mock_find_unique
|
||||
|
||||
|
||||
class TestScimTransformations:
|
||||
@pytest.mark.asyncio
|
||||
async def test_transform_litellm_user_to_scim_user(
|
||||
self, mock_user, mock_prisma_client
|
||||
):
|
||||
mock_client, mock_find_unique = mock_prisma_client
|
||||
|
||||
# Mock the team lookup
|
||||
team1 = LiteLLM_TeamTable(
|
||||
team_id="team-1", team_alias="Team One", members_with_roles=[]
|
||||
)
|
||||
team2 = LiteLLM_TeamTable(
|
||||
team_id="team-2", team_alias="Team Two", members_with_roles=[]
|
||||
)
|
||||
|
||||
mock_find_unique.side_effect = [team1, team2]
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_client):
|
||||
scim_user = await ScimTransformations.transform_litellm_user_to_scim_user(
|
||||
mock_user
|
||||
)
|
||||
|
||||
assert scim_user.id == mock_user.user_id
|
||||
assert scim_user.userName == mock_user.user_email
|
||||
assert scim_user.displayName == mock_user.user_email
|
||||
assert scim_user.name.familyName == mock_user.user_alias
|
||||
assert scim_user.name.givenName == mock_user.user_alias
|
||||
assert len(scim_user.emails) == 1
|
||||
assert scim_user.emails[0].value == mock_user.user_email
|
||||
assert len(scim_user.groups) == 2
|
||||
assert scim_user.groups[0].value == "team-1"
|
||||
assert scim_user.groups[0].display == "Team One"
|
||||
assert scim_user.groups[1].value == "team-2"
|
||||
assert scim_user.groups[1].display == "Team Two"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transform_user_with_scim_metadata(
|
||||
self, mock_user_with_scim_metadata, mock_prisma_client
|
||||
):
|
||||
mock_client, mock_find_unique = mock_prisma_client
|
||||
|
||||
# Mock the team lookup
|
||||
team1 = LiteLLM_TeamTable(
|
||||
team_id="team-1", team_alias="Team One", members_with_roles=[]
|
||||
)
|
||||
mock_find_unique.return_value = team1
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_client):
|
||||
scim_user = await ScimTransformations.transform_litellm_user_to_scim_user(
|
||||
mock_user_with_scim_metadata
|
||||
)
|
||||
|
||||
assert scim_user.name.givenName == "Test"
|
||||
assert scim_user.name.familyName == "User"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transform_litellm_team_to_scim_group(
|
||||
self, mock_team, mock_prisma_client
|
||||
):
|
||||
mock_client, _ = mock_prisma_client
|
||||
|
||||
with patch("litellm.proxy.proxy_server.prisma_client", mock_client):
|
||||
scim_group = await ScimTransformations.transform_litellm_team_to_scim_group(
|
||||
mock_team
|
||||
)
|
||||
|
||||
assert scim_group.id == mock_team.team_id
|
||||
assert scim_group.displayName == mock_team.team_alias
|
||||
assert len(scim_group.members) == 2
|
||||
assert scim_group.members[0].value == "test@example.com"
|
||||
assert scim_group.members[0].display == "test@example.com"
|
||||
assert scim_group.members[1].value == "test2@example.com"
|
||||
assert scim_group.members[1].display == "test2@example.com"
|
||||
|
||||
def test_get_scim_user_name(self, mock_user, mock_user_minimal):
|
||||
# User with email
|
||||
result = ScimTransformations._get_scim_user_name(mock_user)
|
||||
assert result == mock_user.user_email
|
||||
|
||||
# User without email
|
||||
result = ScimTransformations._get_scim_user_name(mock_user_minimal)
|
||||
assert result == ScimTransformations.DEFAULT_SCIM_DISPLAY_NAME
|
||||
|
||||
def test_get_scim_family_name(
|
||||
self, mock_user, mock_user_with_scim_metadata, mock_user_minimal
|
||||
):
|
||||
# User with alias
|
||||
result = ScimTransformations._get_scim_family_name(mock_user)
|
||||
assert result == mock_user.user_alias
|
||||
|
||||
# User with SCIM metadata
|
||||
result = ScimTransformations._get_scim_family_name(mock_user_with_scim_metadata)
|
||||
assert result == "User"
|
||||
|
||||
# User without alias or metadata
|
||||
result = ScimTransformations._get_scim_family_name(mock_user_minimal)
|
||||
assert result == ScimTransformations.DEFAULT_SCIM_FAMILY_NAME
|
||||
|
||||
def test_get_scim_given_name(
|
||||
self, mock_user, mock_user_with_scim_metadata, mock_user_minimal
|
||||
):
|
||||
# User with alias
|
||||
result = ScimTransformations._get_scim_given_name(mock_user)
|
||||
assert result == mock_user.user_alias
|
||||
|
||||
# User with SCIM metadata
|
||||
result = ScimTransformations._get_scim_given_name(mock_user_with_scim_metadata)
|
||||
assert result == "Test"
|
||||
|
||||
# User without alias or metadata
|
||||
result = ScimTransformations._get_scim_given_name(mock_user_minimal)
|
||||
assert result == ScimTransformations.DEFAULT_SCIM_NAME
|
||||
|
||||
def test_get_scim_member_value(self):
|
||||
# Member with email
|
||||
member_with_email = Member(
|
||||
user_id="user-123", user_email="test@example.com", role="admin"
|
||||
)
|
||||
result = ScimTransformations._get_scim_member_value(member_with_email)
|
||||
assert result == member_with_email.user_email
|
||||
|
||||
# Member without email
|
||||
member_without_email = Member(user_id="user-456", user_email=None, role="user")
|
||||
result = ScimTransformations._get_scim_member_value(member_without_email)
|
||||
assert result == ScimTransformations.DEFAULT_SCIM_MEMBER_VALUE
|
||||
|
||||
|
||||
class TestSCIMPatchOperations:
|
||||
"""Test SCIM PATCH operation validation and case-insensitive handling"""
|
||||
|
||||
def test_scim_patch_operation_lowercase(self):
|
||||
"""Test that lowercase operations are accepted"""
|
||||
op = SCIMPatchOperation(op="add", path="members", value=[{"value": "user123"}])
|
||||
assert op.op == "add"
|
||||
|
||||
op = SCIMPatchOperation(op="remove", path='members[value eq "user123"]')
|
||||
assert op.op == "remove"
|
||||
|
||||
op = SCIMPatchOperation(op="replace", path="displayName", value="New Name")
|
||||
assert op.op == "replace"
|
||||
|
||||
def test_scim_patch_operation_uppercase(self):
|
||||
"""Test that uppercase operations are normalized to lowercase"""
|
||||
op = SCIMPatchOperation(op="ADD", path="members", value=[{"value": "user123"}])
|
||||
assert op.op == "add"
|
||||
|
||||
op = SCIMPatchOperation(op="REMOVE", path='members[value eq "user123"]')
|
||||
assert op.op == "remove"
|
||||
|
||||
op = SCIMPatchOperation(op="REPLACE", path="displayName", value="New Name")
|
||||
assert op.op == "replace"
|
||||
|
||||
def test_scim_patch_operation_mixed_case(self):
|
||||
"""Test that mixed case operations are normalized to lowercase"""
|
||||
op = SCIMPatchOperation(op="Add", path="members", value=[{"value": "user123"}])
|
||||
assert op.op == "add"
|
||||
|
||||
op = SCIMPatchOperation(op="Remove", path='members[value eq "user123"]')
|
||||
assert op.op == "remove"
|
||||
|
||||
op = SCIMPatchOperation(op="Replace", path="displayName", value="New Name")
|
||||
assert op.op == "replace"
|
||||
|
||||
def test_scim_patch_operation_with_optional_fields(self):
|
||||
"""Test SCIMPatchOperation with and without optional fields"""
|
||||
# Operation with all fields
|
||||
op_full = SCIMPatchOperation(
|
||||
op="Add",
|
||||
path="members",
|
||||
value=[{"value": "user123", "display": "User 123"}],
|
||||
)
|
||||
assert op_full.op == "add"
|
||||
assert op_full.path == "members"
|
||||
assert op_full.value == [{"value": "user123", "display": "User 123"}]
|
||||
|
||||
# Operation with minimal fields (only op is required)
|
||||
op_minimal = SCIMPatchOperation(op="Remove")
|
||||
assert op_minimal.op == "remove"
|
||||
assert op_minimal.path is None
|
||||
assert op_minimal.value is None
|
@@ -0,0 +1,681 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from litellm.proxy._types import LitellmUserRoles, NewUserRequest, ProxyException
|
||||
from litellm.proxy.management_endpoints.scim.scim_v2 import (
|
||||
UserProvisionerHelpers,
|
||||
_handle_team_membership_changes,
|
||||
create_user,
|
||||
get_service_provider_config,
|
||||
patch_user,
|
||||
update_user,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.scim_v2 import (
|
||||
SCIMFeature,
|
||||
SCIMPatchOp,
|
||||
SCIMPatchOperation,
|
||||
SCIMServiceProviderConfig,
|
||||
SCIMUser,
|
||||
SCIMUserEmail,
|
||||
SCIMUserGroup,
|
||||
SCIMUserName,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_existing_user_conflict(mocker):
|
||||
"""If a user already exists, create_user should raise ScimUserAlreadyExists"""
|
||||
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="existing-user",
|
||||
name=SCIMUserName(familyName="User", givenName="Existing"),
|
||||
emails=[SCIMUserEmail(value="existing@example.com")],
|
||||
)
|
||||
|
||||
# Create a properly structured mock for the prisma client
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value={"user_id": "existing-user"})
|
||||
|
||||
# Mock the _get_prisma_client_or_raise_exception to return our mock
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
mocked_new_user = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.new_user",
|
||||
AsyncMock(),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await create_user(user=scim_user)
|
||||
|
||||
# Check that it's an HTTPException with status 409
|
||||
assert exc_info.value.status_code == 409
|
||||
assert "existing-user" in str(exc_info.value.detail)
|
||||
mocked_new_user.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_defaults_to_viewer(mocker, monkeypatch):
|
||||
"""If no role provided, new user should default to viewer"""
|
||||
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="new-user",
|
||||
name=SCIMUserName(familyName="User", givenName="New"),
|
||||
emails=[SCIMUserEmail(value="new@example.com")],
|
||||
)
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.default_internal_user_params", None, raising=False
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="new-user")),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=scim_user),
|
||||
)
|
||||
|
||||
await create_user(user=scim_user)
|
||||
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_uses_default_internal_user_params_role(mocker, monkeypatch):
|
||||
"""If role is set in default_internal_user_params, new user should use that role"""
|
||||
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="new-user",
|
||||
name=SCIMUserName(familyName="User", givenName="New"),
|
||||
emails=[SCIMUserEmail(value="new@example.com")],
|
||||
)
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None)
|
||||
|
||||
# Set default_internal_user_params with a specific role
|
||||
default_params = {
|
||||
"user_role": LitellmUserRoles.PROXY_ADMIN,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
"litellm.default_internal_user_params", default_params, raising=False
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
new_user_mock = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.new_user",
|
||||
AsyncMock(return_value=NewUserRequest(user_id="new-user")),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=scim_user),
|
||||
)
|
||||
|
||||
await create_user(user=scim_user)
|
||||
|
||||
called_args = new_user_mock.call_args.kwargs["data"]
|
||||
assert called_args.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_existing_user_by_email_no_email(mocker):
|
||||
"""Should return None when new_user_request has no email"""
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
|
||||
new_user_request = NewUserRequest(
|
||||
user_id="test-user",
|
||||
user_email=None, # No email provided
|
||||
user_alias="Test User",
|
||||
teams=[],
|
||||
metadata={},
|
||||
auto_create_key=False,
|
||||
)
|
||||
|
||||
result = await UserProvisionerHelpers.handle_existing_user_by_email(
|
||||
prisma_client=mock_prisma_client,
|
||||
new_user_request=new_user_request
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_existing_user_by_email_no_existing_user(mocker):
|
||||
"""Should return None when no existing user is found with the email"""
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=None)
|
||||
|
||||
new_user_request = NewUserRequest(
|
||||
user_id="test-user",
|
||||
user_email="test@example.com",
|
||||
user_alias="Test User",
|
||||
teams=["team1"],
|
||||
metadata={"key": "value"},
|
||||
auto_create_key=False,
|
||||
)
|
||||
|
||||
result = await UserProvisionerHelpers.handle_existing_user_by_email(
|
||||
prisma_client=mock_prisma_client,
|
||||
new_user_request=new_user_request
|
||||
)
|
||||
|
||||
assert result is None
|
||||
mock_prisma_client.db.litellm_usertable.find_first.assert_called_once_with(
|
||||
where={"user_email": "test@example.com"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_existing_user_by_email_existing_user_updated(mocker):
|
||||
"""Should update existing user and return SCIMUser when user with email exists"""
|
||||
# Mock existing user - create a proper mock object with attributes
|
||||
existing_user = mocker.MagicMock()
|
||||
existing_user.user_id = "old-user-id"
|
||||
existing_user.user_email = "test@example.com"
|
||||
existing_user.user_alias = "Old Name"
|
||||
existing_user.teams = ["old-team"]
|
||||
existing_user.metadata = {"old": "data"}
|
||||
|
||||
# Mock updated user
|
||||
updated_user = {
|
||||
"user_id": "new-user-id",
|
||||
"user_email": "test@example.com",
|
||||
"user_alias": "New Name",
|
||||
"teams": ["new-team"],
|
||||
"metadata": '{"new": "data"}'
|
||||
}
|
||||
|
||||
# Mock SCIM user to be returned
|
||||
mock_scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id="new-user-id",
|
||||
userName="new-user-id",
|
||||
name=SCIMUserName(familyName="Name", givenName="New"),
|
||||
emails=[SCIMUserEmail(value="test@example.com")],
|
||||
)
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.find_first = AsyncMock(return_value=existing_user)
|
||||
mock_prisma_client.db.litellm_usertable.update = AsyncMock(return_value=updated_user)
|
||||
|
||||
# Mock the transformation function
|
||||
mock_transform = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=mock_scim_user)
|
||||
)
|
||||
|
||||
new_user_request = NewUserRequest(
|
||||
user_id="new-user-id",
|
||||
user_email="test@example.com",
|
||||
user_alias="New Name",
|
||||
teams=["new-team"],
|
||||
metadata={"new": "data"},
|
||||
auto_create_key=False,
|
||||
)
|
||||
|
||||
result = await UserProvisionerHelpers.handle_existing_user_by_email(
|
||||
prisma_client=mock_prisma_client,
|
||||
new_user_request=new_user_request
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result == mock_scim_user
|
||||
|
||||
# Verify database operations
|
||||
mock_prisma_client.db.litellm_usertable.find_first.assert_called_once_with(
|
||||
where={"user_email": "test@example.com"}
|
||||
)
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.update.assert_called_once_with(
|
||||
where={"user_id": "old-user-id"},
|
||||
data={
|
||||
"user_id": "new-user-id",
|
||||
"user_email": "test@example.com",
|
||||
"user_alias": "New Name",
|
||||
"teams": ["new-team"],
|
||||
"metadata": '{"new": "data"}',
|
||||
},
|
||||
)
|
||||
|
||||
# Verify transformation was called
|
||||
mock_transform.assert_called_once_with(updated_user)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_team_membership_changes_no_changes(mocker):
|
||||
"""Should not call patch_team_membership when existing teams equal new teams"""
|
||||
mock_patch_team_membership = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.patch_team_membership",
|
||||
AsyncMock()
|
||||
)
|
||||
|
||||
# Same teams - no changes
|
||||
await _handle_team_membership_changes(
|
||||
user_id="test-user",
|
||||
existing_teams=["team1", "team2"],
|
||||
new_teams=["team1", "team2"]
|
||||
)
|
||||
|
||||
# Should not be called since no changes
|
||||
mock_patch_team_membership.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_team_membership_changes_add_teams(mocker):
|
||||
"""Should call patch_team_membership with teams to add"""
|
||||
mock_patch_team_membership = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.patch_team_membership",
|
||||
AsyncMock()
|
||||
)
|
||||
|
||||
# Adding teams
|
||||
await _handle_team_membership_changes(
|
||||
user_id="test-user",
|
||||
existing_teams=["team1"],
|
||||
new_teams=["team1", "team2", "team3"]
|
||||
)
|
||||
|
||||
# Verify the call was made once
|
||||
mock_patch_team_membership.assert_called_once()
|
||||
|
||||
# Check the arguments more flexibly to handle order variations
|
||||
call_args = mock_patch_team_membership.call_args
|
||||
assert call_args[1]["user_id"] == "test-user"
|
||||
assert set(call_args[1]["teams_ids_to_add_user_to"]) == {"team2", "team3"}
|
||||
assert call_args[1]["teams_ids_to_remove_user_from"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_team_membership_changes_remove_teams(mocker):
|
||||
"""Should call patch_team_membership with teams to remove"""
|
||||
mock_patch_team_membership = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.patch_team_membership",
|
||||
AsyncMock()
|
||||
)
|
||||
|
||||
# Removing teams
|
||||
await _handle_team_membership_changes(
|
||||
user_id="test-user",
|
||||
existing_teams=["team1", "team2", "team3"],
|
||||
new_teams=["team1"]
|
||||
)
|
||||
|
||||
# Verify the call was made once
|
||||
mock_patch_team_membership.assert_called_once()
|
||||
|
||||
# Check the arguments more flexibly to handle order variations
|
||||
call_args = mock_patch_team_membership.call_args
|
||||
assert call_args[1]["user_id"] == "test-user"
|
||||
assert call_args[1]["teams_ids_to_add_user_to"] == []
|
||||
assert set(call_args[1]["teams_ids_to_remove_user_from"]) == {"team2", "team3"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_team_membership_changes_add_and_remove(mocker):
|
||||
"""Should call patch_team_membership with both teams to add and remove"""
|
||||
mock_patch_team_membership = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.patch_team_membership",
|
||||
AsyncMock()
|
||||
)
|
||||
|
||||
# Both adding and removing teams
|
||||
await _handle_team_membership_changes(
|
||||
user_id="test-user",
|
||||
existing_teams=["team1", "team2"],
|
||||
new_teams=["team2", "team3"]
|
||||
)
|
||||
|
||||
# Verify the call was made once
|
||||
mock_patch_team_membership.assert_called_once()
|
||||
|
||||
# Check the arguments - team1 should be removed, team3 should be added, team2 stays
|
||||
call_args = mock_patch_team_membership.call_args
|
||||
assert call_args[1]["user_id"] == "test-user"
|
||||
assert call_args[1]["teams_ids_to_add_user_to"] == ["team3"]
|
||||
assert call_args[1]["teams_ids_to_remove_user_from"] == ["team1"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_success(mocker):
|
||||
"""Should successfully update user with PUT request"""
|
||||
# Mock existing user
|
||||
existing_user = mocker.MagicMock()
|
||||
existing_user.teams = ["old-team"]
|
||||
|
||||
# Mock updated user
|
||||
updated_user = {
|
||||
"user_id": "test-user",
|
||||
"user_email": "updated@example.com",
|
||||
"user_alias": "Updated User",
|
||||
"teams": ["new-team"],
|
||||
"metadata": '{"scim_metadata": {"givenName": "Updated", "familyName": "User"}}'
|
||||
}
|
||||
|
||||
# Mock SCIM user for request
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="test-user",
|
||||
name=SCIMUserName(familyName="User", givenName="Updated"),
|
||||
emails=[SCIMUserEmail(value="updated@example.com")],
|
||||
groups=[SCIMUserGroup(value="new-team")]
|
||||
)
|
||||
|
||||
# Mock SCIM user for response
|
||||
response_scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id="test-user",
|
||||
userName="test-user",
|
||||
name=SCIMUserName(familyName="User", givenName="Updated"),
|
||||
emails=[SCIMUserEmail(value="updated@example.com")],
|
||||
)
|
||||
|
||||
# Mock prisma client
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.update = AsyncMock(return_value=updated_user)
|
||||
|
||||
# Mock dependencies
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client)
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._check_user_exists",
|
||||
AsyncMock(return_value=existing_user)
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._handle_team_membership_changes",
|
||||
AsyncMock()
|
||||
)
|
||||
mock_transform = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=response_scim_user)
|
||||
)
|
||||
|
||||
# Call update_user
|
||||
result = await update_user(user_id="test-user", user=scim_user)
|
||||
|
||||
# Verify result
|
||||
assert result == response_scim_user
|
||||
|
||||
# Verify database update was called with correct data
|
||||
mock_prisma_client.db.litellm_usertable.update.assert_called_once()
|
||||
call_args = mock_prisma_client.db.litellm_usertable.update.call_args
|
||||
assert call_args[1]["where"] == {"user_id": "test-user"}
|
||||
assert call_args[1]["data"]["user_email"] == "updated@example.com"
|
||||
assert call_args[1]["data"]["teams"] == ["new-team"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_user_not_found(mocker):
|
||||
"""Should raise 404 when user doesn't exist"""
|
||||
scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
userName="nonexistent-user",
|
||||
name=SCIMUserName(familyName="User", givenName="Test"),
|
||||
emails=[SCIMUserEmail(value="test@example.com")],
|
||||
)
|
||||
|
||||
# Mock dependencies to raise HTTPException for user not found
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mocker.MagicMock())
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._check_user_exists",
|
||||
AsyncMock(side_effect=HTTPException(status_code=404, detail={"error": "User not found"}))
|
||||
)
|
||||
|
||||
# Should raise ProxyException (which wraps the HTTPException)
|
||||
with pytest.raises(ProxyException):
|
||||
await update_user(user_id="nonexistent-user", user=scim_user)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_success(mocker):
|
||||
"""Should successfully patch user with PATCH request"""
|
||||
# Mock existing user
|
||||
existing_user = mocker.MagicMock()
|
||||
existing_user.teams = ["team1"]
|
||||
existing_user.metadata = {}
|
||||
|
||||
# Mock updated user
|
||||
updated_user = {
|
||||
"user_id": "test-user",
|
||||
"user_alias": "Patched User",
|
||||
"teams": ["team1", "team2"],
|
||||
"metadata": '{"scim_metadata": {}}'
|
||||
}
|
||||
|
||||
# Mock patch operations
|
||||
patch_ops = SCIMPatchOp(
|
||||
schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
Operations=[
|
||||
SCIMPatchOperation(op="replace", path="displayName", value="Patched User"),
|
||||
SCIMPatchOperation(op="add", path="groups", value=[{"value": "team2"}])
|
||||
]
|
||||
)
|
||||
|
||||
# Mock response SCIM user
|
||||
response_scim_user = SCIMUser(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id="test-user",
|
||||
userName="test-user",
|
||||
name=SCIMUserName(familyName="User", givenName="Patched"),
|
||||
)
|
||||
|
||||
# Mock prisma client
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable.update = AsyncMock(return_value=updated_user)
|
||||
|
||||
# Mock dependencies
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client)
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._check_user_exists",
|
||||
AsyncMock(return_value=existing_user)
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._handle_team_membership_changes",
|
||||
AsyncMock()
|
||||
)
|
||||
mock_transform = mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_user_to_scim_user",
|
||||
AsyncMock(return_value=response_scim_user)
|
||||
)
|
||||
|
||||
# Call patch_user
|
||||
result = await patch_user(user_id="test-user", patch_ops=patch_ops)
|
||||
|
||||
# Verify result
|
||||
assert result == response_scim_user
|
||||
|
||||
# Verify database update was called
|
||||
mock_prisma_client.db.litellm_usertable.update.assert_called_once()
|
||||
call_args = mock_prisma_client.db.litellm_usertable.update.call_args
|
||||
assert call_args[1]["where"] == {"user_id": "test-user"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_user_not_found(mocker):
|
||||
"""Should raise 404 when user doesn't exist for patch"""
|
||||
patch_ops = SCIMPatchOp(
|
||||
schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
Operations=[
|
||||
SCIMPatchOperation(op="replace", path="displayName", value="New Name")
|
||||
]
|
||||
)
|
||||
|
||||
# Mock dependencies to raise HTTPException for user not found
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mocker.MagicMock())
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._check_user_exists",
|
||||
AsyncMock(side_effect=HTTPException(status_code=404, detail={"error": "User not found"}))
|
||||
)
|
||||
|
||||
# Should raise ProxyException (which wraps the HTTPException)
|
||||
with pytest.raises(ProxyException):
|
||||
await patch_user(user_id="nonexistent-user", patch_ops=patch_ops)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_service_provider_config(mocker):
|
||||
"""Test the get_service_provider_config endpoint"""
|
||||
# Mock the Request object
|
||||
mock_request = mocker.MagicMock()
|
||||
mock_request.url = "https://example.com/scim/v2/ServiceProviderConfig"
|
||||
|
||||
# Call the endpoint
|
||||
result = await get_service_provider_config(mock_request)
|
||||
|
||||
# Verify it returns the correct response
|
||||
assert isinstance(result, SCIMServiceProviderConfig)
|
||||
assert result.schemas == ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"]
|
||||
assert result.patch.supported is True
|
||||
assert result.bulk.supported is False
|
||||
assert result.meta is not None
|
||||
assert result.meta["resourceType"] == "ServiceProviderConfig"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_group_metadata_serialization_issue(mocker):
|
||||
"""
|
||||
Test that update_group properly serializes metadata to avoid Prisma DataError.
|
||||
|
||||
This test reproduces the issue where metadata was passed as a dict instead of
|
||||
a JSON string, causing: "Invalid argument type. `metadata` should be of any
|
||||
of the following types: `JsonNullValueInput`, `Json`"
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.scim.scim_v2 import update_group
|
||||
from litellm.types.proxy.management_endpoints.scim_v2 import SCIMGroup, SCIMMember
|
||||
|
||||
# Create test data
|
||||
group_id = "test-group-id"
|
||||
scim_group = SCIMGroup(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
id=group_id,
|
||||
displayName="Test Group",
|
||||
members=[SCIMMember(value="user1", display="User One")]
|
||||
)
|
||||
|
||||
# Mock existing team with metadata
|
||||
mock_existing_team = mocker.MagicMock()
|
||||
mock_existing_team.team_id = group_id
|
||||
mock_existing_team.team_alias = "Old Group Name"
|
||||
mock_existing_team.members = ["user1"]
|
||||
mock_existing_team.metadata = {"existing_key": "existing_value"}
|
||||
mock_existing_team.created_at = None
|
||||
mock_existing_team.updated_at = None
|
||||
|
||||
# Mock updated team response
|
||||
mock_updated_team = mocker.MagicMock()
|
||||
mock_updated_team.team_id = group_id
|
||||
mock_updated_team.team_alias = "Test Group"
|
||||
mock_updated_team.members = ["user1"]
|
||||
mock_updated_team.created_at = None
|
||||
mock_updated_team.updated_at = None
|
||||
|
||||
# Create a properly structured mock for the prisma client
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
mock_prisma_client.db = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_teamtable = mocker.MagicMock()
|
||||
mock_prisma_client.db.litellm_usertable = mocker.MagicMock()
|
||||
|
||||
# Mock team operations
|
||||
mock_prisma_client.db.litellm_teamtable.find_unique = AsyncMock(return_value=mock_existing_team)
|
||||
mock_prisma_client.db.litellm_teamtable.update = AsyncMock(return_value=mock_updated_team)
|
||||
|
||||
# Mock user operations
|
||||
mock_user = mocker.MagicMock()
|
||||
mock_user.user_id = "user1"
|
||||
mock_user.user_email = "user1@example.com" # Add proper string value for user_email
|
||||
mock_user.teams = [group_id]
|
||||
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=mock_user)
|
||||
mock_prisma_client.db.litellm_usertable.update = AsyncMock(return_value=mock_user)
|
||||
|
||||
# Mock the _get_prisma_client_or_raise_exception to return our mock
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||
AsyncMock(return_value=mock_prisma_client),
|
||||
)
|
||||
|
||||
# Mock the transformation function
|
||||
mock_scim_group_response = SCIMGroup(
|
||||
schemas=["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
id=group_id,
|
||||
displayName="Test Group",
|
||||
members=[SCIMMember(value="user1", display="User One")]
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.scim.scim_v2.ScimTransformations.transform_litellm_team_to_scim_group",
|
||||
AsyncMock(return_value=mock_scim_group_response),
|
||||
)
|
||||
|
||||
# Call the function that had the bug
|
||||
result = await update_group(group_id=group_id, group=scim_group)
|
||||
|
||||
# Verify the team update was called
|
||||
mock_prisma_client.db.litellm_teamtable.update.assert_called_once()
|
||||
|
||||
# Get the call arguments to verify metadata serialization
|
||||
call_args = mock_prisma_client.db.litellm_teamtable.update.call_args
|
||||
update_data = call_args[1]["data"]
|
||||
|
||||
# Verify that metadata is properly serialized as a string, not a dict
|
||||
# This is the critical check that would have caught the original bug
|
||||
assert "metadata" in update_data
|
||||
metadata = update_data["metadata"]
|
||||
|
||||
# The fix should ensure metadata is serialized as a JSON string
|
||||
assert isinstance(metadata, str), f"metadata should be a JSON string, but got {type(metadata)}"
|
||||
|
||||
# Verify we can parse it back to verify it contains the expected data
|
||||
import json
|
||||
parsed_metadata = json.loads(metadata)
|
||||
assert "existing_key" in parsed_metadata
|
||||
assert "scim_data" in parsed_metadata
|
||||
assert parsed_metadata["existing_key"] == "existing_value"
|
Reference in New Issue
Block a user