test(e2e): rename suites, drop vendor_contract, fix greptile gaps

Move shared status helpers into e2e_http, rename chat auth headers and
chat security suites, remove vendor_contract and dev_config files_settings,
and tighten transcription validation plus vector-store search assertions
This commit is contained in:
mubashir1osmani 2026-07-24 16:16:53 -07:00
parent c8caf61ee2
commit 3c7d66989e
19 changed files with 126 additions and 143 deletions

View file

@ -204,11 +204,6 @@ general_settings:
# configured for the batched model. Defaults to false.
# track_unmanaged_batch_cost: true
# for /v1/files (vector store upload, batches, etc.)
files_settings:
- custom_llm_provider: openai
api_key: os.environ/OPENAI_API_KEY
sandbox_tools:
- sandbox_tool_name: e2b_sandbox
litellm_params:

View file

@ -1,8 +1,8 @@
"""Vendor §11.1 auth security on LLM routes (LIT-4778).
"""Chat Authorization header matrix on LLM routes (LIT-4778).
Virtual-key chat must reject missing and malformed Authorization headers before
any provider call. These cases sit next to the existing valid/invalid key check
and pin the full bearer-token failure matrix from the vendor brief.
and pin the bearer-token failure matrix.
"""
from __future__ import annotations
@ -27,7 +27,7 @@ class RawAuthorizationHeaders(BaseModel):
def _register_model(proxy: ProxyClient, resources: ResourceManager) -> str:
model = f"e2e-auth-sec-{unique_marker()}"
model = f"e2e-auth-headers-{unique_marker()}"
model_id = proxy.create_model(
model,
LiteLLMParamsBody(model=OPENAI_BACKEND, api_key="os.environ/OPENAI_API_KEY"),
@ -56,7 +56,7 @@ def _assert_auth_denied(result: StreamingResponse, context: str) -> None:
)
class TestChatAuthSecurity:
class TestChatAuthHeaders:
@pytest.mark.covers("other.auth.llm_chat.missing_header_denied")
def test_missing_authorization_header_is_denied(
self, proxy: ProxyClient, resources: ResourceManager

View file

@ -220,6 +220,70 @@ def require_successful_call(result: StreamingResponse) -> None:
)
def is_client_error(status: int) -> bool:
return 400 <= status < 500
def is_auth_denied(status: int) -> bool:
return status in (401, 403)
def assert_not_server_error(result: StreamingResponse, context: str) -> None:
assert result.status_code not in (500, 502, 503), (
f"{context}: proxy must not 5xx, got {result.status_code}: {result.body[:300]}"
)
def assert_client_error(result: StreamingResponse, context: str) -> None:
assert is_client_error(result.status_code), (
f"{context}: expected 4xx, got {result.status_code}: {result.body[:300]}"
)
def assert_error_or_server_known(result: StreamingResponse, context: str) -> None:
"""Missing required fields may be 4xx or 5xx per known acceptable proxy behavior."""
assert result.status_code in range(400, 600), (
f"{context}: expected error status, got {result.status_code}: {result.body[:300]}"
)
assert result.status_code != 200
def assert_auth_denied(result: StreamingResponse, context: str) -> None:
assert is_auth_denied(result.status_code), (
f"{context}: expected 401/403, got {result.status_code}: {result.body[:300]}"
)
def is_provider_account_denied(result: StreamingResponse) -> bool:
"""True when the gateway reached the provider and the account/model is disabled."""
if result.status_code not in (400, 403, 404):
return False
body = result.body.lower()
needles = (
"operation not allowed",
"end of its life",
"accessdenied",
"not authorized",
"model use case details have not been submitted",
"you don't have access",
"do not have access",
)
return any(n in body for n in needles)
def require_success_or_provider_denied(result: StreamingResponse, context: str) -> bool:
"""Return True on success; return False when the provider denied the account.
Raises on unexpected failures so real product regressions still fail hard.
"""
if result.ok and not result.stream_error:
return True
if is_provider_account_denied(result):
return False
require_successful_call(result)
return True
def _headers(headers: BaseModel) -> dict[str, str]:
dumped: dict[str, object] = headers.model_dump(by_alias=True, exclude_none=True)
return {key: str(value) for key, value in dumped.items()}

View file

@ -12,11 +12,10 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import require_successful_call
from e2e_http import require_successful_call, assert_error_or_server_known
from endpoints_client import EndpointsClient
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -79,10 +79,12 @@ class TestAudioTranscriptions:
match result:
case Success():
pytest.fail("empty audio file must not succeed as a transcript")
case UnknownApiError(status_code=status):
assert status in range(400, 600), f"unexpected {status}"
case _:
case UnknownApiError(status_code=status) if 400 <= status < 500:
return
case UnknownApiError(status_code=status):
pytest.fail(f"empty audio expected 4xx, got {status}: {result}")
case _:
pytest.fail(f"empty audio unexpected result: {result}")
@pytest.mark.covers("llm.audio_transcriptions.openai.input_validation.nonstream.works")
def test_missing_model_returns_error(
@ -101,7 +103,9 @@ class TestAudioTranscriptions:
match result:
case Success():
pytest.fail("transcription without model must not succeed")
case UnknownApiError(status_code=status):
assert status in range(400, 600), f"unexpected {status}"
case _:
case UnknownApiError(status_code=status) if 400 <= status < 500:
return
case UnknownApiError(status_code=status):
pytest.fail(f"missing model expected 4xx, got {status}: {result}")
case _:
pytest.fail(f"missing model unexpected result: {result}")

View file

@ -7,19 +7,18 @@ missing messages and invalid model handling without crashing the proxy.
from __future__ import annotations
import pytest
import requests
from pydantic import BaseModel
from e2e_config import unique_marker
import requests
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from proxy_client import ProxyClient
from vendor_contract import (
from e2e_http import (
assert_client_error,
assert_error_or_server_known,
require_success_or_provider_denied,
)
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from proxy_client import ProxyClient
pytestmark = pytest.mark.e2e

View file

@ -1,7 +1,7 @@
"""Vendor API strategy coverage for /chat/completions (LIT-4778).
"""Chat completions security and input-sanitization e2e (LIT-4778).
Positive multi-turn history, input validation, boundary handling, contract shape,
and input sanitization against a live proxy and a real OpenAI-compatible model.
Multi-turn history, input validation, boundary handling, response shape, and
SQL/XSS payload sanitization against a live proxy and a real OpenAI-compatible model.
"""
from __future__ import annotations
@ -51,7 +51,7 @@ class ChatErrorEnvelope(BaseModel):
def _register_chat_model(proxy: ProxyClient, resources: ResourceManager) -> tuple[str, str]:
model = f"e2e-vendor-chat-{unique_marker()}"
model = f"e2e-chat-sec-{unique_marker()}"
model_id = proxy.create_model(
model,
LiteLLMParamsBody(model=OPENAI_BACKEND, api_key="os.environ/OPENAI_API_KEY"),
@ -80,7 +80,7 @@ def _assert_not_server_error(result: StreamingResponse, context: str) -> None:
)
class TestChatCompletionsVendorContract:
class TestChatCompletionsSecVulnerability:
@pytest.mark.covers("llm.chat_completions.openai.multi_turn.nonstream.works")
def test_multi_turn_history_is_honored(
self, proxy: ProxyClient, resources: ResourceManager

View file

@ -12,15 +12,15 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import require_successful_call
from endpoints_client import EmbeddingsResult, EndpointsClient
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from vendor_contract import (
from e2e_http import (
assert_client_error,
assert_error_or_server_known,
require_success_or_provider_denied,
require_successful_call,
)
from endpoints_client import EmbeddingsResult, EndpointsClient
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
pytestmark = pytest.mark.e2e

View file

@ -10,11 +10,10 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import NoBody, Success, UnknownApiError
from e2e_http import NoBody, Success, UnknownApiError, assert_error_or_server_known
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from proxy_client import ProxyClient
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -11,15 +11,15 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import require_successful_call
from endpoints_client import EndpointsClient, ImagesResult
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from vendor_contract import (
from e2e_http import (
assert_client_error,
assert_error_or_server_known,
require_success_or_provider_denied,
require_successful_call,
)
from endpoints_client import EndpointsClient, ImagesResult
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
pytestmark = pytest.mark.e2e

View file

@ -12,7 +12,7 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import require_successful_call, unwrap
from e2e_http import require_successful_call, unwrap, assert_error_or_server_known
from endpoints_client import EndpointsClient, MessagesResult
from lifecycle import ResourceManager
from models import (
@ -24,7 +24,6 @@ from models import (
SpendLogRow,
ToolInputSchema,
)
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -11,11 +11,10 @@ from dataclasses import dataclass
import pytest
from e2e_config import unique_marker
from e2e_http import StreamingResponse, UnknownApiError, unwrap
from e2e_http import StreamingResponse, UnknownApiError, unwrap, is_provider_account_denied
from lifecycle import ResourceManager
from models import ChatBody, ChatMessage, LiteLLMParamsBody
from proxy_client import ProxyClient
from vendor_contract import is_provider_account_denied
pytestmark = pytest.mark.e2e

View file

@ -11,11 +11,10 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import unwrap
from e2e_http import unwrap, assert_error_or_server_known
from endpoints_client import EndpointsClient
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -23,11 +23,10 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import unwrap
from e2e_http import unwrap, assert_error_or_server_known
from endpoints_client import EndpointsClient
from lifecycle import ResourceManager
from models import LiteLLMParamsBody, OcrBody, OcrDocument, OcrResponse
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -10,11 +10,10 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import NoBody, unwrap
from e2e_http import NoBody, unwrap, assert_auth_denied
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from proxy_client import ProxyClient
from vendor_contract import assert_auth_denied
pytestmark = pytest.mark.e2e

View file

@ -14,7 +14,14 @@ import pytest
from pydantic import BaseModel, ValidationError
from e2e_config import unique_marker
from e2e_http import require_successful_call
from e2e_http import (
assert_client_error,
assert_error_or_server_known,
assert_not_server_error,
is_client_error,
require_success_or_provider_denied,
require_successful_call,
)
from endpoints_client import (
EndpointsClient,
FunctionParameterProperty,
@ -26,13 +33,6 @@ from endpoints_client import (
)
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from vendor_contract import (
assert_client_error,
assert_error_or_server_known,
assert_not_server_error,
is_client_error,
require_success_or_provider_denied,
)
pytestmark = pytest.mark.e2e

View file

@ -12,10 +12,9 @@ import pytest
from pydantic import BaseModel
from e2e_config import unique_marker
from e2e_http import NoBody, unwrap
from e2e_http import NoBody, unwrap, assert_client_error, assert_error_or_server_known
from lifecycle import ResourceManager
from proxy_client import ProxyClient
from vendor_contract import assert_client_error, assert_error_or_server_known
pytestmark = pytest.mark.e2e

View file

@ -13,11 +13,10 @@ import pytest
from pydantic import BaseModel, ConfigDict
from e2e_config import POLL_INTERVAL, POLL_TIMEOUT, unique_marker
from e2e_http import FileUploadForm, NoBody, unwrap
from e2e_http import FileUploadForm, NoBody, unwrap, assert_error_or_server_known
from lifecycle import ResourceManager
from models import LiteLLMParamsBody
from proxy_client import ProxyClient
from vendor_contract import assert_error_or_server_known
pytestmark = pytest.mark.e2e
@ -221,7 +220,7 @@ class TestVectorStores:
proxy.transport.upload(
"/v1/files",
headers=proxy.transport.bearer(key),
form=FileUploadForm(purpose="assistants"),
form=FileUploadForm(purpose="assistants", custom_llm_provider="openai"),
filename="vs_doc.txt",
content=content,
file_content_type="text/plain",
@ -275,7 +274,16 @@ class TestVectorStores:
response_type=VectorStoreSearchResponse,
)
)
assert search.data is not None, f"search returned no data field: {search}"
assert search.data, f"search returned no hits for marker {marker!r}: {search}"
hit_blob = " ".join(
" ".join(part.get("text", "") for part in (hit.content or []))
+ " "
+ (hit.filename or "")
for hit in search.data
)
assert marker in hit_blob or any(
(hit.file_id or "") == uploaded.id for hit in search.data
), f"search hits must reference marker or uploaded file; marker={marker!r} hits={search.data}"
deleted_file = unwrap(
proxy.transport.delete(

View file

@ -1,79 +0,0 @@
"""Shared helpers for vendor API contract e2e tests (LIT-4778).
Status-centric assertions used across endpoint negatives, sanitization, and
auth matrix cases so each test file stays thin.
"""
from __future__ import annotations
from e2e_http import StreamingResponse
def is_client_error(status: int) -> bool:
return 400 <= status < 500
def is_auth_denied(status: int) -> bool:
return status in (401, 403)
def assert_not_server_error(result: StreamingResponse, context: str) -> None:
assert result.status_code not in (500, 502, 503), (
f"{context}: proxy must not 5xx, got {result.status_code}: {result.body[:300]}"
)
def assert_client_error(result: StreamingResponse, context: str) -> None:
assert is_client_error(result.status_code), (
f"{context}: expected 4xx, got {result.status_code}: {result.body[:300]}"
)
def assert_error_or_server_known(result: StreamingResponse, context: str) -> None:
"""Missing required fields may be 4xx or 5xx per known acceptable proxy behavior."""
assert result.status_code in range(400, 600), (
f"{context}: expected error status, got {result.status_code}: {result.body[:300]}"
)
assert result.status_code != 200
def assert_auth_denied(result: StreamingResponse, context: str) -> None:
assert is_auth_denied(result.status_code), (
f"{context}: expected 401/403, got {result.status_code}: {result.body[:300]}"
)
def is_provider_account_denied(result: StreamingResponse) -> bool:
"""True when the gateway reached the provider and the account/model is disabled.
Common on local AWS accounts that list Bedrock models but cannot InvokeModel
("Operation not allowed") or when a model version is EOL.
"""
if result.status_code not in (400, 403, 404):
return False
body = result.body.lower()
needles = (
"operation not allowed",
"end of its life",
"accessdenied",
"not authorized",
"model use case details have not been submitted",
"you don't have access",
"do not have access",
)
return any(n in body for n in needles)
def require_success_or_provider_denied(result: StreamingResponse, context: str) -> bool:
"""Return True on success; return False when the provider denied the account.
Raises on unexpected failures so real product regressions still fail hard.
"""
if result.ok and not result.stream_error:
return True
if is_provider_account_denied(result):
return False
from e2e_http import require_successful_call
require_successful_call(result)
return True