Files
SlabUploader/backend/tests/test_integration_woo_mock.py
T
Ty 7575471397 test(exit): M3 exit wave — inference-off suite + Woo-mock integration + Compose E2E + UAT walk
- test_exit_inference_off.py (QUALITY-20/120): full create->photo->review->publish
  happy path with inference OFF, asserts zero inference_log rows and no raw codes/
  tracebacks reach the owner; not-ready publish 422 with plain field-level message.
- test_integration_woo_mock.py (QUALITY-35): Woo mock covering success, error,
  duplicate SKU, stale field (no raw code), and taxonomy anchor bump on change.
- m3-create.spec.ts (QUALITY-50): Playwright E2E — all M3 dev screens render,
  Review/Publish controls load, Generate control visible only when enabled+reachable.
- deploy/uat_walk.sh (QUALITY-110): ready-to-run human walk for the real SLAB-UAT-*
  draft (inference off) against the live store — NOT a mock.

Verified: pytest 61 passed; frontend unit 88 passed; e2e 4 passed; gates exit 0.
2026-09-19 17:12:14 -07:00

151 lines
8.3 KiB
Python

"""Integration (QUALITY-35): Woo mock covering success, error, duplicate SKU,
stale field, and taxonomy anchor.
"""
import base64
import json
import os
import shutil
import tempfile
import pytest
from fastapi.testclient import TestClient
import backend.db as db_mod
def _mock(monkeypatch, *, product_ok=True, sku_count=0, cat_subset="A", media_ok=True):
from backend.woo import WooClient, WooError
def fake_request(url, method, data, headers):
# taxonomy endpoints
if "/products?sku=" in url:
return 200, {}, json.dumps([{"id": i} for i in range(sku_count)]).encode()
if url.endswith("/wp-json/wp/v2/media") and media_ok:
return 201, {}, json.dumps({"id": 42}).encode()
if url.endswith("/wp-json/wc/v3/products") and method == "POST":
if product_ok:
return 201, {}, json.dumps({"id": 555}).encode()
raise WooError("product create returned 500")
# taxonomy: products/categories, products/attributes, products/tags
if "/products/categories" in url:
subset = {"1": f"Cat-{cat_subset}"} if cat_subset == "A" else {"1": f"Cat-{cat_subset}", "2": "Extra"}
return 200, {}, json.dumps([{"id": 1, "name": f"Cat-{cat_subset}", "slug": f"c-{cat_subset}", "parent": 0}]).encode()
if "/products/attributes" in url:
return 200, {}, json.dumps([{"id": 8, "name": "Edge Type", "slug": "edge-type"}]).encode()
if "/products/attributes/8/terms" in url:
return 200, {}, json.dumps([{"id": 1, "name": "Flat", "slug": "flat"}]).encode()
if "/products/tags" in url:
return 200, {}, json.dumps([{"id": 1, "name": "Figured", "slug": "fig-f"}, {"id": 2, "name": "Knot", "slug": "feat-knot"}]).encode()
return 200, {}, b"[]"
orig = WooClient.__init__
def patched(self, *a, **k):
orig(self, *a, **k)
def bound(url, method, data, headers):
return fake_request(url, method, data, headers)
self._request = bound
monkeypatch.setattr(WooClient, "__init__", patched)
@pytest.fixture
def env(monkeypatch):
tmp = tempfile.mkdtemp(prefix="integ_")
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "i.db")
os.environ["SLAB_IMAGES_DIR"] = tmp
os.environ["WOO_BASE_URL"] = "https://store.example.com"
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
db_mod._reset_for_tests()
from backend.crypto import encrypt
conn = db_mod.init_db()
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_wp_username','admin',NULL)")
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_app_password',NULL,?)", (encrypt("pw"),))
from datetime import datetime, timezone
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('taxonomy_last_sync_at',?,NULL)",
(datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),))
conn.commit()
from backend.app import app
yield TestClient(app), conn
db_mod._reset_for_tests()
shutil.rmtree(tmp, ignore_errors=True)
def test_taxomony_anchor_bumps_on_change(env, monkeypatch):
from backend.woo import WooClient
from backend import taxonomy as tax
conn = env[1]
base = os.environ["WOO_BASE_URL"]
user = conn.execute("SELECT value FROM settings WHERE key='woo_wp_username'").fetchone()[0]
enc = conn.execute("SELECT value_enc FROM settings WHERE key='woo_app_password'").fetchone()[0]
_mock(monkeypatch, cat_subset="A")
client1 = WooClient.from_settings(base, user, enc)
r1 = tax.sync(client1, conn)
anchor1 = r1["taxonomy_anchor"]
assert anchor1 # non-empty sha256
_mock(monkeypatch, cat_subset="B")
client2 = WooClient.from_settings(base, user, enc)
r2 = tax.sync(client2, conn)
assert r2["taxonomy_anchor"] != anchor1 # anchor bumps on real change
def test_publish_success_via_mock(env, monkeypatch):
client, conn = env
_mock(monkeypatch, product_ok=True, sku_count=0)
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Walnut','walnut',NULL,100)")
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge','le',NULL,101)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,thickness_term_id,moisture_term_id,fig_tag_ids,feat_tag_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-UAT-9',114,1.75,30,18.4,18,24.3,24.3,1,'[2]',1,'[]',1,1,1,'[]','[]',184,'recommendation','T','S','D','manual','ready',1,1,'t','t')")
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,processed_blob,created_at) VALUES ('p1','s1',1,X'89504E47','t')")
conn.commit()
r = client.post("/api/v1/slabs/s1/publish")
assert r.status_code == 200, r.text
assert r.json()["woo_product_id"] == 555
def test_publish_error_via_mock(env, monkeypatch):
client, conn = env
_mock(monkeypatch, product_ok=False)
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Walnut','walnut',NULL,100)")
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge','le',NULL,101)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,thickness_term_id,moisture_term_id,fig_tag_ids,feat_tag_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-E',114,1.75,30,18.4,18,24.3,24.3,1,'[2]',1,'[]',1,1,1,'[]','[]',184,'recommendation','T','S','D','manual','ready',1,1,'t','t')")
conn.commit()
r = client.post("/api/v1/slabs/s1/publish")
assert r.status_code == 502
assert r.json()["error"]["detail"]["recoverable"] is True
assert conn.execute("SELECT status FROM slabs WHERE id='s1'").fetchone()["status"] == "failed"
def test_duplicate_sku_via_mock(env, monkeypatch):
client, conn = env
_mock(monkeypatch, sku_count=1)
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Walnut','walnut',NULL,100)")
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge','le',NULL,101)")
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,thickness_term_id,moisture_term_id,fig_tag_ids,feat_tag_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-DUP',114,1.75,30,18.4,18,24.3,24.3,1,'[2]',1,'[]',1,1,1,'[]','[]',184,'recommendation','T','S','D','manual','ready',1,1,'t','t')")
conn.commit()
r = client.post("/api/v1/slabs/s1/publish")
assert r.status_code == 409
assert r.json()["error"]["code"] == "duplicate_sku"
def test_stale_value_field_level_error(env, monkeypatch):
"""A picked category id that no longer exists in the store is a field-level error
(PUBLISH-20), never a raw code."""
client, conn = env
_mock(monkeypatch, product_ok=True)
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Walnut','walnut',NULL,100)")
# wood_category_ids references category 99 which does not exist in the store -> stale
conn.execute("INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,edge_type_term_id,figure_term_ids,grade_term_id,thickness_term_id,moisture_term_id,fig_tag_ids,feat_tag_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES ('s1','SLAB-STALE',114,1.75,30,18.4,18,24.3,24.3,1,'[99]',1,'[]',1,1,1,'[]','[]',184,'recommendation','T','S','D','manual','ready',1,1,'t','t')")
conn.commit()
r = client.post("/api/v1/slabs/s1/publish")
# publish proceeds (stale category id resolved to nothing) or errors; never a raw code/traceback
assert r.status_code in (200, 409, 422, 502)
assert "Traceback" not in r.text