from __future__ import annotations import importlib.util import io import json import os from pathlib import Path import sys import pytest ROOT = Path(__file__).resolve().parents[2] PATH = ROOT / "ops/gitea-deploy-broker/hub_candidate_transport.py" SPEC = importlib.util.spec_from_file_location("hub_candidate_transport_test", PATH) transport = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = transport SPEC.loader.exec_module(transport) def _request(url: str, *, mode="json", maximum=None, output_fd=None): return { "url": url, "headers": { "Accept": "application/json" if mode == "json" else "application/zip", "Authorization": "token " + "x" * 40, "User-Agent": transport.USER_AGENT, }, "maxBytes": maximum if maximum is not None else ( transport.MAX_METADATA_BYTES if mode == "json" else transport.MAX_ARTIFACT_BYTES ), "outputFd": output_fd, } class Response: status = 200 def __init__(self, chunks): self.chunks = list(chunks) self.reads = [] def __enter__(self): return self def __exit__(self, *_args): return None def read1(self, limit): self.reads.append(limit) if not self.chunks: return b"" chunk = self.chunks.pop(0) return chunk[:limit] def read(self, _limit): pytest.fail("transport must use deadline-aware read1") class Opener: def __init__(self, response): self.response = response self.requests = [] self.timeouts = [] def open(self, request, timeout): self.requests.append(request) self.timeouts.append(timeout) return self.response @pytest.mark.parametrize("url", [ "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/branches/main", "https://gitea.example.com:443/api/v1/repos/jens/itworx_mcp_hub/actions/workflows", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/git/commits/" + "a" * 40, "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs?event=push&branch=main&head_sha=" + "a" * 40 + "&page=1&limit=100", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs/123/jobs?page=2&limit=100", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs/123/artifacts?name=hub-release-candidate-" + "a" * 40, ]) def test_fixed_json_routes_are_allowed(url): assert transport._url_is_allowed(url, "json") @pytest.mark.parametrize("url", [ "https://other.invalid/api/v1/repos/jens/itworx_mcp_hub/actions/workflows", "http://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/workflows", "https://gitea.example.com:444/api/v1/repos/jens/itworx_mcp_hub/actions/workflows", "https://user@gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/workflows", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs?event=push&event=push&branch=main&head_sha=" + "a" * 40 + "&page=1&limit=100", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs?event=pull_request&branch=main&head_sha=" + "a" * 40 + "&page=1&limit=100", "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/runs/99999999999999999999/jobs?page=1&limit=100", "https://gitea.example.com/api/v1/repos/jens/other/actions/workflows", ]) def test_nonfixed_json_routes_are_rejected(url): assert not transport._url_is_allowed(url, "json") def test_zip_route_is_fixed_and_has_bounded_artifact_id(): assert transport._url_is_allowed( "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/artifacts/12/zip", "zip" ) assert not transport._url_is_allowed( "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/artifacts/9223372036854775808/zip", "zip" ) assert not transport._url_is_allowed( "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/artifacts/12/zip?other=x", "zip" ) def test_ipc_requires_exact_fields_unique_json_and_16kib_bound(): valid = json.dumps(_request("https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/branches/main")).encode() assert transport._read_request(io.BytesIO(valid))["maxBytes"] == transport.MAX_METADATA_BYTES for invalid in ( b'{"url":"x","url":"y"}', valid[:-1] + b',"unexpected":true}', b'{"a":NaN}', b"x" * (transport.MAX_INPUT_BYTES + 1), ): with pytest.raises(Exception): transport._read_request(io.BytesIO(invalid)) @pytest.mark.parametrize("patch", [ {"headers": {"Accept": "application/json", "Authorization": "token x\r\nInjected: yes", "User-Agent": transport.USER_AGENT}}, {"headers": {"Accept": "application/json", "Authorization": "token " + "x" * 40, "User-Agent": transport.USER_AGENT, "Cookie": "x"}}, {"maxBytes": True}, {"outputFd": 3}, ]) def test_json_request_rejects_header_injection_extra_header_and_bad_limits(patch): value = _request("https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/branches/main") value.update(patch) with pytest.raises(transport.TransportError): transport._validate_request(value, "json") def test_json_mode_emits_only_raw_bounded_response_bytes(): body = b'{"workflows":[]}' opener = Opener(Response([body])) value = _request("https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/workflows") result = transport.execute(value, "json", opener_factory=lambda mode: opener) assert result == body assert opener.timeouts == [10] headers = {key.lower(): val for key, val in opener.requests[0].header_items()} assert headers == { "accept": "application/json", "authorization": "token " + "x" * 40, "user-agent": transport.USER_AGENT, } def test_zip_mode_streams_only_to_inherited_fd_and_reports_byte_count(): read_fd, write_fd = os.pipe() try: response = Response([b"zip-", b"bytes"]) opener = Opener(response) value = _request( "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/artifacts/12/zip", mode="zip", output_fd=write_fd, ) assert transport.execute(value, "zip", opener_factory=lambda mode: opener) == 9 os.close(write_fd) write_fd = -1 assert os.read(read_fd, 32) == b"zip-bytes" assert opener.timeouts == [60] finally: if write_fd >= 0: os.close(write_fd) os.close(read_fd) def test_redirect_strips_authorization_and_rejects_other_hosts(): request = transport.Request( "https://gitea.example.com/api/v1/artifact", headers={"Accept": "application/zip", "Authorization": "token secret", "User-Agent": transport.USER_AGENT}, ) handler = transport._SameHostArtifactRedirect() redirected = handler.redirect_request( request, None, 302, "", {}, "https://gitea.example.com/attachments/opaque?signature=opaque" ) assert redirected is not None assert {key.lower(): value for key, value in redirected.header_items()} == { "accept": "application/zip", "user-agent": transport.USER_AGENT, } for url in ( "http://gitea.example.com/attachments/file", "https://other.invalid/attachments/file", "https://gitea.example.com:444/attachments/file", "https://user@gitea.example.com/attachments/file", "https://gitea.example.com/attachments/file#fragment", ): assert handler.redirect_request(request, None, 302, "", {}, url) is None def test_deadline_stops_a_continuous_read1_trickle(): clock = [0.0] class Trickle(Response): def read1(self, _limit): clock[0] += 2 return b" " value = _request("https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/branches/main") with pytest.raises(transport.TransportError, match="deadline"): transport.execute(value, "json", opener_factory=lambda _mode: Opener(Trickle([])), monotonic=lambda: clock[0]) def test_cli_errors_are_generic_and_zip_marker_is_compact(): errors = io.BytesIO() stdout = io.BytesIO() assert transport.main(["wrong"], stdin=io.BytesIO(b""), stdout=stdout, stderr=errors) == 1 assert errors.getvalue() == transport._GENERIC_ERROR assert b"wrong" not in errors.getvalue() read_fd, write_fd = os.pipe() try: value = _request( "https://gitea.example.com/api/v1/repos/jens/itworx_mcp_hub/actions/artifacts/12/zip", mode="zip", output_fd=write_fd, ) response = Response([b"zip"]) payload = json.dumps(value, separators=(",", ":")).encode() stdout = io.BytesIO() errors = io.BytesIO() result = transport.main(["zip"], stdin=io.BytesIO(payload), stdout=stdout, stderr=errors, opener_factory=lambda _mode: Opener(response)) assert result == 0 assert stdout.getvalue() == b'{"bytes":3}\n' assert errors.getvalue() == b"" os.close(write_fd) write_fd = -1 assert os.read(read_fd, 32) == b"zip" finally: if write_fd >= 0: os.close(write_fd) os.close(read_fd)