163 lines
6.4 KiB
Python
163 lines
6.4 KiB
Python
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
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / '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)]:
|
|
FakeHandler.status = status
|
|
self.assertEqual(self.request(method, status), {'ok': True})
|
|
|
|
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_reconstructed_method_only_heuristic_rejects_valid_patch_ack(self):
|
|
# Reconstructed historical predicate, NOT a prior runtime RED run.
|
|
old_expected = 201 if 'PATCH' == 'POST' else 200
|
|
self.assertNotEqual(201, old_expected)
|
|
FakeHandler.status = 201
|
|
self.assertEqual(self.request('PATCH', 201), {'ok': True})
|
|
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()
|