"""Unit tests for session content metadata store."""
import os
from datetime import datetime, timezone
import pytest
import navi.content_store as content_store
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):
monkeypatch.setattr(content_store.settings, "session_files_dir", str(tmp_path / "sessions"))
monkeypatch.setattr(content_store.settings, "public_url", "http://localhost:8000")
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):
monkeypatch.setattr(content_store.settings, "public_url", "http://localhost:8000")
monkeypatch.setattr(content_store.settings, "session_files_dir", str(tmp_path / "sessions"))
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",
}]