testsAndMisc-archive/python_pkg/download_cats/tests/test_http_status_anki.py
Copilot 9ba46e4758 Add HTTP status code Anki deck generator with http.cat images (#3)
* Initial plan

* Add HTTP status code Anki deck generator with cat images

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

* Address code review feedback: improve test parameter handling

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
2026-01-14 21:52:20 +01:00

377 lines
13 KiB
Python

"""Unit tests for HTTP status code Anki generator."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import requests
from python_pkg.download_cats.http_status_anki import (
CACHE_DIR,
HTTP_STATUS_CODES,
REQUEST_TIMEOUT,
_download_cat_image,
_get_cached_image_path,
generate_anki_package,
get_or_download_image,
main,
)
class TestDownloadCatImage:
"""Tests for _download_cat_image function."""
def test_successful_download(self) -> None:
"""Test successful image download."""
status_code = 200
image_content = b"fake cat image"
mock_response = MagicMock()
mock_response.content = image_content
with patch("requests.get", return_value=mock_response) as mock_get:
result = _download_cat_image(status_code)
mock_get.assert_called_once_with(
"https://http.cat/200.jpg",
timeout=REQUEST_TIMEOUT,
)
mock_response.raise_for_status.assert_called_once()
assert result == image_content
def test_http_error_raised(self) -> None:
"""Test that HTTP errors are raised."""
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
"404 Not Found"
)
with (
patch("requests.get", return_value=mock_response),
pytest.raises(requests.exceptions.HTTPError),
):
_download_cat_image(404)
def test_connection_error_raised(self) -> None:
"""Test that connection errors are raised."""
with (
patch(
"requests.get",
side_effect=requests.exceptions.ConnectionError("Network error"),
),
pytest.raises(requests.exceptions.ConnectionError),
):
_download_cat_image(500)
class TestGetCachedImagePath:
"""Tests for _get_cached_image_path function."""
def test_returns_correct_path(self) -> None:
"""Test that correct cache path is returned."""
status_code = 200
expected_path = CACHE_DIR / "200.jpg"
result = _get_cached_image_path(status_code)
assert result == expected_path
def test_different_codes_different_paths(self) -> None:
"""Test that different status codes get different paths."""
path_200 = _get_cached_image_path(200)
path_404 = _get_cached_image_path(404)
assert path_200 != path_404
assert "200.jpg" in str(path_200)
assert "404.jpg" in str(path_404)
class TestGetOrDownloadImage:
"""Tests for get_or_download_image function."""
def test_uses_cache_when_available(self) -> None:
"""Test that cached image is used when available."""
status_code = 200
cached_content = b"cached image"
with (
patch(
"pathlib.Path.exists",
return_value=True,
),
patch(
"pathlib.Path.read_bytes",
return_value=cached_content,
),
patch(
"python_pkg.download_cats.http_status_anki._download_cat_image"
) as mock_download,
):
result = get_or_download_image(status_code, use_cache=True)
assert result == cached_content
mock_download.assert_not_called()
def test_downloads_when_not_cached(self) -> None:
"""Test that image is downloaded when not in cache."""
status_code = 404
downloaded_content = b"downloaded image"
with (
patch("pathlib.Path.exists", return_value=False),
patch("pathlib.Path.mkdir"),
patch(
"python_pkg.download_cats.http_status_anki._download_cat_image",
return_value=downloaded_content,
) as mock_download,
patch("pathlib.Path.write_bytes") as mock_write,
):
result = get_or_download_image(status_code, use_cache=True)
assert result == downloaded_content
mock_download.assert_called_once_with(status_code)
mock_write.assert_called_once_with(downloaded_content)
def test_ignores_cache_when_disabled(self) -> None:
"""Test that cache is ignored when use_cache=False."""
status_code = 200
downloaded_content = b"fresh download"
with (
patch("pathlib.Path.exists", return_value=True),
patch("pathlib.Path.mkdir"),
patch(
"python_pkg.download_cats.http_status_anki._download_cat_image",
return_value=downloaded_content,
) as mock_download,
patch("pathlib.Path.write_bytes"),
):
result = get_or_download_image(status_code, use_cache=False)
assert result == downloaded_content
mock_download.assert_called_once_with(status_code)
def test_creates_cache_directory(self) -> None:
"""Test that cache directory is created if it doesn't exist."""
with (
patch("pathlib.Path.exists", return_value=False),
patch("pathlib.Path.mkdir") as mock_mkdir,
patch(
"python_pkg.download_cats.http_status_anki._download_cat_image",
return_value=b"image",
),
patch("pathlib.Path.write_bytes"),
):
get_or_download_image(200, use_cache=True)
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
class TestGenerateAnkiPackage:
"""Tests for generate_anki_package function."""
def test_creates_package_with_all_codes(self) -> None:
"""Test that package is created with cards for all status codes."""
test_codes = {200: "OK", 404: "Not Found"}
with (
patch(
"python_pkg.download_cats.http_status_anki.get_or_download_image",
return_value=b"fake image",
),
patch("pathlib.Path.write_bytes"),
):
package = generate_anki_package(test_codes, use_cache=True)
# Should have 2 cards per status code (bidirectional)
assert len(package.decks[0].notes) == 4
def test_uses_correct_deck_name(self) -> None:
"""Test that deck uses specified name."""
test_codes = {200: "OK"}
deck_name = "Test Deck"
with (
patch(
"python_pkg.download_cats.http_status_anki.get_or_download_image",
return_value=b"fake image",
),
patch("pathlib.Path.write_bytes"),
):
package = generate_anki_package(test_codes, deck_name, use_cache=True)
assert package.decks[0].name == deck_name
def test_handles_download_errors(self) -> None:
"""Test that download errors are handled gracefully."""
test_codes = {200: "OK", 404: "Not Found"}
def mock_download(code: int, *, use_cache: bool) -> bytes:
del use_cache # Intentionally unused in test mock
error_msg = "Failed"
if code == 404:
raise requests.exceptions.RequestException(error_msg)
return b"image"
with (
patch(
"python_pkg.download_cats.http_status_anki.get_or_download_image",
side_effect=mock_download,
),
patch("pathlib.Path.write_bytes"),
):
package = generate_anki_package(test_codes, use_cache=True)
# Should only have cards for successful downloads (200)
assert len(package.decks[0].notes) == 2 # 2 cards for status 200
def test_creates_media_files(self) -> None:
"""Test that media files are created for images."""
test_codes = {200: "OK"}
with (
patch(
"python_pkg.download_cats.http_status_anki.get_or_download_image",
return_value=b"fake image",
),
patch("pathlib.Path.write_bytes"),
):
package = generate_anki_package(test_codes, use_cache=True)
assert len(package.media_files) == 1
assert "http_cat_200.jpg" in package.media_files[0]
def test_respects_cache_setting(self) -> None:
"""Test that cache setting is passed to download function."""
test_codes = {200: "OK"}
with (
patch(
"python_pkg.download_cats.http_status_anki.get_or_download_image",
return_value=b"fake image",
) as mock_get,
patch("pathlib.Path.write_bytes"),
):
generate_anki_package(test_codes, use_cache=False)
mock_get.assert_called_with(200, use_cache=False)
class TestMain:
"""Tests for main function."""
def test_default_output_path(self) -> None:
"""Test that default output path is used."""
with patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package"
) as mock_gen:
mock_package = MagicMock()
mock_gen.return_value = mock_package
result = main([])
assert result == 0
# Check that write_to_file was called with default path
call_args = mock_package.write_to_file.call_args[0][0]
assert "http_status_codes.apkg" in call_args
def test_custom_output_path(self) -> None:
"""Test that custom output path is used."""
with patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package"
) as mock_gen:
mock_package = MagicMock()
mock_gen.return_value = mock_package
result = main(["--output", "custom.apkg"])
assert result == 0
call_args = mock_package.write_to_file.call_args[0][0]
assert "custom.apkg" in call_args
def test_custom_deck_name(self) -> None:
"""Test that custom deck name is used."""
with patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package"
) as mock_gen:
mock_package = MagicMock()
mock_gen.return_value = mock_package
main(["--deck-name", "My Custom Deck"])
mock_gen.assert_called_once()
assert mock_gen.call_args[0][1] == "My Custom Deck"
def test_no_cache_option(self) -> None:
"""Test that --no-cache option disables caching."""
with patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package"
) as mock_gen:
mock_package = MagicMock()
mock_gen.return_value = mock_package
main(["--no-cache"])
# use_cache should be False (not args.no_cache = True)
assert mock_gen.call_args[1]["use_cache"] is False
def test_error_handling(self) -> None:
"""Test that errors are handled gracefully."""
with patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package",
side_effect=RuntimeError("Test error"),
):
result = main([])
assert result == 1
def test_verbose_logging(self) -> None:
"""Test that verbose flag enables logging."""
with (
patch(
"python_pkg.download_cats.http_status_anki.generate_anki_package"
) as mock_gen,
patch("logging.basicConfig") as mock_config,
):
mock_package = MagicMock()
mock_gen.return_value = mock_package
main(["--verbose"])
# Check that logging was configured
mock_config.assert_called_once()
call_kwargs = mock_config.call_args[1]
assert call_kwargs["level"] == 20 # logging.INFO
class TestConstants:
"""Tests for module constants."""
def test_http_status_codes_not_empty(self) -> None:
"""Test that HTTP_STATUS_CODES is populated."""
assert len(HTTP_STATUS_CODES) > 0
def test_common_status_codes_present(self) -> None:
"""Test that common status codes are present."""
common_codes = [200, 201, 301, 400, 401, 403, 404, 500, 502, 503]
for code in common_codes:
assert code in HTTP_STATUS_CODES
def test_status_codes_have_descriptions(self) -> None:
"""Test that all status codes have non-empty descriptions."""
for code, description in HTTP_STATUS_CODES.items():
assert isinstance(code, int)
assert isinstance(description, str)
assert len(description) > 0
def test_request_timeout_value(self) -> None:
"""Test REQUEST_TIMEOUT constant has expected value."""
assert REQUEST_TIMEOUT == 30
def test_cache_dir_path(self) -> None:
"""Test that CACHE_DIR is properly configured."""
assert isinstance(CACHE_DIR, Path)
assert "http_cat_cache" in str(CACHE_DIR)