Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/example/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Application factory — wires dependencies and registers routes."""

from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
Expand Down Expand Up @@ -62,6 +62,12 @@
UpdateTagUseCase,
)

# OpenAPI / NENE2 PHP parity (FrameworkInfo in hideyukiMORI/NENE2).
_FRAMEWORK_NAME = "NENE2"
_FRAMEWORK_DESCRIPTION = "JSON APIs first, minimal server HTML, frontend ready, AI-readable."
# Matches nene2-js tools/compose-ft-evac.yaml default for local evac smoke.
_DEFAULT_MACHINE_API_KEYS = ["ft-evac-local-machine-api-key-32ch!!"]

type _Repos = tuple[
NoteRepositoryInterface,
TagRepositoryInterface,
Expand Down Expand Up @@ -143,6 +149,16 @@ def create_app(settings: AppSettings | None = None) -> FastAPI:
ApiKeyAuthMiddleware,
verifier=LocalTokenVerifier(cfg.api_keys),
)
if cfg.api_key_enabled and cfg.api_keys:
machine_keys = cfg.api_keys
else:
machine_keys = _DEFAULT_MACHINE_API_KEYS
app.add_middleware(
ApiKeyAuthMiddleware,
verifier=LocalTokenVerifier(machine_keys),
include_paths=["/machine/health"],
header_name="X-NENE2-API-Key",
)
# CORS must be outermost — register last so preflight OPTIONS is handled
# before throttle, auth, or any other middleware runs.
if cfg.cors_enabled:
Expand Down Expand Up @@ -199,10 +215,31 @@ def create_app(settings: AppSettings | None = None) -> FastAPI:
prefix="/examples",
)

@app.get("/", tags=["system"], summary="Framework smoke endpoint")
async def framework_smoke() -> JSONResponse:
return JSONResponse(
{
"name": _FRAMEWORK_NAME,
"description": _FRAMEWORK_DESCRIPTION,
"status": "ok",
}
)

@app.get("/examples/ping", tags=["system"], summary="Example ping")
async def example_ping() -> JSONResponse:
return JSONResponse({"message": "pong", "status": "ok"})

@app.get("/machine/health", tags=["system"], summary="Protected machine health endpoint")
async def machine_health(request: Request) -> JSONResponse:
credential_type = getattr(request.state, "nene2_auth_credential_type", "api_key")
return JSONResponse(
{
"status": "ok",
"service": _FRAMEWORK_NAME,
"credential_type": credential_type,
}
)

db_health = DatabaseHealthCheck(db_executor) if db_executor else None

@app.get("/health", tags=["system"], summary="Health check")
Expand Down
1 change: 1 addition & 0 deletions src/nene2/auth/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -
401,
f"A valid {self._header_name} header is required.",
)
request.state.nene2_auth_credential_type = "api_key"
return await call_next(request)
33 changes: 33 additions & 0 deletions tests/example/test_system_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""System route parity with NENE2 OpenAPI (GET /, /machine/health)."""

from fastapi.testclient import TestClient

from example.app import create_app
from nene2.config import AppSettings

_MACHINE_KEY = "ft-evac-local-machine-api-key-32ch!!"


def test_framework_smoke_root() -> None:
client = TestClient(create_app(AppSettings(throttle_enabled=False)))
r = client.get("/")
assert r.status_code == 200
body = r.json()
assert body["name"] == "NENE2"
assert body["status"] == "ok"
assert "description" in body


def test_machine_health_requires_api_key() -> None:
client = TestClient(create_app(AppSettings(throttle_enabled=False)))
assert client.get("/machine/health").status_code == 401


def test_machine_health_with_api_key() -> None:
client = TestClient(create_app(AppSettings(throttle_enabled=False)))
r = client.get("/machine/health", headers={"X-NENE2-API-Key": _MACHINE_KEY})
assert r.status_code == 200
body = r.json()
assert body["status"] == "ok"
assert body["service"] == "NENE2"
assert body["credential_type"] == "api_key"
Loading