2f9b8ebd02
Full-screen WooSetupGate on App mount when this browser lacks device WP user + app password. Save + Test Woo dismisses only after a successful test. Publish/test-woo auth failures reopen the gate. Server: client_for_request requires X-Woo-* on publish, taxonomy sync, and test-woo (401 woo_device_creds_required); health probe keeps Settings. Optional SLAB_WOO_ALLOW_SETTINGS_FALLBACK for legacy tests. See PROTOTYPE.md.
132 lines
6.1 KiB
Python
132 lines
6.1 KiB
Python
"""Exit wave (QUALITY-20, QUALITY-120): full create-to-publish happy path with
|
|
inference OFF, asserting zero inference calls and no raw codes reach the owner.
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
import backend.db as db_mod
|
|
|
|
|
|
@pytest.fixture
|
|
def env(monkeypatch):
|
|
tmp = tempfile.mkdtemp(prefix="exit_")
|
|
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "e.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()
|
|
os.environ["SLAB_WOO_ALLOW_SETTINGS_FALLBACK"] = "1" # legacy tests; prefer X-Woo-* headers
|
|
db_mod._reset_for_tests()
|
|
from backend.crypto import encrypt
|
|
|
|
conn = db_mod.init_db()
|
|
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Black Walnut','black-walnut',NULL,100)")
|
|
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge wood','live-edge-wood',NULL,101)")
|
|
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Edge Type','edge-type',200)")
|
|
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (1,8,'Flat','flat',300)")
|
|
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"),))
|
|
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('inference_enabled','false',NULL)")
|
|
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('content_llm_enabled','false',NULL)")
|
|
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 _mock(monkeypatch, sku_exists=0, product_id=777):
|
|
from backend.woo import WooClient
|
|
|
|
def fake_request(url, method, data, headers):
|
|
if "/products?sku=" in url:
|
|
return 200, {}, json.dumps([{"id": 1}] * sku_exists).encode()
|
|
if url.endswith("/wp-json/wp/v2/media"):
|
|
return 201, {}, json.dumps({"id": 42}).encode()
|
|
if url.endswith("/wp-json/wc/v3/products"):
|
|
return 201, {}, json.dumps({"id": product_id}).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)
|
|
|
|
|
|
def test_full_happy_path_inference_off(env, monkeypatch):
|
|
"""Create slab -> attach photo -> review -> publish, inference OFF.
|
|
Zero inference_log rows; all responses carry plain messages (QUALITY-120)."""
|
|
client, conn = env
|
|
_mock(monkeypatch)
|
|
# 1. create slab
|
|
r = client.post("/api/v1/slabs", json={
|
|
"id": "s1", "sku": "SLAB-UAT-7", "length_in": 114.0, "thickness_in": 1.75, "sqft": 30.0,
|
|
"bdft": 18.4, "width_min_in": 18.0, "width_max_in": 24.3, "width_avg_in": 24.3,
|
|
"species_id": 1, "wood_category_ids": [2], "edge_type_term_id": 1, "figure_term_ids": [],
|
|
"grade_term_id": 1, "thickness_term_id": 1, "moisture_term_id": 1,
|
|
"fig_tag_ids": [], "feat_tag_ids": [], "price": 184.0, "price_source": "recommendation",
|
|
"title": "Black Walnut Live Edge Slab", "short_description": "slab, 9ft 6in",
|
|
"description": "A fine slab.", "content_source": "manual",
|
|
"species_confidence": None, "figure_confidence": None, "grade_confidence": None,
|
|
"inference_status": None, "calibration_photo_id": None,
|
|
})
|
|
assert r.status_code in (201, 200), r.text
|
|
# 2. attach photo
|
|
r = client.post("/api/v1/slabs/s1/photos", data={"photo_id": "p1"},
|
|
files={"original": ("o.png", b"\x89PNG", "image/png")})
|
|
assert r.status_code == 201, r.text
|
|
# 3. review (deterministic copy)
|
|
r = client.post("/api/v1/slabs/s1/review", json={})
|
|
assert r.status_code == 200
|
|
# 4. publish
|
|
r = client.post("/api/v1/slabs/s1/publish")
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["ok"] is True and body["woo_product_id"] == 777 and body["draft"] is True
|
|
# QUALITY-20: zero inference calls in off mode
|
|
assert conn.execute("SELECT COUNT(*) c FROM inference_log").fetchone()["c"] == 0
|
|
# QUALITY-120: response has no raw status codes / stack traces in owner-facing message
|
|
assert "Traceback" not in json.dumps(body)
|
|
# success log
|
|
assert conn.execute("SELECT status FROM sync_log").fetchone()["status"] == "success"
|
|
|
|
|
|
def test_not_ready_publish_422_plain_message(env, monkeypatch):
|
|
"""Failure path returns a plain field-level message, no raw code (QUALITY-120)."""
|
|
client, conn = env
|
|
_mock(monkeypatch)
|
|
client.post("/api/v1/slabs", json={
|
|
"id": "s1", "sku": "SLAB-01", "length_in": 100, "thickness_in": 2, "sqft": 20, "bdft": 8,
|
|
"width_min_in": 10, "width_max_in": 12, "width_avg_in": 11, "species_id": 1,
|
|
"wood_category_ids": [2], "edge_type_term_id": 1, "figure_term_ids": [], "grade_term_id": 1,
|
|
"thickness_term_id": 1, "moisture_term_id": 1, "fig_tag_ids": [], "feat_tag_ids": [],
|
|
"price": None, "price_source": None, "title": "T", "short_description": "S",
|
|
"description": "D", "content_source": "manual",
|
|
"species_confidence": None, "figure_confidence": None, "grade_confidence": None,
|
|
"inference_status": None, "calibration_photo_id": None,
|
|
})
|
|
r = client.post("/api/v1/slabs/s1/publish")
|
|
assert r.status_code == 422
|
|
body = r.json()
|
|
assert "missing" in body["error"]["detail"]
|
|
assert "Traceback" not in json.dumps(body)
|
|
assert "price" in body["error"]["detail"]["missing"]
|