SLICE-4 (part 1): deploy contract — versioned health, backend-served SPA, SmtpMailer + honest 502 (SD-0001 §7.2) #10
+12
-2
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import psycopg
|
import psycopg
|
||||||
@@ -26,6 +27,15 @@ from app.platform.mailer import Mailer
|
|||||||
from app.platform import session as session_mod
|
from app.platform import session as session_mod
|
||||||
|
|
||||||
|
|
||||||
|
# The repo-root VERSION file is the single version source: the deploy pin checks out its
|
||||||
|
# tag, and /healthz must report it back (flotilla-core's verify gate compares them).
|
||||||
|
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
try:
|
||||||
|
_APP_VERSION = (_REPO_ROOT / "VERSION").read_text().strip()
|
||||||
|
except OSError:
|
||||||
|
_APP_VERSION = "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
class RequestCodeBody(BaseModel):
|
class RequestCodeBody(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
|
|
||||||
@@ -94,7 +104,7 @@ def create_app(database_url: str | None = None) -> FastAPI:
|
|||||||
finally:
|
finally:
|
||||||
app.state.pool.close()
|
app.state.pool.close()
|
||||||
|
|
||||||
app = FastAPI(title="ecomm", version="0.3", lifespan=lifespan)
|
app = FastAPI(title="ecomm", version=_APP_VERSION, lifespan=lifespan)
|
||||||
|
|
||||||
@app.get("/healthz")
|
@app.get("/healthz")
|
||||||
def healthz(response: Response, conn: psycopg.Connection = Depends(get_conn)):
|
def healthz(response: Response, conn: psycopg.Connection = Depends(get_conn)):
|
||||||
@@ -108,7 +118,7 @@ def create_app(database_url: str | None = None) -> FastAPI:
|
|||||||
if pending:
|
if pending:
|
||||||
response.status_code = 503
|
response.status_code = 503
|
||||||
return {"status": "unavailable", "reason": "migrations_pending"}
|
return {"status": "unavailable", "reason": "migrations_pending"}
|
||||||
return {"status": "ok"}
|
return {"status": "ok", "version": _APP_VERSION}
|
||||||
|
|
||||||
@app.post("/api/auth/request-code")
|
@app.post("/api/auth/request-code")
|
||||||
def request_code(
|
def request_code(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def test_inv_1_bootstrap_whole_flow_from_empty(fresh_db_url):
|
|||||||
# fresh_db_url is a brand-new empty database; create_app() self-migrates (INV-7).
|
# fresh_db_url is a brand-new empty database; create_app() self-migrates (INV-7).
|
||||||
with TestClient(create_app(database_url=fresh_db_url)) as client:
|
with TestClient(create_app(database_url=fresh_db_url)) as client:
|
||||||
# a fresh deployment serves healthz green before any row exists
|
# a fresh deployment serves healthz green before any row exists
|
||||||
assert client.get("/healthz").json() == {"status": "ok"}
|
assert client.get("/healthz").json()["status"] == "ok"
|
||||||
|
|
||||||
# PUC-2: first visitor requests a code; it reaches them via the dev channel
|
# PUC-2: first visitor requests a code; it reaches them via the dev channel
|
||||||
assert client.post(
|
assert client.post(
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
|
|
||||||
|
# /healthz reports the VERSION file's value — the flotilla deploy gate compares
|
||||||
|
# body.version against the pinned target (flotilla-core SPEC §8.1 phase 8).
|
||||||
|
_VERSION = (Path(__file__).resolve().parents[2] / "VERSION").read_text().strip()
|
||||||
|
|
||||||
|
|
||||||
def test_healthz_ok_on_migrated_empty_db(fresh_db_url):
|
def test_healthz_ok_on_migrated_empty_db(fresh_db_url):
|
||||||
app = create_app(database_url=fresh_db_url)
|
app = create_app(database_url=fresh_db_url)
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
resp = client.get("/healthz")
|
resp = client.get("/healthz")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json() == {"status": "ok"}
|
assert resp.json() == {"status": "ok", "version": _VERSION}
|
||||||
|
|
||||||
|
|
||||||
def test_startup_migrates_from_empty(fresh_db_url):
|
def test_startup_migrates_from_empty(fresh_db_url):
|
||||||
|
|||||||
Reference in New Issue
Block a user