import asyncio
import json
import hashlib
import sys
import zipfile
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
from fastapi import UploadFile
from PIL import Image
import pytest
BACKEND_DIR = Path(__file__).resolve().parents[1]
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
from service import app # noqa: E402
SVG = (
''
)
def png_bytes(color=(51, 102, 255)):
buffer = BytesIO()
Image.new("RGB", (1, 1), color).save(buffer, format="PNG")
return buffer.getvalue()
@pytest.fixture
def isolated_asset_store(tmp_path, monkeypatch):
assets_dir = tmp_path / "service_assets"
assets_dir.mkdir()
monkeypatch.setattr(app, "ASSETS_DIR", assets_dir)
design_templates_dir = tmp_path / "service_design_templates"
design_templates_dir.mkdir()
monkeypatch.setattr(app, "DESIGN_TEMPLATES_DIR", design_templates_dir)
return assets_dir
def asset_dirs(assets_dir):
return sorted(path for path in assets_dir.glob("*/*") if path.is_dir())
def test_duplicate_content_is_reused(isolated_asset_store):
first = app._register_import_asset("first", SVG.encode(), "image/svg+xml")
second = app._register_import_asset("second", SVG.encode(), "image/svg+xml")
assert first["asset_id"] == second["asset_id"]
assert second["name"] == "first"
assert len(asset_dirs(isolated_asset_store)) == 1
def test_same_name_different_content_gets_unique_assets(isolated_asset_store):
first = app._register_import_asset("same-name", SVG.encode(), "image/svg+xml")
second = app._register_import_asset(
"same-name", SVG.replace("#3366ff", "#ff3366").encode(), "image/svg+xml"
)
assert first["asset_id"] != second["asset_id"]
assert len(asset_dirs(isolated_asset_store)) == 2
assert first["sha256"] != second["sha256"]
def test_stale_sha256_meta_does_not_reuse_mismatched_file(isolated_asset_store):
content = SVG.encode()
existing_id = "asset_stale00000000000000000000000000"
existing_dir = isolated_asset_store / existing_id[:2] / existing_id
existing_dir.mkdir(parents=True)
(existing_dir / "asset.svg").write_text('')
(existing_dir / "meta.json").write_text(
json.dumps(
{
"asset_id": existing_id,
"name": "old",
"type": "sticker",
"mime_type": "image/svg+xml",
"sha256": hashlib.sha256(content).hexdigest(),
}
)
)
imported = app._register_import_asset("new", content, "image/svg+xml")
assert imported["asset_id"] != existing_id
assert len(asset_dirs(isolated_asset_store)) == 2
def test_wcd_production_uses_real_asset_ids(tmp_path, isolated_asset_store, monkeypatch):
manifest = {
"format": "wordcloud-canvas",
"version": 1,
"name": "test-design",
"assets": [{"id": "pkg-1", "name": "sticker", "type": "svg", "mimeType": "image/svg+xml"}],
}
document_json = {
"width": 40,
"height": 20,
"background": "#ffffff",
"layers": [],
"layerFolders": [],
"elements": [
{
"id": "element-1",
"type": "sticker",
"name": "sticker",
"assetId": "pkg-1",
"x": 5,
"y": 5,
"width": 10,
"height": 10,
"rotation": 0,
"opacity": 1,
}
],
}
package = BytesIO()
with zipfile.ZipFile(package, "w") as archive:
archive.writestr("manifest.json", json.dumps(manifest))
archive.writestr("document.json", json.dumps(document_json))
archive.writestr("assets/pkg-1.png", png_bytes())
class Paths:
input_dir = tmp_path / "input"
output_dir = tmp_path / "output"
def prepare_job_dirs(job_id):
Paths.input_dir.mkdir(parents=True)
Paths.output_dir.mkdir(parents=True)
return Paths
manager = SimpleNamespace(
create_job=lambda: "job-test",
set_artifacts=lambda *args, **kwargs: None,
set_status=lambda *args, **kwargs: None,
)
original_compose = app._compose_design_png
captured = {}
def compose(document, file_map, output_path):
captured["document"] = document
captured["file_map"] = dict(file_map)
original_compose(document, file_map, output_path)
monkeypatch.setattr(app, "manager", manager)
monkeypatch.setattr(app, "storage", SimpleNamespace(prepare_job_dirs=prepare_job_dirs))
monkeypatch.setattr(app, "_write_order", lambda *args, **kwargs: None)
monkeypatch.setattr(app, "_compose_design_png", compose)
monkeypatch.setattr(app.threading, "Thread", lambda target, **kwargs: SimpleNamespace(start=target))
upload = UploadFile(filename="test.wcd", file=BytesIO(package.getvalue()))
asyncio.run(app._create_wcd_job(upload, {}))
document = captured["document"]
file_map = captured["file_map"]
real_ids = [element["assetId"] for element in document["elements"]]
assert real_ids
assert all(asset_id in file_map for asset_id in real_ids)
assert all(Path(asset_id).is_absolute() is False for asset_id in real_ids)
png_path = isolated_asset_store.parent / "composed.png"
original_compose(document, file_map, png_path)
with Image.open(png_path) as image:
assert image.getpixel((10, 10)) == (51, 102, 255)