49c22c9385
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.
151 lines
8.3 KiB
Python
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
|