Files
playground/hamsterimg_test.py
T

196 lines
8.6 KiB
Python

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["httpx", "pytest"]
# ///
import json
import sys
from pathlib import Path
from threading import Barrier
from unittest.mock import patch
import httpx
import pytest
import hamsterimg as script
def test_hamsterimg_uploads_in_parallel_and_preserves_order(tmp_path: Path) -> None:
image_paths = [tmp_path / "first.jpg", tmp_path / "second.png"]
for image_path in image_paths:
image_path.write_bytes(image_path.name.encode())
barrier = Barrier(parties=2)
def respond(request: httpx.Request) -> httpx.Response:
assert str(request.url) == "https://hamsterimg.net/api/1/upload"
assert request.method == "POST"
assert request.headers["X-API-Key"] == "test-key"
assert request.headers["Content-Type"].startswith("multipart/form-data;")
body = request.read()
assert b'name="source"' in body
assert b'name="format"' in body
filename = "first.jpg" if b'filename="first.jpg"' in body else "second.png"
barrier.wait(timeout=5)
return httpx.Response(status_code=200, json={
"status_code": 200,
"image": {
"url": f"https://images.example/{filename}",
"thumb": {"url": f"https://images.example/thumb/{filename}"},
"delete_url": f"https://hamsterimg.net/{filename}/delete/token",
},
})
client = httpx.Client(transport=httpx.MockTransport(handler=respond), headers={"X-API-Key": "test-key"})
with patch.object(script.httpx, "Client", return_value=client) as factory:
images = script.upload_image_to_hamsterimg(api_key="test-key", image_paths=image_paths)
factory.assert_called_once_with(headers={"X-API-Key": "test-key"}, timeout=60)
assert images == [
script.UploadedImage(
url="https://images.example/first.jpg",
thumb_url="https://images.example/thumb/first.jpg",
delete_url="https://hamsterimg.net/first.jpg/delete/token",
),
script.UploadedImage(
url="https://images.example/second.png",
thumb_url="https://images.example/thumb/second.png",
delete_url="https://hamsterimg.net/second.png/delete/token",
),
]
@pytest.mark.parametrize("status, body, error", [
(401, {"error": {"message": "Invalid API key"}}, RuntimeError),
(200, {"status_code": 400, "error": {"message": "Invalid image"}}, RuntimeError),
(200, {"status_code": 200, "image": {}}, ValueError),
(200, {"status_code": 200, "image": {"url": "https://images.example/full.jpg", "thumb": {"url": "https://images.example/thumb.jpg"}}}, ValueError),
(200, {"status_code": 200, "image": {"url": "https://images.example/full.jpg", "thumb": {"url": None}, "delete_url": "https://hamsterimg.net/delete/token"}}, ValueError),
])
def test_hamsterimg_reports_upload_errors(tmp_path: Path, status: int, body: dict, error: type[Exception]) -> None:
image_path = tmp_path / "image.jpg"
image_path.write_bytes(b"image")
def respond(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code=status, json=body)
client = httpx.Client(transport=httpx.MockTransport(handler=respond))
with patch.object(script.httpx, "Client", return_value=client):
with pytest.raises(error):
script.upload_image_to_hamsterimg(api_key="test-key", image_paths=[image_path])
def test_hamsterimg_upload_error_includes_api_reason(tmp_path: Path) -> None:
image_path = tmp_path / "image.jpg"
image_path.write_bytes(b"image")
def respond(request: httpx.Request) -> httpx.Response:
return httpx.Response(
status_code=400,
json={"status_code": 400, "error": {"message": "Unable to read image"}},
)
client = httpx.Client(transport=httpx.MockTransport(handler=respond))
with patch.object(script.httpx, "Client", return_value=client):
with pytest.raises(RuntimeError, match="Unable to read image"):
script.upload_image_to_hamsterimg(api_key="test-key", image_paths=[image_path])
def test_hamsterimg_validates_inputs_before_uploading(tmp_path: Path) -> None:
image_path = tmp_path / "image.jpg"
image_path.write_bytes(b"image")
with patch.object(script.httpx, "Client") as client:
assert script.upload_image_to_hamsterimg(api_key="test-key", image_paths=[]) == []
with pytest.raises(ValueError, match="API key"):
script.upload_image_to_hamsterimg(api_key="", image_paths=[image_path])
with pytest.raises(FileNotFoundError):
script.upload_image_to_hamsterimg(api_key="test-key", image_paths=[image_path, tmp_path / "missing.jpg"])
client.assert_not_called()
@pytest.mark.parametrize("output_format", [None, "text", "json", "bbcode"])
def test_upload_image_command_output(
tmp_path: Path, output_format: str | None, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
) -> None:
image_paths = [tmp_path / "first.jpg", tmp_path / "second.png"]
for image_path in image_paths:
image_path.write_bytes(b"image")
images = [
script.UploadedImage(
url="https://images.example/first.jpg",
thumb_url="https://images.example/first.th.jpg",
delete_url="https://hamsterimg.net/first/delete/token",
),
script.UploadedImage(
url="https://images.example/second.png",
thumb_url="https://images.example/second.th.png",
delete_url="https://hamsterimg.net/second/delete/token",
),
]
arguments = ["hamsterimg.py", *map(str, image_paths)]
if output_format is not None:
arguments.extend(["--format", output_format])
monkeypatch.setattr(sys, "argv", arguments)
monkeypatch.setenv("HAMSTERIMG_API_KEY", "test-key")
with patch.object(script, "upload_image_to_hamsterimg", return_value=images) as upload:
assert script.main() == 0
result = capsys.readouterr()
assert result.err == ""
upload.assert_called_once_with(api_key="test-key", image_paths=image_paths)
if output_format == "json":
assert json.loads(result.out) == [
{
"url": "https://images.example/first.jpg",
"thumb_url": "https://images.example/first.th.jpg",
"delete_url": "https://hamsterimg.net/first/delete/token",
},
{
"url": "https://images.example/second.png",
"thumb_url": "https://images.example/second.th.png",
"delete_url": "https://hamsterimg.net/second/delete/token",
},
]
elif output_format == "bbcode":
assert result.out == (
"[url=https://images.example/first.jpg][img]https://images.example/first.th.jpg[/img][/url]\n"
"[url=https://images.example/second.png][img]https://images.example/second.th.png[/img][/url]\n"
)
else:
assert result.out == "https://images.example/first.jpg\nhttps://images.example/second.png\n"
def test_upload_image_command_failure(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture,
) -> None:
image_path = tmp_path / "image.jpg"
image_path.write_bytes(b"image")
monkeypatch.setattr(sys, "argv", ["hamsterimg.py", str(image_path), "--api-key", "explicit-key", "--format", "json"])
with patch.object(script, "upload_image_to_hamsterimg", side_effect=RuntimeError("Invalid image")) as upload:
assert script.main() == 1
assert capsys.readouterr().out == ""
assert "Invalid image" in caplog.text
upload.assert_called_once_with(api_key="explicit-key", image_paths=[image_path])
def test_upload_image_command_requires_paths(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sys, "argv", ["hamsterimg.py", "--api-key", "test-key"])
with patch.object(script, "upload_image_to_hamsterimg") as upload:
with pytest.raises(SystemExit) as error:
script.main()
assert error.value.code == 2
upload.assert_not_called()
def test_upload_image_command_requires_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
image_path = tmp_path / "image.jpg"
image_path.write_bytes(b"image")
monkeypatch.setattr(sys, "argv", ["hamsterimg.py", str(image_path)])
monkeypatch.delenv("HAMSTERIMG_API_KEY", raising=False)
with patch.object(script, "upload_image_to_hamsterimg") as upload:
with pytest.raises(SystemExit) as error:
script.main()
assert error.value.code == 2
upload.assert_not_called()
if __name__ == "__main__":
sys.exit(pytest.main(args=[str(Path(__file__).resolve()), *sys.argv[1:]]))