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.
This commit is contained in:
+43
-35
@@ -1,5 +1,4 @@
|
||||
"""S4 Woo client + taxonomy sync tests (mocked transport; never hit the real store)."""
|
||||
|
||||
"""Woo client + taxonomy sync tests (mocked transport; never hit the real store)."""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
@@ -10,6 +9,7 @@ import pytest
|
||||
|
||||
import backend.db as db_mod
|
||||
from backend.woo import (
|
||||
_USER_AGENT,
|
||||
WooAuthError,
|
||||
WooClient,
|
||||
WooConnectionError,
|
||||
@@ -21,21 +21,20 @@ from backend import taxonomy as taxonomy_mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_dir():
|
||||
def db_dir(monkeypatch):
|
||||
tmp = tempfile.mkdtemp(prefix="woo_")
|
||||
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "t.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()
|
||||
conn = db_mod.init_db()
|
||||
monkeypatch.setenv("SLAB_DB_PATH", os.path.join(tmp, "t.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())
|
||||
db_mod.init_db()
|
||||
conn = db_mod.get_connection()
|
||||
from backend.crypto import encrypt
|
||||
|
||||
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("secret"),))
|
||||
conn.commit()
|
||||
yield conn
|
||||
db_mod._reset_for_tests()
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
@@ -46,8 +45,7 @@ def _client(responses):
|
||||
def fake_request(url, method, data, headers):
|
||||
i = min(calls["n"], len(responses) - 1)
|
||||
calls["n"] += 1
|
||||
code, hdr, body = responses[i]
|
||||
return code, hdr, body
|
||||
return responses[i]
|
||||
|
||||
return WooClient("admin", "secret", "https://store.example.com", request=fake_request), calls
|
||||
|
||||
@@ -86,15 +84,47 @@ def test_missing_password_fails_closed(db_dir):
|
||||
WooClient.from_settings("https://store.example.com", "admin", None)
|
||||
|
||||
|
||||
def test_missing_username_fails_closed(db_dir):
|
||||
with pytest.raises(WooCredentialsMissing):
|
||||
WooClient.from_settings("https://store.example.com", None, "enc")
|
||||
|
||||
|
||||
def test_bad_ciphertext_fails_closed(db_dir):
|
||||
with pytest.raises(WooCredentialsMissing):
|
||||
WooClient.from_settings("https://store.example.com", "admin", "!!!not-base64!!!")
|
||||
|
||||
|
||||
def test_401_raises_auth_error(db_dir):
|
||||
def fake_request(url, method, data, headers):
|
||||
raise WooAuthError("authentication rejected (401)")
|
||||
return 401, {}, b'{"message": "Invalid username"}'
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
with pytest.raises(WooAuthError):
|
||||
c.get_categories()
|
||||
|
||||
|
||||
def test_403_raises_forbidden_error(db_dir):
|
||||
def fake_request(url, method, data, headers):
|
||||
return 403, {}, b"{}"
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
with pytest.raises(WooForbiddenError):
|
||||
c.get_categories()
|
||||
|
||||
|
||||
def test_requests_include_user_agent(db_dir):
|
||||
seen_headers = {}
|
||||
|
||||
def fake_request(url, method, data, headers):
|
||||
seen_headers.update(headers)
|
||||
return 200, {"X-WP-TotalPages": "1"}, b"[]"
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
c.get_categories()
|
||||
assert seen_headers.get("User-Agent") == _USER_AGENT
|
||||
assert "SlabUploader" in seen_headers["User-Agent"]
|
||||
|
||||
|
||||
def test_sync_populates_scoped(db_dir):
|
||||
cats = (200, {"X-WP-TotalPages": "1"}, json.dumps([{"id": 1, "name": "Cedar", "slug": "cedar", "parent_id": None, "woo_id": 10}]).encode())
|
||||
tags = (200, {"X-WP-TotalPages": "1"}, json.dumps([
|
||||
@@ -208,25 +238,3 @@ def test_sync_with_slab_species_fk_does_not_fail(db_dir):
|
||||
}
|
||||
assert rows[11] == 10 # parent mapped; row kept for FK
|
||||
assert conn.execute("SELECT species_id FROM slabs WHERE id='slab-fk-1'").fetchone()[0] == 11
|
||||
|
||||
|
||||
def test_403_raises_forbidden_error(db_dir):
|
||||
def fake_request(url, method, data, headers):
|
||||
raise WooForbiddenError("request forbidden (403)")
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
with pytest.raises(WooForbiddenError):
|
||||
c.get_categories()
|
||||
|
||||
|
||||
def test_requests_include_user_agent(db_dir):
|
||||
seen_headers = {}
|
||||
|
||||
def fake_request(url, method, data, headers):
|
||||
seen_headers.update(headers)
|
||||
return 200, {"X-WP-TotalPages": "1"}, b"[]"
|
||||
|
||||
c = WooClient("admin", "secret", "https://store.example.com", request=fake_request)
|
||||
c.get_categories()
|
||||
assert "User-Agent" in seen_headers
|
||||
assert "SlabUploader" in seen_headers["User-Agent"]
|
||||
|
||||
Reference in New Issue
Block a user