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