#!/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:]]))