litellm/tests/unit/integrations/gcs_pubsub/test_pub_sub.py
devin-ai-integration[bot] 1dbde0845e
test: migrate SDK and proxy-booting legacy CircleCI tests to their tiers (#45758)
* test: move guardrails, image, audio, batches, search legacy tests to unit

* test: restore the custom guardrail and remaining chunk 1 unit rows

* test: move the ocr matrix to e2e

* test: drop comments from moved tests and fix vertex ocr credentials

* test: move litellm utils and unified google legacy tests to their tiers

* test: move logging callback and pass-through unit legacy tests to their tiers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: move store_model_in_db legacy tests and start the admin ui migration

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: finish the proxy job rows and drop their CircleCI jobs

Moves the remaining multi-instance, openai endpoint, otel, pass-through and
top-level proxy tests into tests/integration with scripted upstreams, rewrites
the weak replacements to assert the outbound wire and exact responses, and
removes the build_and_test job group this PR owns from CircleCI

* test: tighten the key generation and error log assertions, rename migrated unit files to their mapped names

* test: shard the pagerduty unit tests and record the rust harness file on the coverage allowlist

* test: drive the SSO sign-in tests through a scripted IdP and isolate the cost center stand-in

* test: type the e2e beta header, prompt cache and secret manager values

* test: assert the missing model error message on the OpenAI routes

* test: drop the deleted otel base class import and restore mcp redaction and exact langfuse usage checks

* test: make the langfuse fixture directory a package for the layout check

* test: restore collected no-match cases, real langfuse and gcs payload checks, and gate the aws secret lane

* test: keep the ocr e2e pdf fixture next to the test

* test: put meta below covers on the streaming tool search e2e

* test: run the OCR matrix against a scripted upstream and fix two runner-only e2e failures

* test: count only OCR calls in the OCR matrix recorder

* test: pin OCR parse bodies and billed cost, group new e2e cases into classes

* ci: restore the python image anchor dropped in the main sync

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-11 00:17:21 -07:00

248 lines
9.3 KiB
Python

import asyncio
import base64
import datetime
import json
import os
import unittest
from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
from typing import Final, List, Optional, Tuple, cast
from unittest.mock import ANY, MagicMock, Mock, patch
import httpx
import pytest
import respx
import litellm
from pydantic import TypeAdapter
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.types.utils import StandardLoggingPayload
@dataclass(frozen=True, slots=True)
class _CachedVertexCredentials:
token: str
quota_project_id: str | None
expired: bool = False
def refresh(self, request: object) -> None:
return None
@pytest.fixture(autouse=True)
def _httpx_transport(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
litellm.in_memory_llm_clients_cache.flush_cache()
yield
litellm.in_memory_llm_clients_cache.flush_cache()
@pytest.mark.asyncio
async def test_construct_request_headers_project_id_from_env(monkeypatch):
"""Test that construct_request_headers uses GCS_PUBSUB_PROJECT_ID environment variable."""
from litellm.integrations.gcs_pubsub.pub_sub import GcsPubSubLogger
# Set up test environment variable
test_project_id = "test-project-123"
monkeypatch.setenv("GCS_PUBSUB_PROJECT_ID", test_project_id)
monkeypatch.setattr(
"litellm.proxy.proxy_server.premium_user",
True,
)
try:
# Create handler with no project_id
handler = GcsPubSubLogger(
topic_id="test-topic", credentials_path="test-path.json"
)
# Mock the Vertex AI auth calls
mock_auth_header = "mock-auth-header"
mock_token = "mock-token"
with patch(
"litellm.vertex_chat_completion.ensure_access_token_async"
) as mock_ensure_token:
mock_ensure_token.return_value = (mock_auth_header, test_project_id)
with patch(
"litellm.vertex_chat_completion._get_token_and_url"
) as mock_get_token:
mock_get_token.return_value = (mock_token, "mock-url")
# Call construct_request_headers
headers = await handler.construct_request_headers()
# Verify headers
assert headers == {
"Authorization": f"Bearer {mock_token}",
"Content-Type": "application/json",
}
# Verify ensure_access_token_async was called with correct project_id
mock_ensure_token.assert_called_once_with(
credentials="test-path.json",
project_id=test_project_id,
custom_llm_provider="vertex_ai",
)
finally:
# Clean up environment variable
del os.environ["GCS_PUBSUB_PROJECT_ID"]
def _authorized_user_credentials(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, project_id: str) -> str:
credentials_path: Final = tmp_path / "google-auth.json"
credentials_path.write_text(
json.dumps(
{
"type": "authorized_user",
"client_id": "unit-client",
"client_secret": "unit-secret",
"refresh_token": "unit-refresh-token",
"token": "unit-access-token",
"token_uri": "https://oauth2.googleapis.com/token",
"quota_project_id": "unit-project",
"expiry": "2099-01-01T00:00:00Z",
}
)
)
credentials_path_string: Final = str(credentials_path)
credentials: Final = _CachedVertexCredentials(token="unit-access-token", quota_project_id=project_id)
vertex_chat_completion: Final = litellm.vertex_chat_completion
monkeypatch.setitem(
vertex_chat_completion._credentials_project_mapping,
(credentials_path_string, project_id),
(credentials, project_id),
)
return credentials_path_string
@pytest.mark.asyncio
async def test_publish_message_encodes_the_standard_payload(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, tmp_path: Path
) -> None:
from litellm.integrations.gcs_pubsub.pub_sub import GcsPubSubLogger
project_id: Final = "unit-project"
topic_id: Final = "unit-topic"
url: Final = f"https://pubsub.googleapis.com/v1/projects/{project_id}/topics/{topic_id}:publish"
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
route: Final = respx_mock.post(url).mock(return_value=httpx.Response(202, json={"messageIds": ["publish-1"]}))
logger: Final = GcsPubSubLogger(
project_id=project_id,
topic_id=topic_id,
credentials_path=_authorized_user_credentials(monkeypatch, tmp_path, project_id),
)
payload: Final = cast(
StandardLoggingPayload,
{
"request_id": "request-1",
"model": "vertex_ai/gemini-2.5-flash",
"messages": [{"role": "user", "content": "publish this"}],
"response": {"choices": [{"message": {"content": "published"}}]},
},
)
response: Final = TypeAdapter(dict[str, list[str]]).validate_python(await logger.publish_message(payload))
request_json: Final = TypeAdapter(dict[str, list[dict[str, str]]]).validate_json(route.calls.last.request.content)
decoded_message: Final = TypeAdapter(dict[str, object]).validate_json(
base64.b64decode(request_json["messages"][0]["data"])
)
assert route.call_count == 1
assert route.calls.last.request.url == url
assert route.calls.last.request.headers["Authorization"] == "Bearer unit-access-token"
assert request_json == {
"messages": [
{
"data": base64.b64encode(json.dumps(payload, default=str).encode("utf-8")).decode("utf-8"),
}
]
}
assert decoded_message == payload
assert response == {"messageIds": ["publish-1"]}
def _without_volatile_gcs_payload_fields(value: object, path: str = "") -> object:
ignored_paths: Final = frozenset(
{
"request_id",
"litellm_call_id",
"metadata.litellm_call_id",
"session_id",
"startTime",
"endTime",
"completionStartTime",
"request_duration_ms",
"metadata.model_map_information",
"metadata.usage_object",
"metadata.cold_storage_object_key",
"metadata.litellm_overhead_time_ms",
"metadata.cost_breakdown",
"metadata.autorouter_savings",
"metadata.eval_information",
}
)
if isinstance(value, Mapping):
mapping: Final = TypeAdapter(dict[str, object]).validate_python(value)
return MappingProxyType(
{
key: _without_volatile_gcs_payload_fields(item, f"{path}.{key}" if path else key)
for key, item in mapping.items()
if (f"{path}.{key}" if path else key) not in ignored_paths
}
)
if isinstance(value, list):
items: Final = TypeAdapter(list[object]).validate_python(value)
return tuple(
_without_volatile_gcs_payload_fields(item, f"{path}[{index}]") for index, item in enumerate(items)
)
return value
def _with_decoded_metadata(payload: Mapping[str, object]) -> dict[str, object]:
metadata: Final = payload["metadata"]
return {**payload, "metadata": json.loads(metadata) if isinstance(metadata, str) else metadata}
@pytest.mark.asyncio
async def test_v1_success_event_publishes_the_spend_log_fixture(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, tmp_path: Path
) -> None:
from litellm.integrations.gcs_pubsub.pub_sub import GcsPubSubLogger
project_id: Final = "unit-project"
topic_id: Final = "unit-topic"
url: Final = f"https://pubsub.googleapis.com/v1/projects/{project_id}/topics/{topic_id}:publish"
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
monkeypatch.setattr(litellm, "gcs_pub_sub_use_v1", True)
route: Final = respx_mock.post(url).mock(return_value=httpx.Response(202, json={"messageIds": ["publish-2"]}))
logger: Final = GcsPubSubLogger(
project_id=project_id,
topic_id=topic_id,
credentials_path=_authorized_user_credentials(monkeypatch, tmp_path, project_id),
)
monkeypatch.setattr(litellm, "callbacks", [logger])
await litellm.acompletion(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello, world!"}],
mock_response="hi",
)
await asyncio.sleep(0)
await GLOBAL_LOGGING_WORKER.flush()
await logger.async_send_batch()
request_json: Final = TypeAdapter(dict[str, list[dict[str, str]]]).validate_json(route.calls.last.request.content)
decoded_payload: Final = TypeAdapter(dict[str, object]).validate_json(
base64.b64decode(request_json["messages"][0]["data"])
)
expected_payload: Final = TypeAdapter(dict[str, object]).validate_json(
Path(__file__).with_name("spend_logs_payload.json").read_text()
)
assert route.call_count == 1
assert route.calls.last.request.url == url
assert _without_volatile_gcs_payload_fields(_with_decoded_metadata(decoded_payload)) == (
_without_volatile_gcs_payload_fields(_with_decoded_metadata(expected_payload))
)