Files
DumiFlex-ComfyUI-Wildcard-P…/tests/wp_api/test_import_commit.py
T

304 lines
10 KiB
Python

"""Tests for POST /wp/api/import/commit — Task 14 / importer-exporter v2.
Endpoint wraps ``engine.importer.commit_import``. Body buckets ``adds``,
``replaces``, ``renames`` are applied atomically. Success returns
``{"ok": True, "undo_entry_id": str, "summary": {...}}`` with status
200; contract violations and wrapped DB errors return
``{"error": str}`` with status 400.
"""
import pytest
pytestmark = pytest.mark.asyncio
# ── helpers ────────────────────────────────────────────────────────────────
def _wildcard_entity(id_: str, name: str = "color") -> dict:
return {
"id": id_,
"type": "wildcard",
"name": name,
"description": "",
"category_id": None,
"tags": [],
"is_favorite": False,
"payload": {"options": [{"id": "r", "value": "red", "weight": 1}]},
"version": 1,
}
def _bundle_entity(id_: str, name: str = "pack") -> dict:
return {
"id": id_,
"name": name,
"description": "",
"color": None,
"category_id": None,
"tags": [],
"is_favorite": False,
"children": [],
"version": 1,
}
def _category_entity(id_: str, name: str) -> dict:
return {
"id": id_, "name": name, "color": None, "icon": None, "sort_order": 0,
}
def _template_entity(id_: str, name: str = "tpl") -> dict:
return {
"id": id_, "name": name, "description": "",
"category_id": None, "tags": [], "is_favorite": False,
"template_string": "$subject, $style",
}
async def _seed_wildcard(wp_client, name: str = "seed") -> dict:
resp = await wp_client.post("/wp/api/modules", json={
"type": "wildcard", "name": name, "description": "",
"category_id": None, "tags": [], "payload": {"options": []},
})
assert resp.status == 201
return await resp.json()
async def _seed_category(wp_client, name: str = "Style") -> dict:
resp = await wp_client.post("/wp/api/categories", json={
"name": name, "color": None, "icon": None,
})
assert resp.status == 201
return await resp.json()
# ── Happy paths ───────────────────────────────────────────────────────────
async def test_commit_add_wildcard(wp_client):
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "wildcard", "entity": _wildcard_entity("aabbccdd")}],
"replaces": [],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
assert isinstance(body["undo_entry_id"], str)
assert body["undo_entry_id"] # non-empty
# Confirm the module landed in the DB at the supplied id.
g = await wp_client.get("/wp/api/modules/aabbccdd")
assert g.status == 200
row = await g.json()
assert row["type"] == "wildcard"
assert row["name"] == "color"
async def test_commit_replace_wildcard(wp_client):
seed = await _seed_wildcard(wp_client, name="before")
sid = seed["id"]
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [],
"replaces": [{
"kind": "wildcard",
"id": sid,
"new_content": {
"name": "after",
"description": "renamed via commit",
"category_id": None,
"tags": ["edited"],
"is_favorite": False,
"payload": {"options": [{"id": "r", "value": "red", "weight": 1}]},
"version": 1,
},
}],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
g = await wp_client.get(f"/wp/api/modules/{sid}")
assert g.status == 200
row = await g.json()
assert row["name"] == "after"
assert row["description"] == "renamed via commit"
assert row["tags"] == ["edited"]
async def test_commit_add_bundle(wp_client):
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "bundle", "entity": _bundle_entity("ccddee11", name="pack")}],
"replaces": [],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
assert body["undo_entry_id"]
g = await wp_client.get("/wp/api/bundles/ccddee11")
assert g.status == 200
row = await g.json()
assert row["name"] == "pack"
async def test_commit_add_template(wp_client):
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "template", "entity": _template_entity("ttee1122")}],
"replaces": [],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
assert body["undo_entry_id"]
g = await wp_client.get("/wp/api/templates/ttee1122")
assert g.status == 200
row = await g.json()
assert row["name"] == "tpl"
assert row["template_string"] == "$subject, $style"
async def test_commit_add_category_name_merge(wp_client):
"""Pre-seed category with name 'Style' (id derived: 'style'). Commit
another 'Style'-named category at a different id. Engine's name-based
merge collapses it — no new row, no error."""
await _seed_category(wp_client, name="Style")
# Sanity: one category present pre-commit.
listing = await (await wp_client.get("/wp/api/categories")).json()
assert len(listing["items"]) == 1
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{
"kind": "category",
"entity": _category_entity("other_id", "STYLE"), # case-insensitive
}],
"replaces": [],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
# Still exactly one category — the engine recorded a skip, not an insert.
after = await (await wp_client.get("/wp/api/categories")).json()
assert len(after["items"]) == 1
async def test_commit_add_fixed_values_module(wp_client):
"""Smoke test for a non-wildcard module kind — exercises the
`_is_module_kind` dispatch for fixed_values through the handler."""
entity = {
"id": "fff10001", "type": "fixed_values", "name": "fv",
"description": "", "category_id": None, "tags": [],
"is_favorite": False,
"payload": {"values": [{"id": "v1", "name": "lens", "value": "wide"}]},
"version": 1,
}
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "fixed_values", "entity": entity}],
"replaces": [],
"renames": [],
})
assert resp.status == 200
body = await resp.json()
assert body["ok"] is True
g = await wp_client.get("/wp/api/modules/fff10001")
assert g.status == 200
row = await g.json()
assert row["type"] == "fixed_values"
# ── Validation ────────────────────────────────────────────────────────────
async def test_commit_invalid_json_returns_400(wp_client):
resp = await wp_client.post(
"/wp/api/import/commit",
data="not json",
headers={"content-type": "application/json"},
)
assert resp.status == 400
async def test_commit_non_object_body_returns_400(wp_client):
resp = await wp_client.post("/wp/api/import/commit", json=["x"])
assert resp.status == 400
async def test_commit_non_list_bucket_returns_400(wp_client):
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": "not a list",
})
assert resp.status == 400
body = await resp.json()
assert "adds" in body["error"]
# ── Contract violations → 400 ─────────────────────────────────────────────
async def test_commit_missing_entity_field_returns_400(wp_client):
"""Wildcard add missing 'name' — engine raises _ImporterContractError."""
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "wildcard", "entity": {"id": "aabbccdd"}}],
"replaces": [],
"renames": [],
})
assert resp.status == 400
body = await resp.json()
# Message should name the missing field rather than leaking SQL.
assert "name" in body["error"]
assert "wildcard" in body["error"]
async def test_commit_id_collision_on_add_returns_400(wp_client):
"""Pre-seed a module then commit an add at the same id."""
seed = await _seed_wildcard(wp_client)
sid = seed["id"]
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "wildcard", "entity": _wildcard_entity(sid)}],
"replaces": [],
"renames": [],
})
assert resp.status == 400
body = await resp.json()
assert "collision" in body["error"]
# The colliding id must appear in the message so the SPA can highlight it.
assert sid in body["error"]
async def test_commit_unknown_kind_returns_400(wp_client):
"""Garbage kind values trip the unknown-kind branch in the engine."""
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [{"kind": "nonsense", "entity": _wildcard_entity("aabbccdd")}],
"replaces": [],
"renames": [],
})
assert resp.status == 400
body = await resp.json()
assert "unknown kind" in body["error"]
async def test_commit_rolls_back_on_partial_failure(wp_client):
"""Two adds in one commit; the second collides. The first must
NOT land — the engine's `with conn:` rolls back the whole batch."""
resp = await wp_client.post("/wp/api/import/commit", json={
"adds": [
{"kind": "wildcard", "entity": _wildcard_entity("11112222")},
# Same id collides with the first — triggers rollback.
{"kind": "wildcard", "entity": _wildcard_entity("11112222", name="dup")},
],
"replaces": [],
"renames": [],
})
assert resp.status == 400
# Confirm the first add was rolled back.
g = await wp_client.get("/wp/api/modules/11112222")
assert g.status == 404