161 lines
4.7 KiB
Python
161 lines
4.7 KiB
Python
"""Behavioral coverage for the credential-free SCM broker client and I/O."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import pytest
|
|
|
|
from testing.tests.test_hermes_scm_broker_support import Response, _load
|
|
|
|
|
|
def test_client_default_opener_uses_deadline_bounded_exchange(monkeypatch):
|
|
client = _load("scm_broker_client")
|
|
sentinel = object()
|
|
monkeypatch.setattr(
|
|
client.deadline_http,
|
|
"open_bounded",
|
|
lambda request, *, maximum, timeout: (request, maximum, timeout, sentinel),
|
|
)
|
|
assert client._open("request", 4) == (
|
|
"request",
|
|
client.MAX_RESPONSE_BYTES,
|
|
4,
|
|
sentinel,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("endpoint", "payload", "match"),
|
|
[
|
|
("/v1/admin", {}, "outside"),
|
|
("/v1/metadata", {"path": "x" * (64 * 1024)}, "safe size"),
|
|
],
|
|
)
|
|
def test_client_rejects_unknown_or_oversized_operations(endpoint, payload, match):
|
|
client = _load("scm_broker_client")
|
|
with pytest.raises(client.PolicyError, match=match):
|
|
client.request(endpoint, payload, opener=lambda *_a, **_k: None)
|
|
|
|
|
|
class _GetCodeResponse(Response):
|
|
def __init__(self, body: bytes, *, content_type="application/json", code=200):
|
|
super().__init__(body, content_type=content_type)
|
|
del self.status
|
|
self._code = code
|
|
|
|
def getcode(self):
|
|
return self._code
|
|
|
|
|
|
def test_client_accepts_getcode_fallback_and_create_payload():
|
|
client = _load("scm_broker_client")
|
|
seen = []
|
|
|
|
def opener(request, timeout):
|
|
seen.append((request, timeout))
|
|
return _GetCodeResponse(b"{}")
|
|
|
|
result = client.create_draft(
|
|
"cassandra",
|
|
base="main",
|
|
head="feature/coverage",
|
|
head_sha="a" * 40,
|
|
title="WIP: Coverage",
|
|
body="Review evidence",
|
|
opener=opener,
|
|
)
|
|
assert result == b"{}"
|
|
assert json.loads(seen[0][0].data)["repo"] == "cassandra"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("response", "match"),
|
|
[
|
|
(_GetCodeResponse(b"{}", code=201), "HTTP status"),
|
|
(_GetCodeResponse(b"{}", content_type="text/plain"), "response type"),
|
|
(
|
|
Response(b"x" * (2 * 1024 * 1024 + 1), content_type="application/json"),
|
|
"safe size",
|
|
),
|
|
(Response(b"not-json", content_type="application/json"), "Expecting value"),
|
|
],
|
|
)
|
|
def test_client_rejects_bad_status_type_size_and_json(response, match):
|
|
client = _load("scm_broker_client")
|
|
expected = (
|
|
json.JSONDecodeError if match == "Expecting value" else client.PolicyError
|
|
)
|
|
with pytest.raises(expected, match=match):
|
|
client.request(
|
|
"/v1/metadata",
|
|
{"path": "/api/v1/repos/titan/cassandra"},
|
|
opener=lambda *_a, **_k: response,
|
|
)
|
|
|
|
|
|
class _Socket:
|
|
def __init__(self):
|
|
self.timeouts = []
|
|
|
|
def settimeout(self, value):
|
|
self.timeouts.append(value)
|
|
|
|
|
|
class _Stream:
|
|
def __init__(self, body: bytes, socket_shape: str = "direct"):
|
|
self.body = io.BytesIO(body)
|
|
self.socket = _Socket()
|
|
if socket_shape == "nested":
|
|
self.fp = type(
|
|
"FP", (), {"raw": type("Raw", (), {"_sock": self.socket})()}
|
|
)()
|
|
elif socket_shape == "direct":
|
|
self.fp = type("FP", (), {"raw": self.socket})()
|
|
else:
|
|
self.fp = None
|
|
|
|
def read(self, size):
|
|
return self.body.read(size)
|
|
|
|
|
|
def test_response_spool_handles_direct_and_absent_socket_shapes():
|
|
module = _load("scm_broker_io")
|
|
for shape in ("direct", "absent"):
|
|
stream = _Stream(b"safe", shape)
|
|
spool, length = module.spool_response(
|
|
stream,
|
|
16,
|
|
(b"forbidden",),
|
|
memory_limit=8,
|
|
chunk_size=2,
|
|
deadline_seconds=5,
|
|
)
|
|
try:
|
|
assert length == 4 and spool.read() == b"safe"
|
|
finally:
|
|
spool.close()
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ["deadline", "size", "credential", "read"])
|
|
def test_response_spool_closes_and_rejects_each_failure(monkeypatch, failure):
|
|
module = _load("scm_broker_io")
|
|
stream = _Stream(b"safe-forbidden-value")
|
|
kwargs = {
|
|
"maximum": 128,
|
|
"forbidden": (b"forbidden",),
|
|
"memory_limit": 8,
|
|
"chunk_size": 5,
|
|
"deadline_seconds": 5,
|
|
}
|
|
if failure == "deadline":
|
|
ticks = iter((10.0, 16.0))
|
|
monkeypatch.setattr(module.time, "monotonic", lambda: next(ticks))
|
|
elif failure == "size":
|
|
kwargs["maximum"] = 2
|
|
elif failure == "read":
|
|
stream.read = lambda _size: (_ for _ in ()).throw(OSError("read failed"))
|
|
expected = module.PolicyError if failure != "read" else OSError
|
|
with pytest.raises(expected):
|
|
module.spool_response(stream, **kwargs)
|