mirror of
https://github.com/openhop-dev/openhop_repeater.git
synced 2026-10-23 13:32:50 +00:00
174 lines
5.5 KiB
Python
174 lines
5.5 KiB
Python
"""Offline resource-boundary regressions for both plugin download paths."""
|
|
|
|
import hashlib
|
|
import http.client
|
|
import io
|
|
import socket
|
|
import threading
|
|
import time
|
|
from contextlib import contextmanager
|
|
|
|
import pytest
|
|
|
|
from repeater.plugins import catalogue, github_releases
|
|
|
|
WHEEL = b"test wheel"
|
|
URL = "https://github.com/org/repo/releases/download/v1.0/pkg-1.0-py3-none-any.whl"
|
|
|
|
|
|
def operation(kind, opener, tmp_path):
|
|
if kind.startswith("catalogue"):
|
|
client = catalogue.CatalogueClient(opener=opener)
|
|
if kind.endswith("json"):
|
|
return client.fetch()
|
|
plugin = catalogue.CataloguePlugin(
|
|
"test",
|
|
"Test",
|
|
"",
|
|
"org/repo",
|
|
version="1.0",
|
|
wheel_url=URL,
|
|
sha256=hashlib.sha256(WHEEL).hexdigest(),
|
|
)
|
|
return client.download_wheel(plugin, tmp_path)
|
|
client = github_releases.GitHubReleaseClient(opener=opener)
|
|
if kind.endswith("json"):
|
|
return client.list_releases("org/repo")
|
|
release = client._parse_release(
|
|
{
|
|
"tag_name": "v1.0",
|
|
"assets": [{"name": "pkg-1.0-py3-none-any.whl", "browser_download_url": URL}],
|
|
}
|
|
)
|
|
return client.download_wheel(release, tmp_path)
|
|
|
|
|
|
KINDS = ["catalogue-json", "releases-json", "catalogue-wheel", "releases-wheel"]
|
|
|
|
|
|
@pytest.mark.parametrize("kind", KINDS)
|
|
def test_truncated_content_length_is_rejected(kind, tmp_path):
|
|
payload = (
|
|
b'{"schema": 1, "plugins": []}'
|
|
if kind == "catalogue-json"
|
|
else b"[]"
|
|
if kind == "releases-json"
|
|
else WHEEL
|
|
)
|
|
reader, writer = socket.socketpair()
|
|
try:
|
|
writer.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n" + payload)
|
|
writer.shutdown(socket.SHUT_WR)
|
|
response = http.client.HTTPResponse(reader)
|
|
response.begin()
|
|
|
|
def opener(request, timeout):
|
|
response.url = request.full_url
|
|
return response
|
|
|
|
error = (
|
|
catalogue.CatalogueError
|
|
if kind.startswith("catalogue")
|
|
else github_releases.GitHubReleasesError
|
|
)
|
|
with pytest.raises(error):
|
|
operation(kind, opener, tmp_path)
|
|
assert response.closed
|
|
assert list(tmp_path.iterdir()) == []
|
|
finally:
|
|
reader.close()
|
|
writer.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url",
|
|
[
|
|
"http://example.test/catalogue.json",
|
|
"file:///tmp/catalogue.json",
|
|
"ftp://example.test/catalogue.json",
|
|
],
|
|
)
|
|
def test_initial_catalogue_url_requires_https(url):
|
|
with pytest.raises(catalogue.CatalogueError, match="HTTPS"):
|
|
catalogue.CatalogueClient(url=url)
|
|
|
|
|
|
def test_custom_https_catalogue_url_is_preserved():
|
|
url = "https://custom.example.test:8443/custom.json?channel=beta"
|
|
assert catalogue.CatalogueClient(url=url).url == url
|
|
|
|
|
|
@pytest.mark.parametrize("kind", KINDS)
|
|
def test_response_size_is_bounded_without_content_length(kind, monkeypatch, tmp_path):
|
|
module = catalogue if kind.startswith("catalogue") else github_releases
|
|
cap = "MAX_JSON_BYTES" if kind.endswith("json") else "MAX_WHEEL_BYTES"
|
|
monkeypatch.setattr(module, cap, 128, raising=False)
|
|
response = io.BytesIO(b" " * 129 if kind.endswith("json") else WHEEL * 20)
|
|
error = catalogue.CatalogueError if module is catalogue else github_releases.GitHubReleasesError
|
|
with pytest.raises(error, match="exceeds"):
|
|
operation(kind, lambda *a, **kw: response, tmp_path)
|
|
assert response.closed
|
|
assert list(tmp_path.iterdir()) == []
|
|
|
|
|
|
@contextmanager
|
|
def slow_response(mode):
|
|
"""Real stdlib HTTPResponse over local sockets, never the public network."""
|
|
reader, writer = socket.socketpair()
|
|
stop = threading.Event()
|
|
|
|
def serve():
|
|
try:
|
|
if mode == "chunk-header":
|
|
writer.sendall(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1;")
|
|
else:
|
|
writer.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n")
|
|
for _ in range(30):
|
|
if stop.wait(0.02):
|
|
return
|
|
if mode != "idle":
|
|
writer.sendall(b" ")
|
|
except OSError:
|
|
pass
|
|
finally:
|
|
writer.close()
|
|
|
|
thread = threading.Thread(target=serve, daemon=True)
|
|
thread.start()
|
|
response = http.client.HTTPResponse(reader)
|
|
response.begin()
|
|
|
|
def opener(request, timeout):
|
|
response.url = request.full_url
|
|
reader.settimeout(timeout)
|
|
return response
|
|
|
|
try:
|
|
yield opener, response
|
|
finally:
|
|
stop.set()
|
|
reader.close()
|
|
thread.join(timeout=2)
|
|
response.close()
|
|
|
|
|
|
@pytest.mark.parametrize("kind", KINDS)
|
|
@pytest.mark.parametrize("mode", ["trickle", "idle", "chunk-header"])
|
|
def test_total_body_deadline_interrupts_real_http_response(kind, mode, monkeypatch, tmp_path):
|
|
module = catalogue if kind.startswith("catalogue") else github_releases
|
|
setting = (
|
|
"JSON_DOWNLOAD_TIMEOUT_SECONDS"
|
|
if kind.endswith("json")
|
|
else "WHEEL_DOWNLOAD_TIMEOUT_SECONDS"
|
|
)
|
|
monkeypatch.setattr(module, setting, 0.15, raising=False)
|
|
error = catalogue.CatalogueError if module is catalogue else github_releases.GitHubReleasesError
|
|
with slow_response(mode) as (opener, response):
|
|
started = time.monotonic()
|
|
with pytest.raises(error):
|
|
operation(kind, opener, tmp_path)
|
|
elapsed = time.monotonic() - started
|
|
assert elapsed < 0.45, f"deadline did not interrupt {mode}: {elapsed:.3f}s"
|
|
assert response.closed
|
|
assert list(tmp_path.iterdir()) == []
|