2026-08-16 20:41:14 -03:00
|
|
|
"""Behavioral branch coverage for the SCM broker HTTP server primitives."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from testing.tests.test_hermes_scm_broker_support import _load
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Connection:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.timeouts = []
|
|
|
|
|
|
|
|
|
|
def settimeout(self, value):
|
|
|
|
|
self.timeouts.append(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FlushBuffer(io.BytesIO):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.flushes = 0
|
|
|
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
|
self.flushes += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_absolute_reader_supports_idle_limit_newline_eof_and_attribute_delegation():
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
stream = io.BytesIO(b"first\nsecond")
|
|
|
|
|
reader = module._AbsoluteDeadlineReader(stream, _Connection())
|
|
|
|
|
assert reader.readline(3) == b"fir"
|
|
|
|
|
reader.begin(5)
|
|
|
|
|
assert reader.readline() == b"st\n"
|
|
|
|
|
assert reader.readline() == b"second"
|
|
|
|
|
assert reader.readline() == b""
|
|
|
|
|
assert reader.closed is False
|
|
|
|
|
reader.end()
|
|
|
|
|
assert reader.readline() == b""
|
|
|
|
|
|
|
|
|
|
|
2026-09-13 20:07:11 -05:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("phase", "detail", "expected"),
|
|
|
|
|
[
|
|
|
|
|
("pack", "Git command does not match its task grant", "grant-command"),
|
|
|
|
|
("pack", "Git update does not prove fast-forward ancestry", "ancestry"),
|
|
|
|
|
("pack", "Git delta base is outside the pack; push full packs", "thin-pack"),
|
|
|
|
|
("pack", "Git push contains runtime credential material", "credential"),
|
|
|
|
|
("pack", "Git push contains credential-shaped content", "content"),
|
|
|
|
|
("pack", "Git receive-pack command framing is invalid", "framing"),
|
|
|
|
|
("pack", "Git push is limited to namespaced feature branches", "ref"),
|
|
|
|
|
("control", "arbitrary policy detail", "policy"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_rejection_log_uses_fixed_pack_categories_without_error_text(
|
|
|
|
|
monkeypatch, phase, detail, expected
|
|
|
|
|
):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
policy = type("PolicyError", (Exception,), {})
|
|
|
|
|
observed = []
|
|
|
|
|
monkeypatch.setattr(module.logging, "warning", lambda message, *args: observed.append((message, args)))
|
|
|
|
|
|
|
|
|
|
module.log_rejection(phase, policy(detail))
|
|
|
|
|
|
|
|
|
|
assert observed == [("scm_rejected phase=%s category=%s", (phase, expected))]
|
|
|
|
|
|
|
|
|
|
|
2026-08-16 20:41:14 -03:00
|
|
|
def _handler_type(module):
|
|
|
|
|
class StubHandler:
|
|
|
|
|
request_version = "HTTP/1.1"
|
|
|
|
|
command = "GET"
|
|
|
|
|
|
|
|
|
|
def setup(self):
|
|
|
|
|
self.connection = _Connection()
|
|
|
|
|
self.rfile = io.BytesIO(b"")
|
|
|
|
|
self.wfile = _FlushBuffer()
|
|
|
|
|
|
|
|
|
|
def parse_request(self):
|
|
|
|
|
return self.parse_result
|
|
|
|
|
|
|
|
|
|
def send_error(self, code, *_args):
|
|
|
|
|
self.errors.append(code)
|
|
|
|
|
|
|
|
|
|
def do_GET(self):
|
|
|
|
|
self.calls.append("GET")
|
|
|
|
|
|
|
|
|
|
return type(
|
|
|
|
|
"DeadlineHandler", (module.AbsoluteHeaderDeadlineMixin, StubHandler), {}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handler(module, raw: bytes, *, parse=True):
|
|
|
|
|
handler_type = _handler_type(module)
|
|
|
|
|
handler = object.__new__(handler_type)
|
|
|
|
|
handler.setup()
|
|
|
|
|
handler._header_reader._stream = io.BytesIO(raw)
|
|
|
|
|
handler.parse_result = parse
|
|
|
|
|
handler.errors = []
|
|
|
|
|
handler.calls = []
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_mixin_setup_and_known_request_dispatch():
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
handler = _handler(module, b"GET /healthz HTTP/1.1\r\n")
|
|
|
|
|
handler.handle_one_request()
|
|
|
|
|
assert handler.calls == ["GET"]
|
|
|
|
|
assert handler.wfile.flushes == 1
|
|
|
|
|
assert handler._header_reader._deadline is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
("raw", "parse", "command", "expected_error", "closed"),
|
|
|
|
|
[
|
|
|
|
|
(b"x" * 65537, True, "GET", 414, False),
|
|
|
|
|
(b"", True, "GET", None, True),
|
|
|
|
|
(b"GET / HTTP/1.1\r\n", False, "GET", None, False),
|
|
|
|
|
(b"TRACE / HTTP/1.1\r\n", True, "TRACE", 501, False),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_header_mixin_rejects_long_empty_unparsed_and_unknown_requests(
|
|
|
|
|
raw, parse, command, expected_error, closed
|
|
|
|
|
):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
handler = _handler(module, raw, parse=parse)
|
|
|
|
|
handler.command = command
|
|
|
|
|
handler.close_connection = False
|
|
|
|
|
handler.handle_one_request()
|
|
|
|
|
assert handler.errors == ([] if expected_error is None else [expected_error])
|
|
|
|
|
assert handler.close_connection is closed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_mixin_closes_on_absolute_timeout(monkeypatch):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
handler = _handler(module, b"GET / HTTP/1.1\r\n")
|
|
|
|
|
handler.close_connection = False
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
handler._header_reader,
|
|
|
|
|
"readline",
|
|
|
|
|
lambda *_a, **_k: (_ for _ in ()).throw(TimeoutError()),
|
|
|
|
|
)
|
|
|
|
|
handler.handle_one_request()
|
|
|
|
|
assert handler.close_connection is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Slots:
|
|
|
|
|
def __init__(self, available=True):
|
|
|
|
|
self.available = available
|
|
|
|
|
self.releases = 0
|
|
|
|
|
|
|
|
|
|
def acquire(self, *, blocking):
|
|
|
|
|
assert blocking is False
|
|
|
|
|
return self.available
|
|
|
|
|
|
|
|
|
|
def release(self):
|
|
|
|
|
self.releases += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _server_shell(module, available=True):
|
|
|
|
|
server = object.__new__(module.BoundedThreadingHTTPServer)
|
|
|
|
|
server._slots = _Slots(available)
|
|
|
|
|
server.shutdowns = []
|
|
|
|
|
server.shutdown_request = server.shutdowns.append
|
|
|
|
|
return server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bounded_server_delegates_accepted_request(monkeypatch):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
server = _server_shell(module)
|
|
|
|
|
calls = []
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module.ThreadingHTTPServer,
|
|
|
|
|
"process_request",
|
|
|
|
|
lambda self, request, address: calls.append((request, address)),
|
|
|
|
|
)
|
|
|
|
|
server.process_request("socket", ("127.0.0.1", 1))
|
|
|
|
|
assert calls == [("socket", ("127.0.0.1", 1))]
|
|
|
|
|
assert server._slots.releases == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bounded_server_releases_on_dispatch_failure(monkeypatch):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
server = _server_shell(module)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module.ThreadingHTTPServer,
|
|
|
|
|
"process_request",
|
|
|
|
|
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("dispatch")),
|
|
|
|
|
)
|
|
|
|
|
with pytest.raises(RuntimeError, match="dispatch"):
|
|
|
|
|
server.process_request("socket", ("127.0.0.1", 1))
|
|
|
|
|
assert server._slots.releases == 1
|
|
|
|
|
assert server.shutdowns == ["socket"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bounded_server_thread_always_releases(monkeypatch):
|
|
|
|
|
module = _load("scm_broker_server")
|
|
|
|
|
server = _server_shell(module)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module.ThreadingHTTPServer,
|
|
|
|
|
"process_request_thread",
|
|
|
|
|
lambda *_a, **_k: (_ for _ in ()).throw(RuntimeError("handler")),
|
|
|
|
|
)
|
|
|
|
|
with pytest.raises(RuntimeError, match="handler"):
|
|
|
|
|
server.process_request_thread("socket", ("127.0.0.1", 1))
|
|
|
|
|
assert server._slots.releases == 1
|