feat(#27): Gate B [07/10]: FastAPI health endpoint + app factory (H-1)

- App factory pattern (FUND-8): create_app() produces fresh app per test
- Logging config (structured, no secrets)
- Config validation: SLAB_AES_KEY required, WOO_BASE_URL HTTPS-only
- GET /api/health returns 200 {status: ok, version, woo_reachable: false}
- SQLite + alembic skeleton: full DATA-MODEL schema, initial migration
- API contract tests via httpx TestClient: 5 cases all green
  - test_returns_200, test_returns_ok_status, test_woo_reachable_false
  - test_response_shape, test_with_test_client_lifespan (FUND-9/10)
This commit is contained in:
grokbot_ty
2026-09-07 07:53:36 +00:00
committed by grokbot_ty
parent d5d8c49e04
commit e5514fe3bf
29 changed files with 915 additions and 0 deletions
View File
+45
View File
@@ -0,0 +1,45 @@
"""Shared test fixtures for all backend tests.
App factory pattern (FUND-8): each test gets a fresh app instance.
FUND-9: TestClient manages lifespan.
FUND-10: dependency_overrides cleared after every test.
"""
import pytest
from app.config import Settings
from app.db import engine
from app.main import create_app
@pytest.fixture(autouse=True)
def _test_db() -> None:
"""Set up a fresh in-memory SQLite database for each test."""
engine.set_test_url("sqlite+aiosqlite:///:memory:")
yield
engine._test_url = None
engine._engine = None
engine._session_factory = None
@pytest.fixture
def settings() -> Settings:
"""Create test settings with in-memory SQLite."""
return Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk", # 32 bytes base64
woo_base_url="https://test.example.com",
app_hostname="test.local",
)
@pytest.fixture
def app(settings: Settings) -> None:
"""Yield a fresh FastAPI app with test settings, then clean up.
Each test gets its own app instance (FUND-8).
TestClient manages the lifespan (FUND-9).
dependency_overrides are cleared after each test (FUND-10).
"""
app_instance = create_app(settings)
yield app_instance
app_instance.dependency_overrides.clear()
View File
+113
View File
@@ -0,0 +1,113 @@
"""H-1: Health endpoint contract test.
Tests via httpx TestClient. Each test gets a fresh app instance (FUND-8).
H-1: GET /api/health returns 200 {status: ok} with woo_reachable false
when credentials are not configured.
FUND-8: App factory creates fresh instance per test.
FUND-9: TestClient context manager manages lifespan.
FUND-10: dependency_overrides cleared after every test.
"""
from fastapi.testclient import TestClient
from app.main import create_app
class TestHealthEndpoint:
"""GET /api/health contract tests."""
def test_returns_200(self) -> None:
"""H-1: Health endpoint returns 200."""
from app.config import Settings
settings = Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
woo_base_url="https://test.example.com",
)
app = create_app(settings)
client = TestClient(app)
response = client.get("/api/health")
assert response.status_code == 200
client.close()
app.dependency_overrides.clear()
def test_returns_ok_status(self) -> None:
"""H-1: Health endpoint returns status ok."""
from app.config import Settings
settings = Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
woo_base_url="https://test.example.com",
)
app = create_app(settings)
client = TestClient(app)
response = client.get("/api/health")
data = response.json()
assert data["status"] == "ok"
client.close()
app.dependency_overrides.clear()
def test_woo_reachable_false_when_credentials_not_configured(self) -> None:
"""H-1: woo_reachable is false when Woo credentials not configured."""
from app.config import Settings
settings = Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
woo_base_url="https://test.example.com",
)
app = create_app(settings)
client = TestClient(app)
response = client.get("/api/health")
data = response.json()
assert data["woo_reachable"] is False
client.close()
app.dependency_overrides.clear()
def test_response_shape(self) -> None:
"""H-1: Response contains exactly status, version, and woo_reachable."""
from app.config import Settings
settings = Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
woo_base_url="https://test.example.com",
)
app = create_app(settings)
client = TestClient(app)
response = client.get("/api/health")
data = response.json()
assert "status" in data
assert "version" in data
assert "woo_reachable" in data
assert isinstance(data["status"], str)
assert isinstance(data["version"], str)
assert isinstance(data["woo_reachable"], bool)
client.close()
app.dependency_overrides.clear()
def test_with_test_client_lifespan(self) -> None:
"""FUND-9: TestClient with app manages lifespan correctly."""
from app.config import Settings
settings = Settings(
aes_key="dGVzdC1rZXktZm9yLW1hbGxlb3AtdGVzdGluZy1vbmx5LXNob3VsZC1iZS11c2Vk",
woo_base_url="https://test.example.com",
)
app = create_app(settings)
with TestClient(app) as client:
response = client.get("/api/health")
assert response.status_code == 200
assert response.json()["status"] == "ok"
app.dependency_overrides.clear()
View File