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.
124 lines
4.2 KiB
Python
124 lines
4.2 KiB
Python
"""P0.2 app-level tests: health shape, error envelope, request id (APP-30/45/65)."""
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from backend import app as app_module
|
|
|
|
UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
|
|
|
|
|
|
@pytest.fixture
|
|
def client(tmp_path, monkeypatch):
|
|
# Context-manager mode runs the lifespan (startup guards + init_db).
|
|
# SLAB_ENV=dev keeps the mock-safe slice running without store creds;
|
|
# SLAB_DB_PATH drives every per-call connection to the throwaway DB.
|
|
db_path = str(tmp_path / "test.db")
|
|
monkeypatch.setenv("SLAB_DB_PATH", db_path)
|
|
monkeypatch.setenv("SLAB_ENV", "dev")
|
|
app_module.reinit_db(db_path)
|
|
with TestClient(app_module.app) as c:
|
|
yield c
|
|
|
|
|
|
def test_health_200_with_expected_shape(client):
|
|
r = client.get("/api/health")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["status"] == "ok"
|
|
assert body["version"] == app_module.__version__
|
|
assert body["woo_reachable"] is False
|
|
|
|
|
|
def test_404_returns_error_envelope(client):
|
|
r = client.get("/api/v1/slabs/does-not-exist")
|
|
assert r.status_code == 404
|
|
body = r.json()
|
|
assert set(body) == {"error"}
|
|
assert body["error"]["code"] == "404"
|
|
assert body["error"]["message"]
|
|
assert body["error"]["detail"] is None
|
|
|
|
|
|
def test_request_id_assigned_when_absent(client):
|
|
r = client.get("/api/health")
|
|
rid = r.headers.get("X-Request-Id")
|
|
assert rid is not None
|
|
assert UUID_RE.match(rid)
|
|
|
|
|
|
def test_request_id_echoed_when_provided(client):
|
|
r = client.get("/api/health", headers={"X-Request-Id": "req-fixed-123"})
|
|
assert r.headers["X-Request-Id"] == "req-fixed-123"
|
|
|
|
|
|
def test_json_log_line_shape_and_redaction():
|
|
# Formatter is a pure function of the record: assert shape + redaction
|
|
# directly (handler output goes to the pre-pytest stdout, invisible to capsys).
|
|
rec = logging.LogRecord("slab.backend", logging.INFO, __file__, 0,
|
|
"startup: schema ready (idempotent)", None, None)
|
|
parsed = json.loads(app_module.json_formatter.format(rec))
|
|
assert set(parsed) >= {"ts", "level", "logger", "request_id", "msg"}
|
|
assert parsed["logger"] == "slab.backend"
|
|
assert parsed["msg"] == "startup: schema ready (idempotent)"
|
|
|
|
secret = logging.LogRecord("slab.backend", logging.INFO, __file__, 0,
|
|
"woo_app_password=hunter2", None, None)
|
|
parsed = json.loads(app_module.json_formatter.format(secret))
|
|
assert parsed["msg"] == "[REDACTED secret material]"
|
|
|
|
|
|
def _client_with_env(env: dict, tmp_path):
|
|
"""TestClient in lifespan (context-manager) mode with the given env overlay."""
|
|
base = {k: v for k, v in os.environ.items()
|
|
if k not in ("WOO_BASE_URL", "SLAB_AES_KEY", "SLAB_ENV", "SLAB_DB_PATH")}
|
|
base.update(env)
|
|
base["SLAB_DB_PATH"] = str(tmp_path / "guard.db")
|
|
monkeypatch_env = base
|
|
import contextlib
|
|
|
|
@contextlib.contextmanager
|
|
def run():
|
|
saved = dict(os.environ)
|
|
os.environ.clear()
|
|
os.environ.update(monkeypatch_env)
|
|
try:
|
|
app_module.reinit_db(base["SLAB_DB_PATH"])
|
|
with TestClient(app_module.app) as c:
|
|
yield c
|
|
finally:
|
|
os.environ.clear()
|
|
os.environ.update(saved)
|
|
|
|
return run()
|
|
|
|
|
|
def test_startup_guard_refuses_missing_woo_url(tmp_path):
|
|
with pytest.raises(RuntimeError, match="WOO_BASE_URL"):
|
|
with _client_with_env({"SLAB_AES_KEY": "k" * 44}, tmp_path):
|
|
pass
|
|
|
|
|
|
def test_startup_guard_refuses_non_https_woo_url(tmp_path):
|
|
with pytest.raises(RuntimeError, match="https"):
|
|
with _client_with_env({"WOO_BASE_URL": "http://store.example.com",
|
|
"SLAB_AES_KEY": "k" * 44}, tmp_path):
|
|
pass
|
|
|
|
|
|
def test_startup_guard_refuses_missing_aes_key(tmp_path):
|
|
with pytest.raises(RuntimeError, match="SLAB_AES_KEY"):
|
|
with _client_with_env({"WOO_BASE_URL": "https://store.example.com"}, tmp_path):
|
|
pass
|
|
|
|
|
|
def test_startup_guard_warns_in_dev_env(tmp_path):
|
|
with _client_with_env({"SLAB_ENV": "dev"}, tmp_path) as c:
|
|
assert c.get("/api/health").status_code == 200
|