fix(backend): make email lookup non-blocking in set_user_tier endpoint

This commit is contained in:
Zamil Majdy
2026-04-02 18:14:34 +02:00
parent a9d13f0cbf
commit 749a56ca20
2 changed files with 51 additions and 11 deletions

View File

@@ -195,15 +195,22 @@ async def set_user_rate_limit_tier(
admin_user_id: str = Security(get_user_id),
) -> UserTierResponse:
"""Set a user's rate-limit tier. Admin-only."""
resolved_email = await get_user_email_by_id(request.user_id)
if resolved_email is None:
raise HTTPException(status_code=404, detail=f"User {request.user_id} not found")
old_tier = await get_user_tier(request.user_id)
# Resolve email for audit logging (non-blocking — don't fail the
# tier change if email lookup fails).
try:
resolved_email = await get_user_email_by_id(request.user_id)
except Exception:
logger.warning(
"Failed to resolve email for user %s", request.user_id, exc_info=True
)
resolved_email = None
logger.info(
"Admin %s changing tier for user %s: %s -> %s",
"Admin %s changing tier for user %s (%s): %s -> %s",
admin_user_id,
request.user_id,
resolved_email or "unknown",
old_tier.value,
request.tier.value,
)

View File

@@ -316,6 +316,16 @@ def test_set_user_tier(
target_user_id: str,
) -> None:
"""Test setting a user's rate-limit tier (upgrade)."""
mocker.patch(
f"{_MOCK_MODULE}.get_user_email_by_id",
new_callable=AsyncMock,
return_value=_TARGET_EMAIL,
)
mocker.patch(
f"{_MOCK_MODULE}.get_user_tier",
new_callable=AsyncMock,
return_value=SubscriptionTier.FREE,
)
mock_set = mocker.patch(
f"{_MOCK_MODULE}.set_user_tier",
new_callable=AsyncMock,
@@ -338,6 +348,11 @@ def test_set_user_tier_downgrade(
target_user_id: str,
) -> None:
"""Test downgrading a user's tier from PRO to FREE."""
mocker.patch(
f"{_MOCK_MODULE}.get_user_email_by_id",
new_callable=AsyncMock,
return_value=_TARGET_EMAIL,
)
mocker.patch(
f"{_MOCK_MODULE}.get_user_tier",
new_callable=AsyncMock,
@@ -390,17 +405,24 @@ def test_set_user_tier_invalid_tier_uppercase(
assert "detail" in body
def test_set_user_tier_user_not_found(
def test_set_user_tier_email_lookup_failure_non_blocking(
mocker: pytest_mock.MockerFixture,
target_user_id: str,
) -> None:
"""Test that setting tier for nonexistent user returns 404."""
import prisma.errors
"""Test that email lookup failure doesn't block tier change."""
mocker.patch(
f"{_MOCK_MODULE}.get_user_email_by_id",
new_callable=AsyncMock,
side_effect=Exception("DB connection failed"),
)
mocker.patch(
f"{_MOCK_MODULE}.get_user_tier",
new_callable=AsyncMock,
return_value=SubscriptionTier.FREE,
)
mock_set = mocker.patch(
f"{_MOCK_MODULE}.set_user_tier",
new_callable=AsyncMock,
side_effect=prisma.errors.RecordNotFoundError({"error": "Record not found"}),
)
response = client.post(
@@ -408,7 +430,8 @@ def test_set_user_tier_user_not_found(
json={"user_id": target_user_id, "tier": "PRO"},
)
assert response.status_code == 404
assert response.status_code == 200
mock_set.assert_awaited_once()
def test_set_user_tier_db_failure(
@@ -416,6 +439,16 @@ def test_set_user_tier_db_failure(
target_user_id: str,
) -> None:
"""Test that DB failure on set tier returns 500."""
mocker.patch(
f"{_MOCK_MODULE}.get_user_email_by_id",
new_callable=AsyncMock,
return_value=_TARGET_EMAIL,
)
mocker.patch(
f"{_MOCK_MODULE}.get_user_tier",
new_callable=AsyncMock,
return_value=SubscriptionTier.FREE,
)
mocker.patch(
f"{_MOCK_MODULE}.set_user_tier",
new_callable=AsyncMock,