"""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