"""Integration tests against live internet services for hxprobe. Run with: make hx-test-integration cd hxprobe && uv run pytest tests -m integration -v Marked with `pytest.mark.integration` (see module-level `pytestmark` below) so it's excluded from the default `hx-test`/`make test` gate, which runs `pytest -m "not integration"` — these hit the real internet. The point of this file is specifically hxprobe's headline capabilities: real HTTP/2 negotiation and redirect-following. """ import io import socket import unittest import pytest from hxprobe.cli import EXIT_DNS, EXIT_OK, run from hxprobe.probe import Options, measure pytestmark = pytest.mark.integration def _online() -> bool: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(3) s.connect(("8.8.8.8", 53)) s.close() return True except OSError: return False _NEEDS_NET = unittest.skipUnless(_online(), "no internet connectivity") def _run(args: list[str]) -> tuple[int, str, str]: out, err = io.StringIO(), io.StringIO() code = run(args, out, err) return code, out.getvalue(), err.getvalue() @_NEEDS_NET class TestHTTP2Negotiation(unittest.TestCase): """The headline Go-parity feature: real ALPN HTTP/2, not forced HTTP/1.1.""" def test_negotiates_h2_against_cloudflare(self): r = measure("https://example.com", Options(verbose=True)) self.assertIsNone(r.err, r.err) self.assertEqual(r.detail.http_version, "HTTP/2") def test_no_http2_flag_forces_http1(self): code, out, _ = _run(["--no-http2", "-v", "https://example.com"]) self.assertEqual(code, EXIT_OK) self.assertIn("HTTP/1.1", out) self.assertNotIn("HTTP/2", out) @_NEEDS_NET class TestRedirectsLive(unittest.TestCase): """The other headline Go-parity feature: redirects followed by default.""" def test_follows_http_to_https_redirect(self): code, out, _ = _run(["-v", "http://github.com"]) self.assertEqual(code, EXIT_OK) self.assertIn("200", out) self.assertIn("redirect", out) def test_no_follow_redirects_reports_redirect_status(self): code, out, _ = _run(["--no-follow-redirects", "http://github.com"]) self.assertEqual(code, EXIT_OK) self.assertNotIn("redirect_count", out) # text mode never shows the raw key self.assertTrue(any(code_str in out for code_str in ("301", "302", "307", "308"))) @_NEEDS_NET class TestSuccess(unittest.TestCase): def test_https_all_phases_present(self): r = measure("https://example.com") self.assertIsNone(r.err, r.err) self.assertEqual(r.status_code, 200) self.assertTrue(r.dns.present) self.assertTrue(r.connect.present) self.assertTrue(r.tls.present) self.assertTrue(r.ttfb.present) self.assertTrue(r.transfer.present) self.assertTrue(r.total.present) def test_http_no_tls_phase(self): r = measure("http://example.com") self.assertIsNone(r.err, r.err) self.assertFalse(r.tls.present) def test_timings_are_positive(self): r = measure("https://example.com") for attr in ("dns", "connect", "tls", "ttfb", "transfer", "total"): ph = getattr(r, attr) if ph.present: self.assertGreater(ph.ms, 0, f"{attr}.ms should be > 0") @_NEEDS_NET class TestDNSFailure(unittest.TestCase): def test_invalid_tld_phase(self): r = measure("http://no.such.host.invalid") self.assertEqual(r.fail_phase, "dns") self.assertIsNotNone(r.err) self.assertFalse(r.dns.present) self.assertFalse(r.connect.present) self.assertTrue(r.total.present) def test_cli_dns_failure_exit_code(self): code, out, _ = _run(["http://no.such.host.invalid"]) self.assertEqual(code, EXIT_DNS) if __name__ == "__main__": unittest.main()