import json import threading import unittest import sys from pathlib import Path from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.error import HTTPError from urllib.request import Request, urlopen from harness.tests._support.paths import ROOT sys.path.insert(0, str(ROOT / 'harness/agents/impl/skills/gitea-issue-implementation/scripts')) import delivery_response as dr def make_handler(): class FakeHandler(BaseHTTPRequestHandler): status = 200 body = b'{"ok":true}' calls = 0 def do_GET(self): self.respond() def do_POST(self): self.respond() def do_PATCH(self): self.respond() def respond(self): type(self).calls += 1 self.send_response(type(self).status) self.end_headers() self.wfile.write(type(self).body) def log_message(self, format, *args): pass return FakeHandler FakeHandler = make_handler() class DeliveryResponseTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = HTTPServer(('127.0.0.1', 0), FakeHandler) cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True) cls.thread.start() cls.url = 'http://127.0.0.1:%d/' % cls.server.server_port @classmethod def tearDownClass(cls): cls.server.shutdown() cls.server.server_close() def setUp(self): FakeHandler.calls = 0 FakeHandler.body = b'{"ok":true}' FakeHandler.status = 200 def request(self, method, expected): def send(): req = Request(self.url, method=method) try: with urlopen(req, timeout=2) as response: return response.status, response.read(1024 * 1024 + 1) except HTTPError as e: return e.code, e.read(1024 * 1024 + 1) return dr.deliver_once(send, expected_status=expected) def test_endpoint_specific_success_statuses(self): for method, status in [('GET', 200), ('POST', 201), ('PATCH', 201)]: with self.subTest(method=method, status=status): FakeHandler.status = status calls_before = FakeHandler.calls self.assertEqual(self.request(method, status), {'ok': True}) self.assertEqual(FakeHandler.calls - calls_before, 1) def test_rejects_invalid_expected_status_before_send(self): calls = [] for invalid in (None, True, 0, 700, '201'): with self.subTest(invalid=invalid): with self.assertRaises(dr.InvalidExpectationError): dr.deliver_once(lambda: calls.append(1), expected_status=invalid) self.assertEqual(calls, []) def test_unexpected_success_status_uncertain_not_heuristic(self): FakeHandler.status = 202 with self.assertRaises(dr.AcknowledgementUncertainError): self.request('POST', 201) self.assertEqual(FakeHandler.calls, 1) def test_failure_status_cannot_be_configured_as_success(self): calls = [] for status in (100, 199, 300, 400, 500, 599): with self.subTest(status=status), self.assertRaises(dr.InvalidExpectationError): dr.deliver_once(lambda: calls.append(1) or (status, b'{}'), expected_status=status) self.assertEqual(calls, []) def test_raised_auth_or_redirect_error_is_rejected_not_uncertain(self): for status in (300, 302, 304, 399, 401, 403): calls = [] def send(): calls.append(1) raise HTTPError('http://example.invalid/private', status, 'sensitive detail', {}, None) with self.subTest(status=status): with self.assertRaises(dr.RejectedResponseError) as caught: dr.deliver_once(send, expected_status=201) self.assertEqual(calls, [1]) self.assertNotIn('sensitive detail', str(caught.exception)) def test_sender_explicit_rejection_is_not_reclassified_as_uncertain(self): def send(): raise dr.RejectedResponseError('sensitive detail') with self.assertRaises(dr.RejectedResponseError) as caught: dr.deliver_once(send, expected_status=201) self.assertNotIn('sensitive detail', str(caught.exception)) def test_tls_certificate_rejection_is_not_reclassified_as_uncertain(self): import ssl from urllib.error import URLError for error in (ssl.SSLCertVerificationError('sensitive detail'), URLError(ssl.SSLCertVerificationError('sensitive detail'))): def send(): raise error with self.subTest(error=type(error).__name__), self.assertRaises(dr.RejectedResponseError): dr.deliver_once(send, expected_status=201) def test_auth_and_redirect_rejected(self): for status in (401, 403, 302): with self.subTest(status=status): FakeHandler.status = status with self.assertRaises(dr.RejectedResponseError): self.request('POST', 201) def test_bad_json_shape_and_size_are_uncertain(self): for body in (b'{', b'[]', b'null', b'{"x":NaN}', b'"x"', b' ' * (1024 * 1024 + 1)): with self.subTest(body=body[:12]): FakeHandler.body = body FakeHandler.status = 201 with self.assertRaises(dr.AcknowledgementUncertainError): self.request('POST', 201) def test_transport_error_once_and_sanitized_uncertainty(self): calls = [] def send(): calls.append(1) raise OSError('secret token should not leak') with self.assertRaises(dr.AcknowledgementUncertainError) as caught: dr.deliver_once(send, expected_status=201) self.assertEqual(len(calls), 1) self.assertNotIn('secret token', str(caught.exception)) self.assertIn('reconcil', str(caught.exception).lower()) def test_control_exceptions_propagate(self): for exc in (KeyboardInterrupt(), SystemExit()): with self.subTest(exc=type(exc).__name__): with self.assertRaises(type(exc)): dr.deliver_once(lambda: (_ for _ in ()).throw(exc), expected_status=200) if __name__ == '__main__': unittest.main()