Newer
Older
navi-1 / tests / unit / tools / test_image_view.py
"""Unit tests for navi.tools.image_view."""

import io
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest
from PIL import Image

from navi.tools.image_view import ImageViewTool


def _make_image_bytes(mode="RGB", size=(100, 80), fmt="PNG") -> bytes:
    img = Image.new(mode, size, color=(255, 0, 0))
    buf = io.BytesIO()
    img.save(buf, format=fmt)
    return buf.getvalue()


# ── _preprocess tests ────────────────────────────────────────────────────────


class TestPreprocess:
    def test_keeps_small_image(self):
        raw = _make_image_bytes(size=(100, 80))
        processed, mime = ImageViewTool._preprocess(raw)
        assert mime == "image/jpeg"
        img = Image.open(io.BytesIO(processed))
        assert img.size == (100, 80)

    def test_resizes_large_image(self):
        raw = _make_image_bytes(size=(2000, 1500))
        processed, mime = ImageViewTool._preprocess(raw)
        assert mime == "image/jpeg"
        img = Image.open(io.BytesIO(processed))
        assert max(img.size) == 1024

    def test_converts_rgba_to_rgb(self):
        raw = _make_image_bytes(mode="RGBA", size=(50, 50))
        processed, mime = ImageViewTool._preprocess(raw)
        img = Image.open(io.BytesIO(processed))
        assert img.mode == "RGB"


# ── execute tests ────────────────────────────────────────────────────────────


class TestExecute:
    @pytest.mark.asyncio
    async def test_read_file_success(self, tmp_path: Path):
        img_path = tmp_path / "test.png"
        img_path.write_bytes(_make_image_bytes(size=(100, 80)))

        tool = ImageViewTool()
        result = await tool.execute({"source": str(img_path)})
        assert result.success is True
        assert "Image loaded" in result.output
        assert result.metadata["mime"] == "image/jpeg"
        assert "base64" in result.metadata

    @pytest.mark.asyncio
    async def test_mime_guard_rejects_non_image(self, tmp_path: Path):
        txt_path = tmp_path / "test.txt"
        txt_path.write_text("not an image")

        tool = ImageViewTool()
        result = await tool.execute({"source": str(txt_path)})
        assert result.success is False
        assert "Unsupported" in str(result.error)

    @pytest.mark.asyncio
    async def test_fetch_url_success(self):
        tool = ImageViewTool()
        raw = _make_image_bytes(size=(100, 80))

        from unittest.mock import MagicMock

        mock_response = MagicMock()
        mock_response.headers = {"content-type": "image/png"}
        mock_response.content = raw
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.get = AsyncMock(return_value=mock_response)

        with patch("navi.tools.image_view.httpx.AsyncClient", return_value=mock_client):
            result = await tool.execute({"source": "https://example.com/img.png"})

        assert result.success is True
        assert "Image loaded" in result.output
        assert result.metadata["mime"] == "image/jpeg"

    @pytest.mark.asyncio
    async def test_fetch_url_rejects_svg(self):
        from unittest.mock import MagicMock

        tool = ImageViewTool()
        mock_response = MagicMock()
        mock_response.headers = {"content-type": "image/svg+xml"}
        mock_response.content = b"<svg></svg>"
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client.get = AsyncMock(return_value=mock_response)

        with patch("navi.tools.image_view.httpx.AsyncClient", return_value=mock_client):
            result = await tool.execute({"source": "https://example.com/img.svg"})

        assert result.success is False
        assert "SVG" in str(result.error)