2121
2222import math
2323import os
24+ from typing import Callable , TypeVar
2425
2526import httpx
2627import pytest
4142 "GASOLINE_USD" ,
4243 "DIESEL_USD" ,
4344}
45+ T = TypeVar ("T" )
4446
4547
4648@pytest .fixture (scope = "module" )
@@ -49,17 +51,42 @@ def demo() -> DemoResource:
4951 return DemoResource (base_url = DEMO_BASE_URL )
5052
5153
52- def _skip_on_network_error (exc : Exception ) -> None :
53- pytest .skip (f"live demo API unreachable: { exc } " )
54+ def _run_live_request (operation : Callable [[], T ]) -> T :
55+ """Run an always-on synthetic request; every request failure is signal."""
56+ return operation ()
57+
58+
59+ class TestMonitorFailureSemantics :
60+ @pytest .mark .parametrize ("failure_kind" , ["status" , "transport" , "os" ])
61+ def test_request_failures_cannot_skip_the_hosted_monitor (
62+ self , failure_kind : str , monkeypatch : pytest .MonkeyPatch
63+ ) -> None :
64+ request = httpx .Request ("GET" , f"{ DEMO_BASE_URL } /v1/demo/prices" )
65+ if failure_kind == "status" :
66+ response = httpx .Response (503 , request = request )
67+ error : Exception = httpx .HTTPStatusError (
68+ "Service Unavailable" , request = request , response = response
69+ )
70+ elif failure_kind == "transport" :
71+ error = httpx .ConnectError ("connection failed" , request = request )
72+ else :
73+ error = OSError ("network unavailable" )
74+
75+ def fail_request () -> None :
76+ raise error
77+
78+ def fail_if_skipped (reason : str ) -> None :
79+ pytest .fail (f"hosted monitor converted { failure_kind } failure to skip: { reason } " )
80+
81+ monkeypatch .setattr (pytest , "skip" , fail_if_skipped )
82+ with pytest .raises (type (error )):
83+ _run_live_request (fail_request )
5484
5585
5686class TestDemoPricesContract :
5787 def test_prices_envelope_and_parsing (self , demo : DemoResource ) -> None :
5888 """DemoResource.prices() parses the real {status, data:{prices, meta}} envelope."""
59- try :
60- data = demo .prices ()
61- except (httpx .HTTPError , OSError ) as exc : # pragma: no cover - network
62- _skip_on_network_error (exc )
89+ data = _run_live_request (demo .prices )
6390
6491 # data is the unwrapped `data` payload from the envelope.
6592 assert "prices" in data
@@ -88,10 +115,7 @@ def test_prices_envelope_and_parsing(self, demo: DemoResource) -> None:
88115
89116 def test_prices_meta_demo_mode (self , demo : DemoResource ) -> None :
90117 """The demo prices meta block flags demo mode and lists free commodities."""
91- try :
92- data = demo .prices ()
93- except (httpx .HTTPError , OSError ) as exc : # pragma: no cover - network
94- _skip_on_network_error (exc )
118+ data = _run_live_request (demo .prices )
95119
96120 meta = data ["meta" ]
97121 assert meta .get ("demo_mode" ) is True
@@ -101,10 +125,7 @@ def test_prices_meta_demo_mode(self, demo: DemoResource) -> None:
101125class TestDemoCommoditiesContract :
102126 def test_commodities_envelope_and_count (self , demo : DemoResource ) -> None :
103127 """DemoResource.commodities() parses {status, data:{commodities, meta}}; 442 total."""
104- try :
105- data = demo .commodities ()
106- except (httpx .HTTPError , OSError ) as exc : # pragma: no cover - network
107- _skip_on_network_error (exc )
128+ data = _run_live_request (demo .commodities )
108129
109130 assert "commodities" in data
110131 assert "meta" in data
@@ -131,10 +152,7 @@ def test_commodities_envelope_and_count(self, demo: DemoResource) -> None:
131152
132153 def test_commodities_codes_filter (self , demo : DemoResource ) -> None :
133154 """Passing codes= returns only the requested free-tier prices."""
134- try :
135- data = demo .prices (codes = ["BRENT_CRUDE_USD" , "WTI_USD" ])
136- except (httpx .HTTPError , OSError ) as exc : # pragma: no cover - network
137- _skip_on_network_error (exc )
155+ data = _run_live_request (lambda : demo .prices (codes = ["BRENT_CRUDE_USD" , "WTI_USD" ]))
138156
139157 codes = {p ["code" ] for p in data ["prices" ]}
140158 assert codes == {"BRENT_CRUDE_USD" , "WTI_USD" }
0 commit comments