Files
SlabUploader/backend/tests/test_integration_woo_mock.py
T
Ty 49c22c9385 feat(greenfield): rebuild SlabUploader M1-M3 on clean foundation, all gates green
Clean re-implementation of the full product (not a port of the old M3 tree):
- P0 foundation: 13-table schema + per-call sqlite factory (thread-safe),
  FastAPI app (health, JSON logging + secret redaction, CORS, startup guard).
- P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure,
  png crop/inventory/overlay, EXIF decodeOrientedBitmap.
- P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop
  to batch) + photos-ready lineup.
- P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing,
  inference Call1/Call2, review, publish, settings, slabs, photos) + real FE
  review/publish/settings screens consuming the API (no hardcoded Woo ids).
- P4 gate exit: check_requirements + check_coverage both exit 0
  (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green.

Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
2026-09-25 06:57:29 -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_")
monkeypatch.setenv("SLAB_DB_PATH", os.path.join(tmp, "i.db"))
monkeypatch.setenv("SLAB_IMAGES_DIR", tmp)
monkeypatch.setenv("WOO_BASE_URL", "https://store.example.com")
monkeypatch.setenv("SLAB_AES_KEY", base64.b64encode(b"0123456789abcdef0123456789abcdef").decode())
from backend.crypto import encrypt
db_mod.init_db()
conn = db_mod.get_connection()
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
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