engine/migrations/v1_to_v2.py: option sub_category->sub_categories[]; sub-category name slugify (whitespace/grammar chars->_, reserved suffixed, de-duped) cascaded to options + tag_groups; instance category_filter list->boolean-expr string + exclude_null flag; nested @{uuid:a,b,null} refs->@{uuid:a or b!null}. Idempotent. Two entry points share one core: envelope migrate_v1_to_v2 (import/export, registered in _CHAIN) + per-row migrate_module/bundle_v1_to_v2 (lazy/bulk DB path).
Register _CHAIN[1] + bump engine CURRENT_SCHEMA_VERSION 1->2. New SQL migration 016 marches stale DB rows 1->2 at boot via run_bulk_lazy + the per-row migrators (014's documented bump pattern). TS mirror (migrations.ts) + client CURRENT bump follow next (no cross-language version assertion couples them). Update version/entity-count assertions in payload-migration, migration-runner, and export-build tests.
147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
"""Tests for /wp/api/export/build — Task 10 / importer-exporter v2.
|
|
|
|
Endpoint wraps `engine.exporter.build_export_payload` and returns the
|
|
full 8-bucket payload directly. UUID sets are partitioned by bucket;
|
|
mis-typed UUIDs are silently dropped by the exporter (cross-bucket
|
|
isolation guarantee).
|
|
"""
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def _create_wildcard(wp_client, name: str = "colors") -> 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 _create_bundle(wp_client, *, name: str, children=None) -> dict:
|
|
resp = await wp_client.post("/wp/api/bundles", json={
|
|
"name": name,
|
|
"children": children or [],
|
|
})
|
|
assert resp.status == 201
|
|
return await resp.json()
|
|
|
|
|
|
async def _create_template(wp_client, *, name: str = "tpl") -> dict:
|
|
resp = await wp_client.post("/wp/api/templates", json={
|
|
"name": name,
|
|
"template_string": "$subject, $style",
|
|
})
|
|
assert resp.status == 201
|
|
return await resp.json()
|
|
|
|
|
|
# ── Happy path ────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_build_returns_seven_bucket_payload_with_wildcard(wp_client):
|
|
wc = await _create_wildcard(wp_client)
|
|
resp = await wp_client.post("/wp/api/export/build", json={
|
|
"wildcard_uuids": [wc["id"]],
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["schema_version"] == 2
|
|
assert isinstance(body["exported_at"], str)
|
|
assert body["exported_at"] != ""
|
|
# All 8 buckets present.
|
|
for key in (
|
|
"bundles", "wildcards", "fixed_values", "combines",
|
|
"derivations", "constraints", "categories", "templates",
|
|
):
|
|
assert key in body, f"missing bucket: {key}"
|
|
# Wildcard appears in wildcards bucket with non-empty fingerprint.
|
|
assert len(body["wildcards"]) == 1
|
|
assert body["wildcards"][0]["id"] == wc["id"]
|
|
assert body["wildcards"][0].get("snapshot_fingerprint")
|
|
|
|
|
|
async def test_build_with_empty_body_returns_empty_buckets(wp_client):
|
|
resp = await wp_client.post("/wp/api/export/build", json={})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["schema_version"] == 2
|
|
for key in (
|
|
"bundles", "wildcards", "fixed_values", "combines",
|
|
"derivations", "constraints", "categories", "templates",
|
|
):
|
|
assert body[key] == [], f"expected empty bucket: {key}"
|
|
|
|
|
|
async def test_build_transitive_bundle_walk(wp_client):
|
|
"""Bundle A references bundle B via children[type==bundle]; passing
|
|
A's id should pull B in too."""
|
|
b = await _create_bundle(wp_client, name="inner")
|
|
a = await _create_bundle(
|
|
wp_client, name="outer",
|
|
children=[{"id": b["id"], "type": "bundle", "name": "inner"}],
|
|
)
|
|
resp = await wp_client.post("/wp/api/export/build", json={
|
|
"bundle_uuids": [a["id"]],
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
ids = {row["id"] for row in body["bundles"]}
|
|
assert ids == {a["id"], b["id"]}
|
|
|
|
|
|
async def test_build_accepts_template_uuids_and_returns_templates(wp_client):
|
|
tpl = await _create_template(wp_client, name="Hero shot")
|
|
resp = await wp_client.post("/wp/api/export/build", json={
|
|
"template_uuids": [tpl["id"]],
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert len(body["templates"]) == 1
|
|
assert body["templates"][0]["id"] == tpl["id"]
|
|
assert body["templates"][0]["name"] == "Hero shot"
|
|
assert body["templates"][0]["template_string"] == "$subject, $style"
|
|
|
|
|
|
# ── Validation ────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def test_build_invalid_json_returns_400(wp_client):
|
|
resp = await wp_client.post(
|
|
"/wp/api/export/build",
|
|
data="not json",
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_build_non_object_body_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/export/build", json=["x"])
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_build_non_array_uuid_list_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/export/build", json={
|
|
"bundle_uuids": "not a list",
|
|
})
|
|
assert resp.status == 400
|
|
|
|
|
|
# ── Cross-bucket isolation (inherited from exporter) ──────────────────────
|
|
|
|
|
|
async def test_build_wrong_type_uuid_silently_filtered(wp_client):
|
|
"""Passing a wildcard's id under combine_uuids should yield empty
|
|
combines AND empty wildcards — the exporter filters cross-bucket misses."""
|
|
wc = await _create_wildcard(wp_client)
|
|
resp = await wp_client.post("/wp/api/export/build", json={
|
|
"combine_uuids": [wc["id"]],
|
|
})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["combines"] == []
|
|
assert body["wildcards"] == []
|
|
|
|
|