From 431ddbdd2200cf9cd3f99fb5c2839ea944b4c34a Mon Sep 17 00:00:00 2001 From: yucheng Date: Sat, 19 Sep 2026 23:17:18 +0000 Subject: [PATCH] test(team): exercise the member-add audit helper directly and drop its dead user_id None guard Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../management_endpoints/team_endpoints.py | 2 +- .../test_team_endpoints.py | 83 +++++++++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 222cf8e299f..6eaed62013e 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3243,7 +3243,7 @@ def _schedule_team_member_add_audit_logs( return for user in updated_users: - if user.user_id is None or user.user_id in existing_user_ids: + if user.user_id in existing_user_ids: continue asyncio.create_task( create_object_audit_log( diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index fe461eca4a1..dc72a6a2256 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -13737,12 +13737,16 @@ class _UntouchableRoster(Sequence[Member]): raise AssertionError("the roster was read while audit logging is off") -def test_membership_audit_scheduling_skips_the_roster_entirely_when_audit_logging_is_off(monkeypatch): - """Regression: the before/after rosters were serialized on every membership change, even - when audit logs are not stored.""" - from litellm.proxy.management_endpoints.team_endpoints import _schedule_team_membership_audit_log +class _UntouchableUsers(Sequence[LiteLLM_UserTable]): + def __getitem__(self, index): + raise AssertionError("the created users were read while audit logging is off") - monkeypatch.setattr("litellm.store_audit_logs", False) + def __len__(self) -> int: + raise AssertionError("the created users were read while audit logging is off") + + +def _schedule_membership_audit_with_untouchable_roster() -> None: + from litellm.proxy.management_endpoints.team_endpoints import _schedule_team_membership_audit_log _schedule_team_membership_audit_log( team_id="team-quiet", @@ -13754,6 +13758,75 @@ def test_membership_audit_scheduling_skips_the_roster_entirely_when_audit_loggin ) +def _schedule_member_add_audit_with_untouchable_roster() -> None: + from litellm.proxy.management_endpoints.team_endpoints import _schedule_team_member_add_audit_logs + + _schedule_team_member_add_audit_logs( + team_id="team-quiet", + team_alias="quiet", + updated_users=_UntouchableUsers(), + existing_user_ids=frozenset(), + before_members=_UntouchableRoster(), + after_members=_UntouchableRoster(), + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin-user"), + litellm_proxy_admin_name="admin", + ) + + +@pytest.mark.parametrize( + "schedule", + [_schedule_membership_audit_with_untouchable_roster, _schedule_member_add_audit_with_untouchable_roster], +) +def test_membership_audit_scheduling_skips_the_roster_entirely_when_audit_logging_is_off(monkeypatch, schedule): + """Regression: the before/after rosters were serialized on every membership change, even + when audit logs are not stored.""" + monkeypatch.setattr("litellm.store_audit_logs", False) + + schedule() + + +@pytest.mark.asyncio +async def test_member_add_audit_reports_only_the_users_it_created_plus_the_roster_change(monkeypatch): + from litellm.proxy.management_endpoints.team_endpoints import _schedule_team_member_add_audit_logs + + audit_logger = _wire_audit_log_callback(monkeypatch) + mock_prisma_client = MagicMock() + mock_prisma_client.db.litellm_auditlog.create = AsyncMock() + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + before = (Member(user_id="alice", role="admin"),) + after = (*before, Member(user_id="bob", role="user"), Member(user_id="carol", role="user")) + + _schedule_team_member_add_audit_logs( + team_id="team-add-audit", + team_alias="add-audit", + updated_users=[ + LiteLLM_UserTable(user_id="bob", user_email="bob@example.com", teams=["team-add-audit"]), + LiteLLM_UserTable(user_id="carol", teams=["team-add-audit"]), + ], + existing_user_ids=frozenset({"bob"}), + before_members=before, + after_members=after, + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-admin", user_id="admin-1"), + litellm_proxy_admin_name="admin", + ) + await _settle_audit_log_tasks() + + created_users = [ + p + for p in audit_logger.payloads + if p["table_name"] == LitellmTableNames.USER_TABLE_NAME and p["action"] == "created" + ] + assert [p["object_id"] for p in created_users] == ["carol"], "only the user this request created is audited" + assert json.loads(created_users[0]["updated_values"])["teams"] == ["team-add-audit"] + + [roster_event] = _team_roster_events(audit_logger, "updated") + assert roster_event["object_id"] == "team-add-audit" + assert _roster_user_roles(roster_event["before_value"]) == {"alice": "admin"} + assert _roster_user_roles(roster_event["updated_values"]) == {"alice": "admin", "bob": "user", "carol": "user"} + assert _roster_team_alias(roster_event["updated_values"]) == "add-audit" + + @pytest.mark.asyncio async def test_delete_team_emits_only_the_deleted_audit_event(monkeypatch): from litellm.proxy._types import DeleteTeamRequest