"""Tests for config and image error conditions, and exception attribute contracts.""" from pathlib import Path import pytest from errors import ( ConfigFileError, ConfigNotLoadedError, ConfigValidationError, ImageFileNotFoundError, ImageReadError, ) from logic.images import crop_save, prep_img_b64, serve_crop # ── Helpers ─────────────────────────────────────────────────────────────────── def _make_png(tmp_path: Path, filename: str = "img.png") -> Path: """Write a minimal 4x4 red PNG to tmp_path and return its path.""" from PIL import Image path = tmp_path / filename img = Image.new("RGB", (4, 4), color=(255, 0, 0)) img.save(path, format="PNG") return path def _make_corrupt(tmp_path: Path, filename: str = "bad.jpg") -> Path: """Write a file with invalid image bytes and return its path.""" path = tmp_path / filename path.write_bytes(b"this is not an image\xff\xfe") return path # ── ImageFileNotFoundError ──────────────────────────────────────────────────── def test_prep_img_b64_file_not_found(tmp_path: Path) -> None: missing = tmp_path / "missing.png" with pytest.raises(ImageFileNotFoundError) as exc_info: prep_img_b64(missing) assert exc_info.value.path == missing assert str(missing) in str(exc_info.value) def test_crop_save_file_not_found(tmp_path: Path) -> None: missing = tmp_path / "missing.png" with pytest.raises(ImageFileNotFoundError) as exc_info: crop_save(missing, 0, 0, 2, 2) assert exc_info.value.path == missing def test_serve_crop_file_not_found(tmp_path: Path) -> None: missing = tmp_path / "missing.png" with pytest.raises(ImageFileNotFoundError) as exc_info: serve_crop(missing, None) assert exc_info.value.path == missing # ── ImageReadError ──────────────────────────────────────────────────────────── def test_prep_img_b64_corrupt_file(tmp_path: Path) -> None: bad = _make_corrupt(tmp_path) with pytest.raises(ImageReadError) as exc_info: prep_img_b64(bad) assert exc_info.value.path == bad assert str(bad) in str(exc_info.value) assert exc_info.value.reason # non-empty reason def test_crop_save_corrupt_file(tmp_path: Path) -> None: bad = _make_corrupt(tmp_path) with pytest.raises(ImageReadError) as exc_info: crop_save(bad, 0, 0, 2, 2) assert exc_info.value.path == bad def test_serve_crop_corrupt_file(tmp_path: Path) -> None: bad = _make_corrupt(tmp_path) with pytest.raises(ImageReadError) as exc_info: serve_crop(bad, None) assert exc_info.value.path == bad # ── prep_img_b64 success path ───────────────────────────────────────────────── def test_prep_img_b64_success(tmp_path: Path) -> None: path = _make_png(tmp_path) b64, mime = prep_img_b64(path) assert mime == "image/png" assert len(b64) > 0 def test_prep_img_b64_with_crop(tmp_path: Path) -> None: path = _make_png(tmp_path) b64, mime = prep_img_b64(path, crop_frac=(0.0, 0.0, 0.5, 0.5)) assert mime == "image/png" assert len(b64) > 0 # ── Config exception attribute contracts ────────────────────────────────────── def test_config_not_loaded_error() -> None: exc = ConfigNotLoadedError() assert "load_config" in str(exc) def test_config_file_error() -> None: path = Path("config/missing.yaml") exc = ConfigFileError(path, "file not found") assert exc.path == path assert exc.reason == "file not found" assert "missing.yaml" in str(exc) assert "file not found" in str(exc) def test_config_validation_error() -> None: exc = ConfigValidationError("unexpected field 'foo'") assert exc.reason == "unexpected field 'foo'" assert "unexpected field" in str(exc) # ── Config loading errors ───────────────────────────────────────────────────── def test_load_config_raises_on_invalid_yaml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import config as config_module cfg_dir = tmp_path / "config" cfg_dir.mkdir() (cfg_dir / "credentials.default.yaml").write_text(": invalid: yaml: {\n") # write empty valid files for other categories for cat in ["models", "functions", "ui"]: (cfg_dir / f"{cat}.default.yaml").write_text(f"{cat}: {{}}\n") monkeypatch.setattr(config_module, "_CONFIG_DIR", cfg_dir) with pytest.raises(ConfigFileError) as exc_info: config_module.load_config() assert exc_info.value.path == cfg_dir / "credentials.default.yaml" assert exc_info.value.reason def test_load_config_raises_on_schema_mismatch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: import config as config_module cfg_dir = tmp_path / "config" cfg_dir.mkdir() # credentials expects CredentialConfig but we give it a non-dict value (cfg_dir / "credentials.default.yaml").write_text("credentials:\n openrouter: not_a_dict\n") for cat in ["models", "functions", "ui"]: (cfg_dir / f"{cat}.default.yaml").write_text("") monkeypatch.setattr(config_module, "_CONFIG_DIR", cfg_dir) with pytest.raises(ConfigValidationError) as exc_info: config_module.load_config() assert exc_info.value.reason def test_get_config_raises_if_not_loaded(monkeypatch: pytest.MonkeyPatch) -> None: import config as config_module # Clear the holder to simulate unloaded state original = list(config_module.config_holder) config_module.config_holder.clear() try: with pytest.raises(ConfigNotLoadedError): config_module.get_config() finally: config_module.config_holder.extend(original) # ── Image exception string representation ───────────────────────────────────── def test_image_file_not_found_str() -> None: exc = ImageFileNotFoundError(Path("/data/images/img.jpg")) assert exc.path == Path("/data/images/img.jpg") assert "img.jpg" in str(exc) def test_image_read_error_str() -> None: exc = ImageReadError(Path("/data/images/img.jpg"), "cannot identify image file") assert exc.path == Path("/data/images/img.jpg") assert exc.reason == "cannot identify image file" assert "img.jpg" in str(exc) assert "cannot identify image file" in str(exc)