"""Unit tests for runner/harness/http.py (Phase 2 ยง4.2 vendored helpers). Pure-Python: a tiny in-process http.server on 127.0.0.1 serves rigged responses so we can prove the parsing + retry semantics deterministically. No network egress. """ from __future__ import annotations import http.server import json import os import socket import socketserver import sys import threading import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) from harness import http as harness_http # noqa: E402 class _State: counter = 0 healthy_after = 0 # set by tests class _Handler(http.server.BaseHTTPRequestHandler): def log_message(self, *_a, **_k): # silence the test log pass def _respond(self, code: int, body: bytes, content_type: str = "application/json"): self.send_response(code) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): if self.path == "/ok-json": self._respond(200, b'{"hello": "world"}') return if self.path == "/notfound": self._respond(404, b'{"error": "nope"}') return if self.path == "/text": self._respond(200, b"plain", content_type="text/plain") return if self.path == "/flaky": _State.counter += 1 if _State.counter <= _State.healthy_after: self._respond(503, b'{"err": "unhealthy"}') else: self._respond(200, b'{"ok": true}') return if self.path.startswith("/echo-headers"): self._respond(200, json.dumps(dict(self.headers)).encode()) return self._respond(500, b'{"err": "unknown"}') def do_POST(self): length = int(self.headers.get("Content-Length", "0")) body = self.rfile.read(length) if length else b"" ctype = self.headers.get("Content-Type", "") if self.path == "/echo-json": try: parsed = json.loads(body) if body else None except Exception: # noqa: BLE001 parsed = None self._respond(200, json.dumps({"content_type": ctype, "body": parsed}).encode()) return if self.path == "/echo-form": self._respond( 200, json.dumps({"content_type": ctype, "raw": body.decode()}).encode(), ) return self._respond(500, b'{"err": "unknown"}') def _free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] @pytest.fixture(scope="module") def server(): port = _free_port() srv = socketserver.TCPServer(("127.0.0.1", port), _Handler) thread = threading.Thread(target=srv.serve_forever, daemon=True) thread.start() yield f"http://127.0.0.1:{port}" srv.shutdown() srv.server_close() @pytest.fixture(autouse=True) def _reset_state(): _State.counter = 0 _State.healthy_after = 0 def test_http_get_parses_json(server): status, body = harness_http.http_get(f"{server}/ok-json") assert status == 200 assert body == {"hello": "world"} def test_http_get_returns_status_on_4xx_with_body(server): status, body = harness_http.http_get(f"{server}/notfound") assert status == 404 assert body == {"error": "nope"} def test_http_get_handles_non_json_body(server): status, body = harness_http.http_get(f"{server}/text") assert status == 200 assert body is None # not JSON => None def test_http_get_transport_failure_returns_status_zero(): status, body = harness_http.http_get("http://127.0.0.1:1/nope", timeout=1) assert status == 0 assert body is None def test_http_get_sends_headers(server): status, body = harness_http.http_get(f"{server}/echo-headers", headers={"X-Auth": "token-123"}) assert status == 200 assert body["X-Auth"] == "token-123" def test_http_post_json(server): status, body = harness_http.http_post(f"{server}/echo-json", data={"k": "v"}) assert status == 200 assert body["content_type"] == "application/json" assert body["body"] == {"k": "v"} def test_http_post_form(server): status, body = harness_http.http_post( f"{server}/echo-form", data={"grant_type": "password", "username": "alice"}, content_type="application/x-www-form-urlencoded", ) assert status == 200 assert body["content_type"] == "application/x-www-form-urlencoded" assert "grant_type=password" in body["raw"] assert "username=alice" in body["raw"] def test_retry_http_get_converges(server): _State.healthy_after = 2 # fail twice, then succeed status, body = harness_http.retry_http_get( f"{server}/flaky", expect_status=200, max_wait=5, interval=1 ) assert status == 200 assert body == {"ok": True} assert _State.counter == 3 def test_retry_http_get_times_out(): # nonexistent host; expect a RuntimeError after the short max_wait with pytest.raises(RuntimeError, match="Did not converge"): harness_http.retry_http_get( "http://127.0.0.1:1/never", expect_status=200, max_wait=2, interval=1, timeout=1, ) def test_wait_for_http_returns_status(server): status = harness_http.wait_for_http(f"{server}/ok-json", "ok", max_wait=2, interval=1) assert status == 200 def test_assert_converges_returns_value(): n = [0] def fn(): n[0] += 1 return "done" if n[0] >= 3 else None assert harness_http.assert_converges(fn, "fn", max_wait=2, interval=0) == "done"