Files
teng-lin--notebooklm-py/tests/unit/test_error_injection_middleware.py
wehub-resource-sync 09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

623 lines
24 KiB
Python

"""Unit tests for :class:`ErrorInjectionMiddleware` (Tier-12 PR 12.6 / PR 12.7).
Pins the contract documented in ``src/notebooklm/_middleware/error_injection.py``
and ADR-0009 §"Chain ordering":
- **Pass-through when env var is unset.** The middleware delegates straight
to ``next_call``; production behavior is byte-for-byte unchanged.
- **Pass-through when no builder is wired (issue #1005).** Even with the
env var set to a valid mode, the production default ``builder=None``
makes ``__call__`` a pass-through. Tests that exercise substitution
must construct the middleware with an explicit ``builder=`` argument.
- **Raise transport exceptions for 429 / 5xx (PR 12.7).** When the env var
resolves to ``"429"`` AND a builder is wired, the middleware raises
:class:`TransportRateLimited` (carrying the synthetic ``Retry-After``);
``"5xx"`` raises :class:`TransportServerError`. This is the contract
that lets the OUTER ``RetryMiddleware`` retry, restoring ADR-0009
§"ErrorInjection inside Retry — synthetic transient failures trigger
retry" (codex iter-1 catch on PR 12.7).
- **Raise raw ``httpx.HTTPStatusError`` for expired_csrf (HTTP 400).** That
mode lets the OUTER ``AuthRefreshMiddleware`` catch via ``is_auth_error`` and
drive refresh-then-retry.
- **Request shape preserved on the wrapped response.** The wrapped
:class:`httpx.Response` (whether returned or carried in a raised
exception) has a ``response.request`` attached whose
``method`` / ``url`` / ``body`` mirror the incoming
:class:`RpcRequest`.
The tests use a real :class:`ErrorInjectionMiddleware` instance plus the
canonical chain fixtures (``make_request`` and a one-shot terminal stub)
rather than mocking the substitution logic. Activation is flipped via
:func:`monkeypatch.setenv` against ``NOTEBOOKLM_VCR_RECORD_ERRORS`` so the
production env-var resolution code path
(:func:`notebooklm._error_injection._get_error_injection_mode`) is
exercised end-to-end. Tests that need substitution to fire pass
``builder=build_synthetic_error_response`` from
``tests.cassette_patterns`` explicitly into the middleware constructor —
that's the post-#1005 wiring that replaces the previous filesystem-load
implementation.
"""
from __future__ import annotations
import httpx
import pytest
from notebooklm._error_injection import ERROR_INJECT_ENV_VAR
from notebooklm._middleware.core import NextCall, RpcRequest, RpcResponse, build_chain
from notebooklm._middleware.error_injection import ErrorInjectionMiddleware
from notebooklm._transport_errors import TransportRateLimited, TransportServerError
# The ``tests/`` package chain is complete; ``tests._fixtures.chain`` is the
# fully-qualified import path documented in ``tests/_fixtures/__init__.py``.
from tests._fixtures.chain import make_request
from tests.cassette_patterns import build_synthetic_error_response
def _static_terminal(response: httpx.Response) -> NextCall:
"""Build a chain-terminal coroutine that wraps ``response``."""
async def terminal(request: RpcRequest) -> RpcResponse:
return RpcResponse(response=response, context=request.context)
return terminal
def _recording_terminal() -> tuple[NextCall, list[RpcRequest]]:
"""Build a terminal that records every request it sees."""
calls: list[RpcRequest] = []
async def terminal(request: RpcRequest) -> RpcResponse:
calls.append(request)
return RpcResponse(
response=httpx.Response(status_code=200, content=b"leaf-reached"),
context=request.context,
)
return terminal, calls
# ---------------------------------------------------------------------------
# Pass-through when env var is unset
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_passes_through_when_env_var_unset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Default path: env var unset → middleware delegates to ``next_call``."""
monkeypatch.delenv(ERROR_INJECT_ENV_VAR, raising=False)
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware()
chain = build_chain([middleware], terminal)
response = await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
assert len(calls) == 1
assert response.response.status_code == 200
assert response.response.content == b"leaf-reached"
@pytest.mark.asyncio
async def test_passes_through_when_env_var_empty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An empty-string env var also resolves to ``None`` → pass-through."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, " ")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware()
chain = build_chain([middleware], terminal)
await chain(make_request())
assert len(calls) == 1
@pytest.mark.asyncio
async def test_passes_through_when_env_var_unknown_mode(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Unrecognized mode → ``_get_error_injection_mode`` returns ``None``."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "418") # not in VALID_ERROR_MODES
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware()
chain = build_chain([middleware], terminal)
await chain(make_request())
assert len(calls) == 1
# ---------------------------------------------------------------------------
# 429 → raise TransportRateLimited
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_429_mode_raises_transport_rate_limited(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``429`` mode raises :class:`TransportRateLimited` for ``RetryMiddleware``.
Restores ADR-0009 §"ErrorInjection inside Retry — synthetic transient
failures trigger retry" (codex iter-1 catch on PR 12.7). The raised
exception carries the synthetic ``Retry-After`` so the outer retry
honors rate-limit timing.
"""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "429")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], terminal)
with pytest.raises(TransportRateLimited) as excinfo:
await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
assert calls == []
exc = excinfo.value
assert exc.retry_after == 1
assert exc.response is not None
assert exc.response.status_code == 429
assert "RPC LIST_NOTEBOOKS" in str(exc)
@pytest.mark.asyncio
async def test_429_response_carries_synthetic_request_url_and_body(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The synthetic ``httpx.Response.request`` mirrors the chain request."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "429")
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], _static_terminal(httpx.Response(200, content=b"unreached")))
custom_url = "https://example.test/_/LabsTailwindUi/data/batchexecute?authuser=0"
with pytest.raises(TransportRateLimited) as excinfo:
await chain(make_request(url=custom_url, body=b"chain-body"))
response = excinfo.value.response
assert response is not None
assert response.request is not None
assert response.request.method == "POST"
assert str(response.request.url) == custom_url
assert response.request.content == b"chain-body"
# ---------------------------------------------------------------------------
# 5xx → raise TransportServerError
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_5xx_mode_raises_transport_server_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``5xx`` mode raises :class:`TransportServerError` for ``RetryMiddleware``."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "5xx")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], terminal)
with pytest.raises(TransportServerError) as excinfo:
await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
assert calls == []
exc = excinfo.value
assert exc.status_code == 500
assert exc.response is not None
assert "application/json" in exc.response.headers.get("content-type", "")
assert b'"error"' in exc.response.content
# ---------------------------------------------------------------------------
# expired_csrf → raise httpx.HTTPStatusError (AuthRefreshMiddleware catches)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_expired_csrf_mode_raises_http_status_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""``expired_csrf`` mode raises the raw ``httpx.HTTPStatusError``.
PR 12.8 wired AuthRefreshMiddleware outside this middleware in the
final chain ordering. AuthRefresh catches via ``is_auth_error``
(which recognizes 400/401/403 from Google's auth-shape responses)
and drives the refresh-then-retry flow. Pre-PR-12.6 this happened
naturally because the legacy ``_SyntheticErrorTransport`` returned
the synthetic 400 below httpx and the leaf's auth-refresh branch
handled it.
"""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "expired_csrf")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], terminal)
with pytest.raises(httpx.HTTPStatusError) as excinfo:
await chain(make_request())
assert calls == [] # leaf NOT reached
assert excinfo.value.response.status_code == 400
assert "HTTP 400" in str(excinfo.value)
@pytest.mark.asyncio
async def test_expired_csrf_response_carries_synthetic_request_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The raised HTTPStatusError wraps a synthetic ``httpx.Response`` whose
``.request`` mirrors the chain envelope."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "expired_csrf")
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], _static_terminal(httpx.Response(200, content=b"unreached")))
custom_url = "https://example.test/_/LabsTailwindUi/data/batchexecute?authuser=0"
with pytest.raises(httpx.HTTPStatusError) as excinfo:
await chain(make_request(url=custom_url))
response = excinfo.value.response
assert response.status_code == 400
assert response.request is not None
assert str(response.request.url) == custom_url
# ---------------------------------------------------------------------------
# End-to-end: Retry + ErrorInjection actually retries (codex iter-1 finding)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_retry_outside_error_injection_retries_synthetic_429(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""End-to-end: chain ``[Retry, ErrorInjection]`` retries synthetic 429s.
This is the codex iter-1 catch: without ErrorInjection raising
:class:`TransportRateLimited`, the synthetic 429 flowed back as a
returned response and Retry never saw it. With the exception-raising
fix, Retry catches and retries N times before re-raising.
"""
from notebooklm._middleware.retry import RetryMiddleware
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "429")
slept: list[float] = []
async def fake_sleep(seconds: float) -> None:
slept.append(seconds)
error_injection = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
retry = RetryMiddleware(
rate_limit_max_retries=2,
server_error_max_retries=2,
sleep=fake_sleep,
)
chain = build_chain(
[retry, error_injection], _static_terminal(httpx.Response(200, content=b"x"))
)
with pytest.raises(TransportRateLimited):
await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
# 1 initial + 2 retries → 2 sleeps observed.
assert len(slept) == 2
# Each sleep honors the synthetic Retry-After=1.
assert slept == [1.0, 1.0]
@pytest.mark.asyncio
async def test_retry_outside_error_injection_retries_synthetic_5xx(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""End-to-end: chain ``[Retry, ErrorInjection]`` retries synthetic 5xx too."""
from notebooklm._middleware.retry import RetryMiddleware
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "5xx")
slept: list[float] = []
async def fake_sleep(seconds: float) -> None:
slept.append(seconds)
error_injection = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
retry = RetryMiddleware(
rate_limit_max_retries=2,
server_error_max_retries=1,
sleep=fake_sleep,
)
chain = build_chain(
[retry, error_injection], _static_terminal(httpx.Response(200, content=b"x"))
)
with pytest.raises(TransportServerError):
await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
# 1 initial + 1 retry → 1 sleep observed.
assert len(slept) == 1
# ---------------------------------------------------------------------------
# End-to-end: AuthRefresh + ErrorInjection drives refresh on synthetic 400
# (codex iter-1 finding on PR 12.8 — locks in the regression fix)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_auth_refresh_outside_error_injection_triggers_refresh_on_expired_csrf(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""End-to-end: chain ``[AuthRefresh, ErrorInjection]`` refreshes on synthetic 400.
Codex iter-1 catch on PR 12.8: PR 12.6 broke the refresh-on-synthetic-400
path by returning an ``RpcResponse`` from :class:`ErrorInjectionMiddleware`
for ``expired_csrf`` mode. PR 12.8 fixes by raising raw
``httpx.HTTPStatusError`` so :class:`AuthRefreshMiddleware` outside it
catches via ``is_auth_error`` and drives refresh-then-retry.
This is the missing E2E counterpart to the ``[Retry, ErrorInjection]``
pair above — without it the integration is only validated by two
independent unit tests (the leaf raises 400; AuthRefresh catches 400)
but never end-to-end on a real two-middleware chain.
Test shape: env var stays on across the retry leg, so the retry leg
also raises ``HTTPStatusError(400)``. The exactly-once contract from
ADR-0009 §"Retry semantics" means refresh runs exactly once and the
second 400 propagates without recursion.
"""
from notebooklm._middleware.auth_refresh import AuthRefreshMiddleware
from notebooklm._runtime.helpers import is_auth_error as auth_error_predicate
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "expired_csrf")
refresh_calls: list[None] = []
async def refresh() -> None:
refresh_calls.append(None)
auth_refresh = AuthRefreshMiddleware(
refresh_callable=refresh,
is_auth_error=auth_error_predicate,
refresh_callback_enabled=lambda: True,
refresh_retry_delay=lambda: 0.0,
)
error_injection = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain(
[auth_refresh, error_injection],
_static_terminal(httpx.Response(200, content=b"unreached-because-env-is-on")),
)
with pytest.raises(httpx.HTTPStatusError) as excinfo:
await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
# AuthRefresh caught the synthetic 400 from ErrorInjection and drove
# ONE refresh — the retry leg's 400 propagates unchanged (exactly-once
# contract). Without PR 12.8's fix, ErrorInjection would have RETURNED
# a 400 RpcResponse, AuthRefresh would have seen no exception, refresh
# would never have fired, and ``refresh_calls`` would be empty.
assert len(refresh_calls) == 1
assert excinfo.value.response.status_code == 400
@pytest.mark.asyncio
async def test_auth_refresh_outside_error_injection_completes_when_env_flips_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""End-to-end happy path: ``[AuthRefresh, ErrorInjection]`` retries successfully.
Companion to the test above: when the env var is flipped off during
refresh (production analogue: a real ``__Secure-1PSIDTS`` rotation
succeeded and the retry no longer hits the synthetic 400 path), the
chain returns 200 cleanly. This pins the full refresh-then-retry
success path, not just the propagation path.
"""
from notebooklm._middleware.auth_refresh import AuthRefreshMiddleware
from notebooklm._runtime.helpers import is_auth_error as auth_error_predicate
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "expired_csrf")
refresh_calls: list[None] = []
async def refresh() -> None:
# Simulate a successful token rotation that disarms the injector
# before the retry leg runs.
refresh_calls.append(None)
monkeypatch.delenv(ERROR_INJECT_ENV_VAR, raising=False)
auth_refresh = AuthRefreshMiddleware(
refresh_callable=refresh,
is_auth_error=auth_error_predicate,
refresh_callback_enabled=lambda: True,
refresh_retry_delay=lambda: 0.0,
)
error_injection = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain(
[auth_refresh, error_injection],
_static_terminal(httpx.Response(200, content=b"after-refresh-success")),
)
response = await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
assert len(refresh_calls) == 1
assert response.response.status_code == 200
assert response.response.content == b"after-refresh-success"
# ---------------------------------------------------------------------------
# Builder injection (issue #1005)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_default_constructor_has_no_builder_and_passes_through(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Default ``ErrorInjectionMiddleware()`` (no builder arg) is a pass-through.
Issue #1005 hardening: even with the env var set to a valid mode, a
middleware constructed without an injected builder must NOT short-circuit.
Production code (``MiddlewareChainBuilder``) constructs the middleware
this way, so this test pins the "leaked env var on a user install can
never trigger synthetic substitution" invariant.
"""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "429")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware() # no builder → pass-through
assert middleware._builder is None
chain = build_chain([middleware], terminal)
response = await chain(make_request(context={"log_label": "RPC LIST_NOTEBOOKS"}))
# Leaf was reached — middleware did NOT raise / short-circuit.
assert len(calls) == 1
assert response.response.status_code == 200
@pytest.mark.asyncio
async def test_explicit_builder_is_used_on_every_call(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The injected ``builder`` callable is invoked on every chain call.
Replaces the legacy ``_load_builder`` caching test: now the builder is
supplied at construction time so there's no lazy-load to cache. We
verify the SAME injected callable is what produces the synthetic
response on each call by counting invocations.
"""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "429")
invocations: list[str] = []
def counting_builder(mode: str) -> tuple[int, bytes, dict[str, str]]:
invocations.append(mode)
return build_synthetic_error_response(mode)
middleware = ErrorInjectionMiddleware(builder=counting_builder)
chain = build_chain([middleware], _static_terminal(httpx.Response(200, content=b"unreached")))
with pytest.raises(TransportRateLimited):
await chain(make_request())
with pytest.raises(TransportRateLimited):
await chain(make_request())
# Injected builder was invoked on each chain call (the synthetic
# response object is built fresh per call — there's no instance cache
# of the response itself, only the builder is held).
assert invocations == ["429", "429"]
assert middleware._builder is counting_builder
# ---------------------------------------------------------------------------
# Activation flip mid-chain
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_activation_flip_between_calls_is_observed(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Flipping the env var between two chain calls observes both modes."""
monkeypatch.delenv(ERROR_INJECT_ENV_VAR, raising=False)
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], terminal)
# Call 1: pass-through, leaf reached.
await chain(make_request())
assert len(calls) == 1
# Flip on.
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "5xx")
# Call 2: short-circuit (now raises), leaf NOT reached again.
with pytest.raises(TransportServerError):
await chain(make_request())
assert len(calls) == 1 # still 1 — leaf was bypassed on second call
# ---------------------------------------------------------------------------
# Type hygiene
# ---------------------------------------------------------------------------
def test_middleware_satisfies_protocol() -> None:
"""``ErrorInjectionMiddleware`` instance is assignable to ``Middleware``."""
from notebooklm._middleware.core import Middleware
middleware: Middleware = ErrorInjectionMiddleware()
assert callable(middleware)
# ---------------------------------------------------------------------------
# Monkeypatch seam + activation log
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_monkeypatch_setattr_on_get_error_injection_mode_is_live(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The middleware resolves ``_get_error_injection_mode`` through the
module at call time, so ``monkeypatch.setattr(_error_injection,
"_get_error_injection_mode", …)`` reaches the chain.
"""
from notebooklm import _error_injection as _eim_module
monkeypatch.delenv(ERROR_INJECT_ENV_VAR, raising=False)
monkeypatch.setattr(_eim_module, "_get_error_injection_mode", lambda: "5xx")
terminal, calls = _recording_terminal()
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], terminal)
with pytest.raises(TransportServerError):
await chain(make_request())
assert calls == []
@pytest.mark.asyncio
async def test_activation_log_fires_once_per_instance(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""The "synthetic-error injection enabled" log line fires exactly once."""
monkeypatch.setenv(ERROR_INJECT_ENV_VAR, "5xx")
middleware = ErrorInjectionMiddleware(builder=build_synthetic_error_response)
chain = build_chain([middleware], _static_terminal(httpx.Response(200, content=b"x")))
with caplog.at_level("INFO", logger="notebooklm._core"):
# Each call raises; this verifies the log is emitted on the FIRST
# call's path and suppressed on subsequent calls.
with pytest.raises(TransportServerError):
await chain(make_request())
with pytest.raises(TransportServerError):
await chain(make_request())
with pytest.raises(TransportServerError):
await chain(make_request())
activations = [r for r in caplog.records if "synthetic-error injection enabled" in r.message]
assert len(activations) == 1
# ---------------------------------------------------------------------------
# Function-call signature
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_call_receives_next_call_and_invokes_it(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the env var is unset, ``__call__`` invokes ``next_call(request)``."""
monkeypatch.delenv(ERROR_INJECT_ENV_VAR, raising=False)
seen: list[RpcRequest] = []
async def next_call(request: RpcRequest) -> RpcResponse:
seen.append(request)
return RpcResponse(
response=httpx.Response(status_code=200, content=b"next-called"),
context=request.context,
)
middleware = ErrorInjectionMiddleware()
request = make_request()
response = await middleware(request, next_call)
assert seen == [request]
assert response.response.content == b"next-called"