SLICE-4 (part 1): deploy contract — versioned health, backend-served SPA, SmtpMailer + honest 502 (SD-0001 §7.2) #10

Merged
ben.stull merged 5 commits from slice-4-deploy-contract into main 2026-06-11 06:36:27 +00:00
2 changed files with 33 additions and 1 deletions
Showing only changes of commit 9f4295b77e - Show all commits
+9 -1
View File
@@ -17,6 +17,7 @@ from typing import Any
import psycopg import psycopg
from fastapi import Depends, FastAPI, Response from fastapi import Depends, FastAPI, Response
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from app.domains import accounts, storefronts from app.domains import accounts, storefronts
@@ -89,7 +90,7 @@ def _set_session_cookie(response: Response, account: accounts.Account) -> None:
) )
def create_app(database_url: str | None = None) -> FastAPI: def create_app(database_url: str | None = None, static_dir: str | Path | None = None) -> FastAPI:
_ensure_app_logging() _ensure_app_logging()
dsn = database_url or config.database_url() dsn = database_url or config.database_url()
@@ -202,6 +203,13 @@ def create_app(database_url: str | None = None) -> FastAPI:
) )
return JSONResponse(status_code=201, content={"id": sf.id, "name": sf.name}) return JSONResponse(status_code=201, content={"id": sf.id, "name": sf.name})
# Deployed topology (launch-app SPEC §2): nginx proxies everything here, so the
# backend serves the built SPA. Mounted LAST so /healthz and /api/* win. In dev the
# dist dir doesn't exist (Vite serves the frontend) and the mount is skipped.
spa_dir = Path(static_dir) if static_dir is not None else _REPO_ROOT / "frontend" / "dist"
if (spa_dir / "index.html").is_file():
app.mount("/", StaticFiles(directory=spa_dir, html=True), name="spa")
return app return app
+24
View File
@@ -0,0 +1,24 @@
"""Deployed topology (launch-app SPEC §2): nginx proxies EVERYTHING to the backend, so
the backend must serve the built SPA (frontend/dist) itself. Dev is unaffected — Vite
serves the frontend and the default dist dir simply doesn't exist."""
from fastapi.testclient import TestClient
from app.main import create_app
def test_spa_served_when_dist_present(fresh_db_url, tmp_path):
(tmp_path / "index.html").write_text("<!doctype html><title>ecomm spa</title>")
with TestClient(create_app(database_url=fresh_db_url, static_dir=tmp_path)) as client:
root = client.get("/")
assert root.status_code == 200
assert "ecomm spa" in root.text
# API + health still win over the mount
assert client.get("/healthz").json()["status"] == "ok"
assert client.get("/api/auth/me").status_code == 401
def test_no_mount_when_dist_absent(fresh_db_url, tmp_path):
# An empty/missing dist (the dev case) must not 500 the app — / just 404s.
with TestClient(create_app(database_url=fresh_db_url, static_dir=tmp_path / "nope")) as client:
assert client.get("/").status_code == 404
assert client.get("/healthz").json()["status"] == "ok"