import base64 import json import pytest from fastapi.testclient import TestClient from app.demo_library import DEMO_ANIMATION_ID, DEMO_STATIC_ID from app.library_order import LibraryOrderValidationError from app.main import create_app def scene(color=(0, 0, 0)) -> dict: return { "version": 1, "width": 64, "height": 64, "pixelRgb": base64.b64encode(bytes(color) * (64 * 64)).decode("ascii"), "elements": [], } def create_template(client: TestClient, name: str) -> dict: response = client.post("/api/templates", json={"name": name, "scene": scene()}) assert response.status_code == 201 return response.json() def create_animation(client: TestClient, name: str) -> dict: response = client.post("/api/animations", json={"name": name}) assert response.status_code == 201 return response.json() def put_order(client: TestClient, items: list[dict], revision: str): return client.put( "/api/library/order", headers={"If-Match": f'"{revision}"'}, json={"items": items}, ) def test_library_order_defaults_mixes_types_and_persists_only_after_update(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) first_template = create_template(client, "静态一") second_template = create_template(client, "静态二") first_animation = create_animation(client, "动图一") second_animation = create_animation(client, "动图二") order_path = tmp_path / "data" / "library" / "order.json" default = client.get("/api/library/order") assert default.status_code == 200 assert default.json()["items"] == [ {"type": "template", "id": second_template["id"]}, {"type": "template", "id": first_template["id"]}, {"type": "animation", "id": second_animation["id"]}, {"type": "animation", "id": first_animation["id"]}, ] assert not order_path.exists() mixed = [ {"type": "animation", "id": first_animation["id"]}, {"type": "template", "id": first_template["id"]}, {"type": "animation", "id": second_animation["id"]}, {"type": "template", "id": second_template["id"]}, ] assert client.put("/api/library/order", json={"items": mixed}).status_code == 428 updated = put_order(client, mixed, default.json()["revision"]) assert updated.status_code == 200 assert updated.json()["items"] == mixed persisted = json.loads(order_path.read_text(encoding="utf-8")) assert persisted == {"schema_version": 1, "items": mixed} restarted = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) assert restarted.get("/api/library/order").json() == updated.json() stale = put_order(restarted, list(reversed(mixed)), default.json()["revision"]) assert stale.status_code == 409 def test_library_order_rejects_invalid_demo_duplicate_and_changed_sets(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) template = create_template(client, "静态") animation = create_animation(client, "动图") current = client.get("/api/library/order").json() demo = put_order( client, [{"type": "template", "id": DEMO_STATIC_ID}, {"type": "animation", "id": DEMO_ANIMATION_ID}], current["revision"], ) assert demo.status_code == 422 duplicate = put_order( client, [ {"type": "template", "id": template["id"]}, {"type": "template", "id": template["id"]}, {"type": "animation", "id": animation["id"]}, ], current["revision"], ) assert duplicate.status_code == 422 missing = put_order( client, [{"type": "template", "id": template["id"]}], current["revision"], ) assert missing.status_code == 409 extra_field = client.put( "/api/library/order", headers={"If-Match": f'"{current["revision"]}"'}, json={"items": [{"type": "template", "id": template["id"], "name": "bad"}]}, ) assert extra_field.status_code == 422 def test_library_order_appends_new_items_and_filters_deleted_references(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) first = create_template(client, "静态一") animation = create_animation(client, "动图") initial = client.get("/api/library/order").json() ordered = [ {"type": "animation", "id": animation["id"]}, {"type": "template", "id": first["id"]}, ] assert put_order(client, ordered, initial["revision"]).status_code == 200 appended = create_template(client, "静态二") after_create = client.get("/api/library/order").json() assert after_create["items"] == [ *ordered, {"type": "template", "id": appended["id"]}, ] removed = client.delete( f"/api/templates/{first['id']}", headers={"If-Match": f'"{first["revision"]}"'}, ) assert removed.status_code == 200 after_delete = client.get("/api/library/order").json() assert after_delete["items"] == [ {"type": "animation", "id": animation["id"]}, {"type": "template", "id": appended["id"]}, ] @pytest.mark.parametrize( "content", [ b"not json", json.dumps({"schema_version": 2, "items": []}).encode(), json.dumps({"schema_version": 1, "items": [], "extra": True}).encode(), json.dumps({ "schema_version": 1, "items": [ {"type": "template", "id": "11111111-1111-4111-8111-111111111111"}, {"type": "template", "id": "11111111-1111-4111-8111-111111111111"}, ], }).encode(), json.dumps({ "schema_version": 1, "items": [{"type": "template", "id": DEMO_STATIC_ID}], }).encode(), ], ) def test_invalid_library_order_blocks_startup_and_preserves_original(tmp_path, content): path = tmp_path / "data" / "library" / "order.json" path.parent.mkdir(parents=True) path.write_bytes(content) with pytest.raises(LibraryOrderValidationError): create_app(project_root=tmp_path, driver_kind="mock") assert path.read_bytes() == content