polyglot-alpha / tests /test_e2e_malformed_input.py
licaomeng
deploy: main@8970ffb → HF Spaces (2026-05-27T05:19Z)
88d2f2a
"""E2E tests for malformed /trigger/event payloads.
Verifies the Pydantic validators in
``polyglot_alpha.api.routes.trigger`` reject ill-formed inputs with a
422 before they reach the orchestrator. Exercises:
* empty title in ``user_payload`` mode
* invalid language code (length cap + non-empty when relevant)
* negative bid amounts
* mock_bids list larger than MAX_BIDS_PER_REQUEST
* NaN / inf bid amounts
Uses TestClient (synchronous) — these tests do not run a real lifecycle
so MockLLM and judge stubs are not needed.
"""
from __future__ import annotations
import math
from typing import Any
import pytest
from fastapi.testclient import TestClient
def _build_app() -> Any:
from polyglot_alpha.api.main import create_app
return create_app()
def _baseline_bids() -> list[dict[str, Any]]:
return [{"agent_address": "0xagent", "bid_amount": 1.0}]
# ---------------------------------------------------------------------------
# 1. Empty title in ``user_payload`` mode -> 422.
# ---------------------------------------------------------------------------
def test_trigger_with_empty_title_user_payload_422(isolated_db: str) -> None:
"""``title=""`` (or whitespace) must be rejected with 422.
The route enforces this explicitly — Pydantic's ``max_length`` allows
empty strings, but the trigger handler raises HTTPException(422) when
title is empty for ``user_payload``.
"""
app = _build_app()
with TestClient(app) as client:
# Whitespace-only title -> 422.
r = client.post(
"/trigger/event",
json={
"title": " ",
"sources": [{"name": "t", "url": "https://example.com"}],
"auction_window_seconds": 0.0,
"mock_bids": _baseline_bids(),
},
)
assert r.status_code == 422, r.text
# Empty title -> 422.
r2 = client.post(
"/trigger/event",
json={
"title": "",
"sources": [{"name": "t", "url": "https://example.com"}],
"auction_window_seconds": 0.0,
"mock_bids": _baseline_bids(),
},
)
assert r2.status_code == 422, r2.text
# ---------------------------------------------------------------------------
# 2. Invalid language code -> 422.
# ---------------------------------------------------------------------------
def test_trigger_with_invalid_language_code_422(isolated_db: str) -> None:
"""A 64-char language string overflows the 16-char Pydantic cap -> 422.
The TriggerRequest model declares ``language: str = Field(default="en",
max_length=16)``. A 64-char garbage string therefore fails validation.
A 3-char string like ``"xxx"`` is accepted (length OK) and the
orchestrator falls back to that as an opaque tag, which is the
documented behaviour.
"""
app = _build_app()
with TestClient(app) as client:
# Overlong language code -> 422 (length cap).
r = client.post(
"/trigger/event",
json={
"title": "Language overflow test",
"language": "x" * 64,
"sources": [{"name": "t", "url": "https://example.com"}],
"auction_window_seconds": 0.0,
"mock_bids": _baseline_bids(),
},
)
assert r.status_code == 422, r.text
# ---------------------------------------------------------------------------
# 3. Negative bid amount -> 422.
# ---------------------------------------------------------------------------
def test_trigger_with_negative_bid_amount_422(isolated_db: str) -> None:
"""``bid_amount=-1`` violates ``ge=MIN_BID_AMOUNT`` -> 422."""
app = _build_app()
with TestClient(app) as client:
r = client.post(
"/trigger/event",
json={
"title": "Negative bid test",
"sources": [{"name": "t", "url": "https://example.com"}],
"auction_window_seconds": 0.0,
"mock_bids": [
{"agent_address": "0xneg", "bid_amount": -1.0},
],
},
)
assert r.status_code == 422, r.text
# ---------------------------------------------------------------------------
# 4. mock_bids list overflow -> 422.
# ---------------------------------------------------------------------------
def test_trigger_with_too_many_bids_rejects(isolated_db: str) -> None:
"""21 mock_bids > MAX_BIDS_PER_REQUEST (20) -> 422."""
app = _build_app()
with TestClient(app) as client:
bids = [
{"agent_address": f"0xbid{i:02d}", "bid_amount": float(i + 1)}
for i in range(21)
]
r = client.post(
"/trigger/event",
json={
"title": "Too many bids test",
"sources": [{"name": "t", "url": "https://example.com"}],
"auction_window_seconds": 0.0,
"mock_bids": bids,
},
)
assert r.status_code == 422, r.text
# ---------------------------------------------------------------------------
# 5. NaN / inf bid amount -> 422.
# ---------------------------------------------------------------------------
def test_trigger_with_nan_inf_in_bid_rejected(isolated_db: str) -> None:
"""``bid_amount=inf`` / ``nan`` rejected by the explicit finite validator.
The TriggerBid model has a ``_reject_non_finite`` validator. We
invoke the Pydantic model directly to verify it raises, because
sending NaN/Inf through HTTP triggers a downstream JSON-encoding
crash in FastAPI's 422 path (the rejected float gets echoed into
``detail.input``, where the JSON encoder cannot serialise NaN/Inf —
documented in E3_test_findings.md).
"""
from pydantic import ValidationError
from polyglot_alpha.api.routes.trigger import TriggerBid
# NaN, +inf, -inf are all rejected by the chain of validators
# (Pydantic ``le``/``ge`` bounds + the explicit ``_reject_non_finite``
# validator). Any one of them triggering ValidationError satisfies
# the "never reaches the orchestrator" contract.
for bad in (math.nan, math.inf, -math.inf):
with pytest.raises(ValidationError):
TriggerBid(agent_address="0xnonfinite", bid_amount=bad)