"""Exercise the real HTTP boundary with synthetic credentials and local servers.""" from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from threading import Thread from types import SimpleNamespace import pytest from secrets_engine import approval_consume from secrets_engine.decision_check import check_decision from secrets_engine.errors import DecisionError @contextmanager def server(*, status=200, body=b'{}', location=None): seen = [] class Handler(BaseHTTPRequestHandler): def handle_request(self): # Record only a boolean about our synthetic credential. seen.append(self.headers.get('Authorization') == 'Bearer synthetic-pdp-credential') self.rfile.read(int(self.headers.get('Content-Length', '0'))) self.send_response(status) if location: self.send_header('Location', location) self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) do_POST = do_GET = handle_request def log_message(self, *_args): pass httpd = ThreadingHTTPServer(('127.0.0.1', 0), Handler) thread = Thread(target=httpd.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True) thread.start() try: yield f'http://127.0.0.1:{httpd.server_port}', seen finally: httpd.shutdown() httpd.server_close() thread.join(timeout=2) @pytest.fixture def credential(tmp_path): path = tmp_path / 'synthetic.token' path.write_text('synthetic-pdp-credential') path.chmod(0o600) return path @pytest.fixture(autouse=True) def clear_proxies(monkeypatch): # Tests must not contact proxies configured on the developer's machine. import os import urllib.request monkeypatch.setattr(urllib.request, "_opener", None) for name in os.environ: if name.lower().endswith('_proxy'): monkeypatch.delenv(name) @pytest.mark.parametrize('status', [301, 302, 303, 307, 308]) def test_redirect_never_contacts_another_origin(credential, status): with server() as (destination, received): with server(status=status, location=destination + '/sink') as (source, sent): with pytest.raises(DecisionError): check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={}) assert sent == [True] assert received == [] def test_environment_proxy_never_receives_request(credential, monkeypatch): with server() as (proxy, proxied): with server() as (source, direct): monkeypatch.setenv('http_proxy', proxy) monkeypatch.setenv('no_proxy', '') assert check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={}) == {} assert direct == [True] assert proxied == [] from tests.signature_support import signature_tools, admit_tools def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch, signature_tools): observation = SimpleNamespace(request={}) monkeypatch.setattr(approval_consume, 'resolve_approval_observation', lambda *a, **kw: observation) with server() as (destination, received): with server(status=302, location=destination + '/sink') as (source, sent): cfg = SimpleNamespace(pdp_url=source, pdp_token_file=credential, authorization_policy_package='test', authorization_policy_version='v1') admit_tools(cfg, credential.parent, signature_tools) with pytest.raises(DecisionError): approval_consume.authorize_action(cfg, object(), 'rotate', None) assert sent == [True] assert received == [] @pytest.mark.parametrize('url', [ 'http://localhost:8080', 'http://localhost.:8080', 'http://user:synthetic-password@127.0.0.1:8080', 'http://127.0.0.1:8080?query=value', 'http://127.0.0.1:8080#fragment', 'http://127.0.0.1:8080/path', 'http://127.0.0.1:0', 'http://127.0.0.1:wrong', 'http://[::1', 'http://127.0.0.1:65536', 'http://127.0.0.1:\n8080', ' http://127.0.0.1:8080', 'file://127.0.0.1', ]) def test_invalid_address_refused_before_credential_read(url, tmp_path): with pytest.raises(DecisionError, match='URL'): check_decision(allow_unsigned_test_fixture=True, base_url=url, token_file=tmp_path / 'missing', request={}) @pytest.mark.parametrize('body,reason', [ (b'{}' + b' ' * (512 * 1024), 'too large'), (b'\xff', 'non-JSON'), (b'[]', 'non-object'), ], ids=['oversized', 'invalid-utf8', 'array']) def test_response_limits_and_encoding_fail_closed(credential, body, reason): with server(body=body) as (source, _): with pytest.raises(DecisionError, match=reason): check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={})