from concurrent.futures import ThreadPoolExecutor import json import pytest from fastapi.testclient import TestClient from app.fonts.store import FontCatalog, FontValidationError from app.main import create_app def test_api_validation_and_persistence(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) items = client.get("/api/fonts").json()["items"] assert client.get("/api/fonts").json()["favorite_ids"] == [] ids = [item["id"] for item in items[:3]] for identifier in ids: assert client.put("/api/fonts/favorite", json={"id": identifier, "favorite": True}).status_code == 200 path = app.state.font_catalog.root / "favorites.json" before = path.read_bytes() client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": True}) assert path.read_bytes() == before assert FontCatalog(path.parent.parent).document()["favorite_ids"] == ids for payload in ({"id": "default", "favorite": "true"}, {"id": "default", "favorite": 1}, {"id": "", "favorite": True}, {"id": "default", "favorite": True, "extra": 1}): assert client.put("/api/fonts/favorite", json=payload).status_code == 422 assert client.put("/api/fonts/favorite", json={"id": "missing", "favorite": True}).status_code == 404 assert client.put("/api/fonts/favorite", json={"id": "missing", "favorite": False}).status_code == 200 client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": False}) assert client.get("/api/fonts").json()["favorite_ids"] == ids[1:] def test_concurrent_merge_and_write_failure(tmp_path, monkeypatch): catalog = FontCatalog(tmp_path) ids = [item["id"] for item in catalog.document()["items"][:3]] with ThreadPoolExecutor(max_workers=3) as pool: list(pool.map(lambda identifier: catalog.set_favorite(identifier, True), ids)) assert set(catalog.document()["favorite_ids"]) == set(ids) before = (catalog.root / "favorites.json").read_bytes() def fail(*args): raise OSError("injected write failure") monkeypatch.setattr("app.fonts.store.atomic_write_bytes", fail) with pytest.raises(OSError): catalog.set_favorite(ids[0], False) assert set(catalog.document()["favorite_ids"]) == set(ids) assert (catalog.root / "favorites.json").read_bytes() == before @pytest.mark.parametrize("content", [b"broken", b'{"schema_version":2,"favorite_ids":[]}', b'{"schema_version":true,"favorite_ids":[]}', b'{"schema_version":1,"favorite_ids":["x","x"]}', b'{"schema_version":1,"favorite_ids":[],"extra":1}']) def test_invalid_file_preserved(tmp_path, content): (tmp_path / "fonts").mkdir() path = tmp_path / "fonts/favorites.json" path.write_bytes(content) with pytest.raises(FontValidationError): FontCatalog(tmp_path) assert path.read_bytes() == content def test_stale_ids_preserved_and_removable(tmp_path): catalog = FontCatalog(tmp_path) path = catalog.root / "favorites.json" path.write_text(json.dumps({"schema_version": 1, "favorite_ids": ["stale"]}), encoding="utf-8") catalog = FontCatalog(tmp_path) assert catalog.document()["favorite_ids"] == ["stale"] assert not any(item["id"] == "stale" for item in catalog.document()["items"]) assert catalog.set_favorite("stale", False) == [] def test_same_family_styles_are_independent(tmp_path): from test_fonts import build_test_font app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) ids = [] for style in ["Bold", "Italic"]: response = client.post("/api/fonts/import?filename=style.ttf", content=build_test_font("Same Family", style), headers={"Content-Type": "application/octet-stream"}) assert response.status_code == 201 ids.append(response.json()["fonts"][0]["id"]) client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": True}) assert client.get("/api/fonts").json()["favorite_ids"] == [ids[0]] client.put("/api/fonts/favorite", json={"id": ids[1], "favorite": True}) client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": False}) assert client.get("/api/fonts").json()["favorite_ids"] == [ids[1]]