the-custodian/docs/changes/CUST-WP-0080/test_forge_routes_safety.py
codex 81f2f59649 security: verify authenticated Forgejo routes live (FORGE6-ROUTES-001 r3)
Revision 2 live run passed 24/25; the clean merge hit Forgejo's 405 during
the mergeability recheck. Revision 3 waits for mergeable, retries only 405
(max 3) and records status codes. 7 boundary tests, isolated rehearsal
(27 checks) and the attended live run (26/26, verified, cleanup confirmed)
pass. Revision 2 proposal and receipts retained.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 02:33:46 +02:00

142 lines
7 KiB
Python

"""Boundary regressions for receipt-scoped cleanup and credential forwarding."""
import importlib.util
import contextlib
import hashlib
import io
import json
from pathlib import Path
import tempfile
import threading
import unittest
from unittest import mock
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
spec = importlib.util.spec_from_file_location('routes', Path(__file__).with_name('accept_forge_routes.py'))
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
class CleanupTests(unittest.TestCase):
def test_replaced_or_changed_repository_is_not_deleted(self):
for change in ({'id': 18}, {'private': False}, {'description': 'changed'},
{'owner': {'login': 'someone-else'}}):
with self.subTest(change=change), tempfile.TemporaryDirectory() as directory:
name = 'custodian-routes-' + 'a' * 24
record = dict(id=17, name=name, private=True, description=name, owner={'login': 'tegwick'})
record.update(change)
class Backend:
user = 'tegwick'
scope = 'test'
def api(self, method, *args):
self_outer.assertEqual(method, 'GET', 'must not delete changed ownership')
return 200, json.dumps(record).encode()
self_outer = self
suite = m.Suite(Backend(), Path(directory) / 'receipt.json', name)
suite.created_id = 17
with self.assertRaises(ValueError):
suite.close()
def test_ambiguous_creation_never_deletes_by_name(self):
class Backend:
user = 'tegwick'
scope = 'test'
def api(self, *args):
raise AssertionError('unowned creation must not issue API calls')
with tempfile.TemporaryDirectory() as directory:
suite = m.Suite(Backend(), Path(directory) / 'receipt.json', 'custodian-routes-' + 'a' * 24)
suite.evidence['creation_attempted'] = True
suite.close()
self.assertTrue(suite.evidence['creation_outcome_requires_reconciliation'])
self.assertEqual(suite.evidence['result'], 'unverified')
def test_generic_error_is_not_detection_evidence(self):
self.assertFalse(m.detected(b'{"message":"internal error"}'))
self.assertFalse(m.detected(b'{"status":"unverified"}'))
class MergeRetryTests(unittest.TestCase):
def suite(self, directory, merges):
calls = []
class Backend:
user = 'tegwick'
scope = 'test'
def api(self, method, path, data=None, authenticated=True):
calls.append(method)
if method == 'GET':
return 200, json.dumps({'mergeable': True}).encode()
return merges.pop(0), b'{}'
suite = m.Suite(Backend(), Path(directory) / 'receipt.json', 'custodian-routes-' + 'a' * 24)
return suite, calls
def test_recheck_405_is_waited_out_and_retried(self):
with tempfile.TemporaryDirectory() as directory, mock.patch.object(m.time, 'sleep'):
suite, calls = self.suite(directory, [405, 200])
self.assertEqual(suite.clean_merge('/pulls/1'), 200)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [405, 200])
self.assertEqual(calls, ['GET', 'POST', 'GET', 'POST'])
def test_other_refusals_are_not_retried_and_retries_are_bounded(self):
with tempfile.TemporaryDirectory() as directory, mock.patch.object(m.time, 'sleep'):
suite, _ = self.suite(directory, [409])
self.assertEqual(suite.clean_merge('/pulls/1'), 409)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [409])
suite, _ = self.suite(directory, [405, 405, 405, 200])
self.assertEqual(suite.clean_merge('/pulls/1'), 405)
self.assertEqual(suite.evidence['clean_merge_status_codes'], [405, 405, 405])
class TransportTests(unittest.TestCase):
def test_http_redirect_does_not_forward_injected_credential(self):
hits = []
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
hits.append(self.path)
self.send_response(302)
self.send_header('Location', '/receiver')
self.end_headers()
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
worker = threading.Thread(target=server.serve_forever, daemon=True); worker.start()
try:
backend = object.__new__(m.Production)
backend.origin = 'http://127.0.0.1:' + str(server.server_port)
backend.token = 'unbound-test-value'
backend.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), m.NoRedirect())
code, _ = backend.api('GET', '/api/v1/user')
self.assertEqual(code, 302)
self.assertEqual(hits, ['/api/v1/user'])
finally:
server.shutdown(); server.server_close(); worker.join()
def test_handoff_is_silent_and_rejects_changed_source_before_fetch(self):
spec = importlib.util.spec_from_file_location('handoff', Path(__file__).with_name('run_forge_routes.py'))
wrapper = importlib.util.module_from_spec(spec); spec.loader.exec_module(wrapper)
for changed in (False, True):
with self.subTest(changed=changed), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
proposal = {'repository_name': 'custodian-routes-' + 'a' * 24, 'source_sha256': {
n: hashlib.sha256((wrapper.HERE / n).read_bytes()).hexdigest()
for n in ('run_forge_routes.py', 'accept_forge_routes.py')}}
if changed:
proposal['source_sha256']['accept_forge_routes.py'] = '0' * 64
(root / 'proposal.json').write_text(json.dumps(proposal))
out, err = io.StringIO(), io.StringIO()
with mock.patch('sys.argv', ['handoff', '--proposal', str(root / 'proposal.json'),
'--receipt', str(root / 'result.json')]), mock.patch.object(
wrapper.subprocess, 'run', return_value=mock.Mock(returncode=5,
stdout=b'UNBOUND-CREDENTIAL-MARKER', stderr=b'UNBOUND-CREDENTIAL-MARKER')) as child:
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
result = wrapper.main()
self.assertEqual(out.getvalue() + err.getvalue(), '')
if changed:
child.assert_not_called()
self.assertEqual(result, 2)
else:
self.assertEqual(result, 5)
self.assertTrue(child.call_args.kwargs['capture_output'])
self.assertNotIn('UNBOUND-CREDENTIAL-MARKER', (root / 'result.transport.json').read_text())
if __name__ == '__main__':
unittest.main()