Newer
Older
navi-1 / tests / unit / test_content_store.py
"""Unit tests for session content metadata store."""

import os
from datetime import datetime, timezone

import pytest

import navi.content_store as content_store
import navi.session_files as session_files_mod
from tests.conftest_factory import FakeConnection, FakePool


class TestEnsureTables:
    async def test_creates_session_content_table_and_indexes(self, monkeypatch):
        conn = FakeConnection()

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)

        await content_store.ensure_tables()

        executed = "\n".join(call[1] for call in conn.calls if call[0] == "execute")
        assert "CREATE TABLE IF NOT EXISTS session_content" in executed
        assert "ALTER TABLE session_content ADD COLUMN IF NOT EXISTS source_filename TEXT" in executed
        assert "idx_session_content_session" in executed
        assert "CREATE UNIQUE INDEX IF NOT EXISTS idx_session_content_file" in executed
        assert "ON session_content (session_id, filename)" in executed

    async def test_ensure_tables_is_idempotent(self, monkeypatch):
        conn = FakeConnection()

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)

        await content_store.ensure_tables()
        await content_store.ensure_tables()

        unique_index_calls = [
            call for call in conn.calls
            if call[0] == "execute" and "idx_session_content_file" in call[1]
        ]
        assert len(unique_index_calls) == 2
        assert all("IF NOT EXISTS" in call[1] for call in unique_index_calls)


class TestPublish:
    @pytest.fixture(autouse=True)
    def _session_dir(self, monkeypatch, tmp_path):
        from navi.config import Settings

        _test_settings = Settings(
            session_files_dir=str(tmp_path / "sessions"), public_url="http://localhost:8000"
        )
        monkeypatch.setattr(content_store, "settings", _test_settings)
        monkeypatch.setattr(session_files_mod, "settings", _test_settings)

    async def test_upserts_by_session_and_filename(self, monkeypatch, tmp_path):
        conn = FakeConnection()
        conn.enqueue("INSERT 0 1")

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)
        sess_dir = tmp_path / "sessions" / "sess-1"
        sess_dir.mkdir(parents=True)
        (sess_dir / "logo.svg").write_text("<svg></svg>")

        info = await content_store.publish(
            session_id="sess-1",
            filename="logo.svg",
            title="Logo",
        )

        assert info["id"] == "sess-1_logo.svg"
        assert info["url"] == "http://localhost:8000/api/sessions/sess-1/files/logo.svg"
        assert info["download_url"] == "http://localhost:8000/api/sessions/sess-1/files/logo.svg?download=1"
        call = conn.calls[0]
        assert call[0] == "execute"
        assert "ON CONFLICT (session_id, filename) DO UPDATE" in call[1]
        assert call[2][1:5] == ("sess-1", "logo.svg", "svg", "Logo")

    async def test_publish_includes_existing_source_file(self, monkeypatch, tmp_path):
        conn = FakeConnection()
        conn.enqueue("INSERT 0 1")

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)
        sess_dir = tmp_path / "sessions" / "sess-1"
        sess_dir.mkdir(parents=True)
        (sess_dir / "model.stl").write_text("solid model")
        (sess_dir / "model.scad").write_text("cube([1, 1, 1]);")

        info = await content_store.publish(
            session_id="sess-1",
            filename="model.stl",
            source_filename="model.scad",
        )

        assert info["source_filename"] == "model.scad"
        assert info["source_url"] == "http://localhost:8000/api/sessions/sess-1/files/model.scad"
        assert conn.calls[0][2][5] == "model.scad"

    async def test_publish_ignores_missing_source_file(self, monkeypatch, tmp_path):
        conn = FakeConnection()
        conn.enqueue("INSERT 0 1")

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)
        sess_dir = tmp_path / "sessions" / "sess-1"
        sess_dir.mkdir(parents=True)
        (sess_dir / "model.stl").write_text("solid model")

        info = await content_store.publish(
            session_id="sess-1",
            filename="model.stl",
            source_filename="missing.scad",
        )

        assert "source_filename" not in info
        assert conn.calls[0][2][5] is None

    async def test_repeated_publish_uses_same_content_id_and_upsert(self, monkeypatch, tmp_path):
        conn = FakeConnection()
        conn.enqueue("INSERT 0 1")
        conn.enqueue("INSERT 0 1")

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)
        sess_dir = tmp_path / "sessions" / "sess-1"
        sess_dir.mkdir(parents=True)
        (sess_dir / "logo.svg").write_text("<svg></svg>")

        first = await content_store.publish(session_id="sess-1", filename="logo.svg", title="Logo")
        second = await content_store.publish(session_id="sess-1", filename="logo.svg", title="Logo 2")

        assert first["id"] == second["id"] == "sess-1_logo.svg"
        execute_calls = [call for call in conn.calls if call[0] == "execute"]
        assert len(execute_calls) == 2
        assert all("ON CONFLICT (session_id, filename) DO UPDATE" in call[1] for call in execute_calls)

    async def test_missing_file_raises(self, monkeypatch, tmp_path):
        async def fake_pool():
            return FakePool(FakeConnection())

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)

        with pytest.raises(FileNotFoundError):
            await content_store.publish(session_id="sess-1", filename="missing.svg")


class TestListForSession:
    @pytest.fixture(autouse=True)
    def _settings(self, monkeypatch, tmp_path):
        from navi.config import Settings

        _test_settings = Settings(
            public_url="http://localhost:8000", session_files_dir=str(tmp_path / "sessions")
        )
        monkeypatch.setattr(content_store, "settings", _test_settings)
        monkeypatch.setattr(session_files_mod, "settings", _test_settings)

    async def test_returns_urls_for_published_content(self, monkeypatch, tmp_path):
        conn = FakeConnection()
        sess_dir = tmp_path / "sessions" / "sess-1"
        sess_dir.mkdir(parents=True)
        logo = sess_dir / "logo.svg"
        logo.write_text("<svg></svg>")
        mtime = datetime(2026, 4, 30, 1, 2, 3, tzinfo=timezone.utc)
        os.utime(logo, (mtime.timestamp(), mtime.timestamp()))

        conn.enqueue([
            {
                "id": "sess-1_logo.svg",
                "filename": "logo.svg",
                "content_type": "svg",
                "title": "Logo",
                "source_filename": "logo.scad",
                "created_at": datetime(2026, 4, 30, tzinfo=timezone.utc),
            }
        ])

        async def fake_pool():
            return FakePool(conn)

        monkeypatch.setattr(content_store, "_get_db_pool", fake_pool)

        items = await content_store.list_for_session("sess-1")

        assert items == [{
            "id": "sess-1_logo.svg",
            "filename": "logo.svg",
            "content_type": "svg",
            "title": "Logo",
            "url": "http://localhost:8000/api/sessions/sess-1/files/logo.svg",
            "download_url": "http://localhost:8000/api/sessions/sess-1/files/logo.svg?download=1",
            "created_at": "2026-04-30T00:00:00+00:00",
            "updated_at": "2026-04-30T01:02:03+00:00",
        }]