Add two new POST handlers in wp_api/import_export.py wrapping the
engine.importer.commit_import / undo_import functions from Task 13.
- /wp/api/import/commit: body {adds, replaces, renames}; 200 with
{ok, undo_entry_id, summary} on success; 400 on contract/DB errors.
- /wp/api/import/undo: body {undo_entry_id}; 200 with {ok: True} on
success; 404 on unknown entry; 400 on shape/other errors.
- Both guard request body via validate_body_size (5 MB cap).
- Engine returns undo_id; handler renames to undo_entry_id at the
HTTP boundary to keep the locked endpoint contract decoupled from
the engine field name.
- Legacy export_bundle + import_bundle + new export_build handlers
remain unchanged.
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""Tests for POST /wp/api/import/undo — Task 14 / importer-exporter v2.
|
|
|
|
Endpoint wraps ``engine.importer.undo_import``. Body shape:
|
|
``{"undo_entry_id": str}``. Success returns ``{"ok": True}`` (200).
|
|
Missing / non-string ``undo_entry_id`` → 400. Unknown
|
|
``undo_entry_id`` → **404** (clean miss). Other engine failures → 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,
|
|
}
|
|
|
|
|
|
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 _commit_add(wp_client, entity: dict, kind: str = "wildcard") -> str:
|
|
"""Commit an add, return the resulting undo_entry_id."""
|
|
resp = await wp_client.post("/wp/api/import/commit", json={
|
|
"adds": [{"kind": kind, "entity": entity}],
|
|
"replaces": [],
|
|
"renames": [],
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
return body["undo_entry_id"]
|
|
|
|
|
|
# ── Happy paths ───────────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_undo_reverses_add(wp_client):
|
|
undo_id = await _commit_add(wp_client, _wildcard_entity("aabbccdd"))
|
|
# Sanity: module present before undo.
|
|
g = await wp_client.get("/wp/api/modules/aabbccdd")
|
|
assert g.status == 200
|
|
|
|
resp = await wp_client.post("/wp/api/import/undo", json={
|
|
"undo_entry_id": undo_id,
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["ok"] is True
|
|
|
|
# Confirm the module is gone.
|
|
g2 = await wp_client.get("/wp/api/modules/aabbccdd")
|
|
assert g2.status == 404
|
|
|
|
|
|
async def test_undo_restores_replaced_content(wp_client):
|
|
seed = await _seed_wildcard(wp_client, name="original")
|
|
sid = seed["id"]
|
|
original_desc = seed.get("description", "")
|
|
original_tags = seed.get("tags", [])
|
|
|
|
# Replace the module — the previous content goes into the undo snapshot.
|
|
commit_resp = await wp_client.post("/wp/api/import/commit", json={
|
|
"adds": [],
|
|
"replaces": [{
|
|
"kind": "wildcard",
|
|
"id": sid,
|
|
"new_content": {
|
|
"name": "mutated",
|
|
"description": "was changed",
|
|
"category_id": None,
|
|
"tags": ["mutated"],
|
|
"is_favorite": False,
|
|
"payload": {"options": []},
|
|
"version": 1,
|
|
},
|
|
}],
|
|
"renames": [],
|
|
})
|
|
assert commit_resp.status == 200
|
|
undo_id = (await commit_resp.json())["undo_entry_id"]
|
|
|
|
# Pre-undo state reflects the replace.
|
|
mid_state = await (await wp_client.get(f"/wp/api/modules/{sid}")).json()
|
|
assert mid_state["name"] == "mutated"
|
|
|
|
# Undo and verify the pre-replace state is bit-restored.
|
|
undo_resp = await wp_client.post("/wp/api/import/undo", json={
|
|
"undo_entry_id": undo_id,
|
|
})
|
|
assert undo_resp.status == 200
|
|
body = await undo_resp.json()
|
|
assert body["ok"] is True
|
|
|
|
restored = await (await wp_client.get(f"/wp/api/modules/{sid}")).json()
|
|
assert restored["name"] == "original"
|
|
assert restored["description"] == original_desc
|
|
assert restored["tags"] == original_tags
|
|
|
|
|
|
# ── Validation ────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_undo_invalid_json_returns_400(wp_client):
|
|
resp = await wp_client.post(
|
|
"/wp/api/import/undo",
|
|
data="not json",
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_undo_non_object_body_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/import/undo", json=["x"])
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_undo_missing_field_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/import/undo", json={})
|
|
assert resp.status == 400
|
|
body = await resp.json()
|
|
assert "required" in body["error"]
|
|
|
|
|
|
async def test_undo_non_string_id_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/import/undo", json={
|
|
"undo_entry_id": 123,
|
|
})
|
|
assert resp.status == 400
|
|
body = await resp.json()
|
|
assert "string" in body["error"]
|
|
|
|
|
|
# ── Missing entry → 404 (clean miss) ──────────────────────────────────────
|
|
|
|
|
|
async def test_undo_unknown_entry_id_returns_404(wp_client):
|
|
resp = await wp_client.post("/wp/api/import/undo", json={
|
|
"undo_entry_id": "doesnotexist",
|
|
})
|
|
assert resp.status == 404
|
|
body = await resp.json()
|
|
assert "not found" in body["error"]
|