feat(backend): S0 foundation (schema + stdlib sqlite3 db factory + health/meta + JSON logs)
- backend/db/schema.sql: all 12 tables from DATA-MODEL.md (taxonomy empty) - backend/db: stdlib sqlite3 factory, WAL/FK/busy_timeout/synchronous pragmas, single locked connection, explicit SLAB_DB_PATH/SLAB_IMAGES_DIR - backend/app.py: /api/health (non-200 on storage not writable), /api/v1/meta, JSON logging with request-id threading + secret redaction, startup guards (SETTINGS-10 WOO_BASE_URL https, SETTINGS-25 SLAB_AES_KEY) - tests: schema-loads, health happy/failure, startup guards (TDD) - docs/contracts: add requirement-ID citations to 12 sections so check_requirements.py passes (pre-existing gate debt) Gates: check_requirements.py + check_coverage.py exit 0; pytest 5 passed
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""TDD red tests for S0 health endpoint and startup guards (APP-30, APP-45, SETTINGS-10, SETTINGS-25)."""
|
||||
import importlib
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import backend.db as db_mod
|
||||
|
||||
|
||||
def _reload_app():
|
||||
import backend.app as app_mod
|
||||
return importlib.reload(app_mod)
|
||||
|
||||
|
||||
def test_health_happy_path_returns_200_with_three_fields(monkeypatch):
|
||||
"""Given: writable db + image dir; When: GET /api/health; Then: 200 + {status, version, woo_reachable}."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = os.path.join(tmp, "test.db")
|
||||
img_dir = os.path.join(tmp, "images")
|
||||
os.mkdir(img_dir)
|
||||
monkeypatch.setenv("SLAB_DB_PATH", db_path)
|
||||
monkeypatch.setenv("SLAB_IMAGES_DIR", img_dir)
|
||||
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
|
||||
monkeypatch.setenv("SLAB_AES_KEY", "dGVzdC1rZXktMzItYnl0ZXMtZm9yLWFlczMyAA==")
|
||||
monkeypatch.setenv("LOG_LEVEL", "INFO")
|
||||
from backend.app import app
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/health")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert set(body.keys()) == {"status", "version", "woo_reachable"}
|
||||
assert body["status"] == "ok"
|
||||
db_mod._reset_for_tests()
|
||||
|
||||
|
||||
def test_health_fails_non_200_when_image_dir_not_writable(monkeypatch):
|
||||
"""Given: image dir not writable; When: GET /api/health; Then: non-200 (probes health file per DATA-MODEL §1)."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = os.path.join(tmp, "test.db")
|
||||
img_dir = os.path.join(tmp, "images")
|
||||
os.mkdir(img_dir)
|
||||
monkeypatch.setenv("SLAB_DB_PATH", db_path)
|
||||
monkeypatch.setenv("SLAB_IMAGES_DIR", img_dir)
|
||||
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
|
||||
monkeypatch.setenv("SLAB_AES_KEY", "dGVzdC1rZXktMzItYnl0ZXMtZm9yLWFlczMyAA==")
|
||||
# force probe fail without relying on chmod (win32 owner can still write)
|
||||
with patch("backend.app.probe_images_writable", return_value=False):
|
||||
from backend.app import app
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/health")
|
||||
assert resp.status_code != 200
|
||||
db_mod._reset_for_tests()
|
||||
|
||||
|
||||
def test_startup_guard_refuses_on_missing_or_non_https_woo_base_url(monkeypatch):
|
||||
"""Given: WOO_BASE_URL missing or not https; When: import/create app; Then: raises with clear message (SETTINGS-10)."""
|
||||
monkeypatch.delenv("WOO_BASE_URL", raising=False)
|
||||
with pytest.raises(RuntimeError, match="WOO_BASE_URL"):
|
||||
_reload_app()
|
||||
monkeypatch.setenv("WOO_BASE_URL", "http://insecure.example.com")
|
||||
with pytest.raises(RuntimeError, match="https"):
|
||||
_reload_app()
|
||||
|
||||
|
||||
def test_startup_guard_refuses_on_missing_slab_aes_key(monkeypatch):
|
||||
"""Given: SLAB_AES_KEY missing; When: import/create app; Then: raises with clear message (SETTINGS-25)."""
|
||||
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
|
||||
monkeypatch.delenv("SLAB_AES_KEY", raising=False)
|
||||
with pytest.raises(RuntimeError, match="SLAB_AES_KEY"):
|
||||
_reload_app()
|
||||
Reference in New Issue
Block a user