Files
Unraid-AppOps-Gateway-Public/backend/tests/test_hub_candidate_transport.py
T

237 lines
9.1 KiB
Python

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)