Skip to content

Commit fed8a40

Browse files
akolotovclaude
andcommitted
Phase 7: Integration Tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dee64e8 commit fed8a40

1 file changed

Lines changed: 48 additions & 1 deletion

File tree

tests/integration/test_common_helpers.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from blockscout_mcp_server.config import config
6-
from blockscout_mcp_server.pro_api_key_context import _client_key_state, _Valid
6+
from blockscout_mcp_server.pro_api_key_context import CreditSink, _client_key_state, _credit_sink, _Valid
77
from blockscout_mcp_server.tools.common import (
88
ChainNotFoundError,
99
ensure_chain_supported,
@@ -206,3 +206,50 @@ async def test_make_blockscout_request_client_key_via_context_var(monkeypatch):
206206
assert "timestamp" in response_data
207207
assert isinstance(response_data["gas_used"], str) # Blockscout API returns this as a string
208208
assert "parent_hash" in response_data
209+
210+
211+
@pytest.mark.integration
212+
@pytest.mark.asyncio
213+
async def test_credit_capture_on_real_pro_api_response():
214+
"""
215+
Confirms that a real metered PRO API GET response carries ``x-credits-remaining``
216+
and that our capture code records a numeric value into the CreditSink.
217+
218+
This validates the request/response contract the whole feature rests on: the
219+
header actually arrives on a live response and the sink records it. POST and
220+
metadata surface coverage is already proven at unit level in Phase 2's
221+
parameterized test; live coverage of a single GET here avoids network
222+
flakiness without omitting anything meaningful.
223+
"""
224+
# ARRANGE — skip immediately when no key is configured; without an authenticated
225+
# metered request the header would never be returned and the test would be
226+
# measuring the wrong thing.
227+
if not config.pro_api_key:
228+
pytest.skip("BLOCKSCOUT_PRO_API_KEY not configured; credit capture requires an authenticated metered request")
229+
230+
sink = CreditSink()
231+
token = _credit_sink.set(sink)
232+
try:
233+
chain_id = "100" # Gnosis Chain — same stable target as the existing block test
234+
block_number = "46282564"
235+
api_path = f"/api/v2/blocks/{block_number}"
236+
237+
# ACT — real network call; retry_on_network_error skips on transient failures
238+
# rather than letting flaky infrastructure cause a false negative.
239+
await retry_on_network_error(
240+
lambda: make_blockscout_request(chain_id=chain_id, api_path=api_path),
241+
action_description="PRO API block request for credit-capture verification",
242+
)
243+
finally:
244+
_credit_sink.reset(token)
245+
246+
# ASSERT — the header was present and the capture recorded a numeric value.
247+
# We do NOT assert a specific magnitude: the balance may be any value
248+
# (including negative for overdraft accounts).
249+
assert sink.remaining is not None, (
250+
"x-credits-remaining was not captured — the header may be absent on this endpoint "
251+
"or the capture logic in _make_blockscout_http_request is broken."
252+
)
253+
assert isinstance(sink.remaining, (int, float)), (
254+
f"Expected a numeric remaining-credits value, got {type(sink.remaining).__name__!r}: {sink.remaining!r}"
255+
)

0 commit comments

Comments
 (0)