9e80a20063
- TemplateStore: CRUD filesystem + asset dir, frontmatter YAML roundtrip, path traversal rejection - OpenRouterClient: async httpx con retry backoff esponenziale (5xx, 429, timeout), no-retry su 4xx, parse usage/cost - GenerationStore: SQLite aiosqlite con schema generations + ephemeral_assets, cleanup TTL, stats aggregate Root pyproject aggiornato con respx + pytest-cov dev deps. 19 + 11 + 9 + 6 = 45 test totali, tutti passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3.3 KiB
Python
110 lines
3.3 KiB
Python
import base64
|
|
|
|
import pytest
|
|
|
|
from mcp_docugen.models import TemplateFrontmatter
|
|
from mcp_docugen.template_store import (
|
|
InvalidFrontmatter,
|
|
TemplateAlreadyExists,
|
|
TemplateNotFound,
|
|
TemplateStore,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path):
|
|
return TemplateStore(base_dir=tmp_path)
|
|
|
|
|
|
def _fm(name="fattura"):
|
|
return TemplateFrontmatter(
|
|
name=name,
|
|
description="Fattura commerciale",
|
|
required_variables=[{"name": "cliente", "type": "string"}],
|
|
)
|
|
|
|
|
|
async def test_create_and_get(store):
|
|
fm = _fm()
|
|
await store.create(name="fattura", frontmatter=fm, body="# Body {{cliente}}")
|
|
got = await store.get("fattura")
|
|
assert got.frontmatter.name == "fattura"
|
|
assert "# Body {{cliente}}" in got.body
|
|
|
|
|
|
async def test_create_duplicate_raises(store):
|
|
fm = _fm()
|
|
await store.create(name="fattura", frontmatter=fm, body="body")
|
|
with pytest.raises(TemplateAlreadyExists):
|
|
await store.create(name="fattura", frontmatter=fm, body="body")
|
|
|
|
|
|
async def test_get_missing_raises(store):
|
|
with pytest.raises(TemplateNotFound):
|
|
await store.get("nope")
|
|
|
|
|
|
async def test_list_returns_summaries(store):
|
|
await store.create(name="a", frontmatter=_fm("a"), body="b")
|
|
await store.create(name="b", frontmatter=_fm("b"), body="b")
|
|
result = await store.list()
|
|
names = sorted(t.name for t in result)
|
|
assert names == ["a", "b"]
|
|
|
|
|
|
async def test_update_overwrites(store):
|
|
await store.create(name="f", frontmatter=_fm("f"), body="old")
|
|
new_fm = TemplateFrontmatter(name="f", description="new description")
|
|
await store.update(name="f", frontmatter=new_fm, body="new body")
|
|
got = await store.get("f")
|
|
assert got.body == "new body"
|
|
assert got.frontmatter.description == "new description"
|
|
|
|
|
|
async def test_update_missing_raises(store):
|
|
with pytest.raises(TemplateNotFound):
|
|
await store.update(name="nope", frontmatter=_fm("nope"), body="x")
|
|
|
|
|
|
async def test_delete_removes(store):
|
|
await store.create(name="f", frontmatter=_fm("f"), body="b")
|
|
await store.delete("f")
|
|
with pytest.raises(TemplateNotFound):
|
|
await store.get("f")
|
|
|
|
|
|
async def test_delete_missing_raises(store):
|
|
with pytest.raises(TemplateNotFound):
|
|
await store.delete("nope")
|
|
|
|
|
|
async def test_assets_are_saved_and_listed(store):
|
|
png_bytes = b"\x89PNG\r\n\x1a\n"
|
|
assets = [
|
|
{
|
|
"filename": "logo.png",
|
|
"data_b64": base64.b64encode(png_bytes).decode(),
|
|
"mime": "image/png",
|
|
}
|
|
]
|
|
await store.create(name="f", frontmatter=_fm("f"), body="b", assets=assets)
|
|
got = await store.get("f")
|
|
asset_names = [a.filename for a in got.assets]
|
|
assert "logo.png" in asset_names
|
|
content = await store.read_asset("f", "logo.png")
|
|
assert content == png_bytes
|
|
|
|
|
|
async def test_asset_filename_rejects_path_traversal(store):
|
|
assets = [{"filename": "../evil.png", "data_b64": "aGk=", "mime": "image/png"}]
|
|
with pytest.raises(ValueError):
|
|
await store.create(name="f", frontmatter=_fm("f"), body="b", assets=assets)
|
|
|
|
|
|
async def test_frontmatter_parsing_rejects_malformed_yaml(store, tmp_path):
|
|
template_dir = tmp_path / "broken"
|
|
template_dir.mkdir()
|
|
(template_dir / "template.md").write_text("---\nname: :::broken\n---\nbody")
|
|
with pytest.raises(InvalidFrontmatter):
|
|
await store.get("broken")
|