Files
2026-07-13 13:12:33 +08:00

327 lines
9.6 KiB
Python

from __future__ import annotations
import base64
import hashlib
import json
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import cast
import httpx
import pytest
from opensquilla.tools.builtin import web
from opensquilla.tools.types import ToolError
HttpRequestCallable = Callable[..., Awaitable[str]]
def _original_http_request() -> HttpRequestCallable:
return cast(HttpRequestCallable, web.http_request.__wrapped__.__wrapped__)
def _patch_response(monkeypatch: pytest.MonkeyPatch, response: httpx.Response) -> None:
class FakeAsyncClient:
def __init__(self, **kwargs: object) -> None:
pass
async def __aenter__(self) -> FakeAsyncClient:
return self
async def __aexit__(self, *args: object) -> None:
return None
async def request(self, **kwargs: object) -> httpx.Response:
return response
monkeypatch.setattr(web.httpx, "AsyncClient", FakeAsyncClient)
@pytest.mark.asyncio
async def test_http_request_returns_body_base64_for_octet_stream_invalid_utf8(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw = b"\xff\xfe\x00PDF"
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/octet-stream"},
request=httpx.Request("GET", "https://example.test/file"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/file"))
assert payload["content_type"] == "application/octet-stream"
assert payload["body"] is None
assert base64.b64decode(payload["body_base64"]) == raw
assert payload["body_base64_truncated"] is False
@pytest.mark.asyncio
async def test_http_request_returns_text_body_for_json_content_type(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_response(
monkeypatch,
httpx.Response(
200,
content=b'{"ok":true}',
headers={"content-type": "application/json; charset=utf-8"},
request=httpx.Request("GET", "https://example.test/data"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/data"))
assert payload["body"] == '{"ok":true}'
assert base64.b64decode(payload["body_base64"]) == b'{"ok":true}'
assert payload["body_truncated"] is False
@pytest.mark.asyncio
async def test_http_request_keeps_body_base64_for_misleading_text_content_type(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw = b"\xff\xfe\x00PDF"
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "text/plain; charset=utf-8"},
request=httpx.Request("GET", "https://example.test/mislabelled"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/mislabelled"))
assert payload["body"] is not None
assert "\ufffd" in payload["body"]
assert base64.b64decode(payload["body_base64"]) == raw
@pytest.mark.asyncio
async def test_http_request_uses_body_base64_when_content_type_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
raw = b"\x00\x01\x02"
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
request=httpx.Request("GET", "https://example.test/blob"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/blob"))
assert payload["content_type"] == ""
assert payload["body"] is None
assert base64.b64decode(payload["body_base64"]) == raw
@pytest.mark.asyncio
async def test_http_request_does_not_implicitly_save_large_binary_response(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
raw = b"x" * 1_000_001
monkeypatch.chdir(tmp_path)
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/octet-stream"},
request=httpx.Request("GET", "https://example.test/large"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/large"))
digest = hashlib.sha256(raw).hexdigest()
saved_path = tmp_path / ".fetch" / f"{digest}.bin"
assert payload["size"] == len(raw)
assert payload["sha256"] == digest
assert payload["body_saved"] is False
assert payload["body"] is None
assert payload["body_base64"] is not None
assert payload["body_base64_truncated"] is True
assert payload["path"] is None
assert not saved_path.exists()
@pytest.mark.asyncio
async def test_http_request_does_not_implicitly_save_large_text_response(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
raw = b"<feed>" + (b"a" * 60_000) + b"</feed>"
monkeypatch.chdir(tmp_path)
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/xml"},
request=httpx.Request("GET", "https://example.test/feed"),
),
)
payload = json.loads(await _original_http_request()(url="https://example.test/feed"))
digest = hashlib.sha256(raw).hexdigest()
saved_path = tmp_path / ".fetch" / f"{digest}.bin"
assert payload["size"] == len(raw)
assert payload["sha256"] == digest
assert payload["body_saved"] is False
assert payload["body"].startswith("<feed>")
assert len(payload["body"]) == 10_000
assert payload["body_base64"] is not None
assert payload["body_truncated"] is True
assert payload["body_base64_truncated"] is False
assert payload["path"] is None
assert not saved_path.exists()
@pytest.mark.asyncio
async def test_http_request_output_path_saves_inside_fetch_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
raw = b'{"ok":true}'
monkeypatch.chdir(tmp_path)
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/json"},
request=httpx.Request("GET", "https://example.test/data"),
),
)
payload = json.loads(
await _original_http_request()(
url="https://example.test/data",
output_path="raw.json",
)
)
saved_path = tmp_path / ".fetch" / "raw.json"
assert Path(payload["path"]) == saved_path
assert saved_path.read_bytes() == raw
assert payload["body_saved"] is True
assert payload["body"] is None
assert payload["body_base64"] is None
@pytest.mark.asyncio
async def test_http_request_output_path_rejects_existing_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
raw = b'{"ok":true}'
monkeypatch.chdir(tmp_path)
existing_path = tmp_path / ".fetch" / "raw.json"
existing_path.parent.mkdir()
existing_path.write_bytes(b"keep")
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/json"},
request=httpx.Request("GET", "https://example.test/data"),
),
)
with pytest.raises(ToolError, match="output_path already exists"):
await _original_http_request()(
url="https://example.test/data",
output_path="raw.json",
)
assert existing_path.read_bytes() == b"keep"
@pytest.mark.asyncio
async def test_http_request_output_path_rejects_fetch_directory_escape(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.chdir(tmp_path)
_patch_response(
monkeypatch,
httpx.Response(
200,
content=b"escape",
headers={"content-type": "text/plain"},
request=httpx.Request("GET", "https://example.test/data"),
),
)
with pytest.raises(ToolError, match="output_path must stay inside"):
await _original_http_request()(
url="https://example.test/data",
output_path="../escape.txt",
)
assert not (tmp_path / "escape.txt").exists()
@pytest.mark.asyncio
async def test_http_request_output_path_rejects_foreign_posix_path_on_windows(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
from opensquilla.tools.builtin import web
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(web.os, "name", "nt")
_patch_response(
monkeypatch,
httpx.Response(
200,
content=b"foreign",
headers={"content-type": "text/plain"},
request=httpx.Request("GET", "https://example.test/data"),
),
)
with pytest.raises(ToolError, match="foreign_host_path"):
await _original_http_request()(
url="https://example.test/data",
output_path="/Users/a1/Desktop/raw.txt",
)
assert not (tmp_path / "Users").exists()
@pytest.mark.asyncio
async def test_http_request_without_output_path_does_not_create_fetch_directory(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
raw = b"x" * 60_000
monkeypatch.chdir(tmp_path)
_patch_response(
monkeypatch,
httpx.Response(
200,
content=raw,
headers={"content-type": "application/octet-stream"},
request=httpx.Request("GET", "https://example.test/blob"),
),
)
first = json.loads(await _original_http_request()(url="https://example.test/blob"))
second = json.loads(await _original_http_request()(url="https://example.test/blob"))
assert first["path"] is None
assert second["path"] is None
assert not (tmp_path / ".fetch").exists()