From 44718754583ce5c3d6b484a1c373bf8620ce6127 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Fri, 22 May 2026 21:47:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(example):=20#582=20GET=20/=20=E3=81=A8?= =?UTF-8?q?=20/machine/health=20=E3=81=AE=20OpenAPI=20=E3=83=91=E3=83=AA?= =?UTF-8?q?=E3=83=86=E3=82=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit frameworkSmoke ルートと API キー保護の machine health を追加。evac 既定キーは nene2-js compose と一致。 Co-authored-by: Cursor --- src/example/app.py | 35 ++++++++++++++++++++++++++++- src/nene2/auth/api_key.py | 1 + tests/example/test_system_routes.py | 33 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/example/test_system_routes.py diff --git a/src/example/app.py b/src/example/app.py index 7c72769..9123d0f 100644 --- a/src/example/app.py +++ b/src/example/app.py @@ -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 @@ -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, @@ -143,6 +149,12 @@ def create_app(settings: AppSettings | None = None) -> FastAPI: ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(cfg.api_keys), ) + machine_keys = cfg.api_keys if cfg.api_key_enabled and cfg.api_keys else _DEFAULT_MACHINE_API_KEYS + app.add_middleware( + ApiKeyAuthMiddleware, + verifier=LocalTokenVerifier(machine_keys), + include_paths=["/machine/health"], + ) # CORS must be outermost — register last so preflight OPTIONS is handled # before throttle, auth, or any other middleware runs. if cfg.cors_enabled: @@ -199,10 +211,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") diff --git a/src/nene2/auth/api_key.py b/src/nene2/auth/api_key.py index 795d72f..d43651a 100644 --- a/src/nene2/auth/api_key.py +++ b/src/nene2/auth/api_key.py @@ -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) diff --git a/tests/example/test_system_routes.py b/tests/example/test_system_routes.py new file mode 100644 index 0000000..3004ab3 --- /dev/null +++ b/tests/example/test_system_routes.py @@ -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-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" From 228bda363380d42f840690e4292bf57f98695195 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Fri, 22 May 2026 21:48:48 +0900 Subject: [PATCH 2/3] style: ruff line length in app.py Co-authored-by: Cursor --- src/example/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/example/app.py b/src/example/app.py index 9123d0f..2eed59b 100644 --- a/src/example/app.py +++ b/src/example/app.py @@ -149,7 +149,10 @@ def create_app(settings: AppSettings | None = None) -> FastAPI: ApiKeyAuthMiddleware, verifier=LocalTokenVerifier(cfg.api_keys), ) - machine_keys = cfg.api_keys if cfg.api_key_enabled and cfg.api_keys else _DEFAULT_MACHINE_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), From aad40bd8fcac920faff102e868299f65d82dba6f Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Fri, 22 May 2026 21:49:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(example):=20machine=20health=20?= =?UTF-8?q?=E3=81=AF=20X-NENE2-API-Key=20=E3=83=98=E3=83=83=E3=83=80?= =?UTF-8?q?=E3=81=A7=E6=A4=9C=E8=A8=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- src/example/app.py | 1 + tests/example/test_system_routes.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/example/app.py b/src/example/app.py index 2eed59b..73f7cb3 100644 --- a/src/example/app.py +++ b/src/example/app.py @@ -157,6 +157,7 @@ def create_app(settings: AppSettings | None = None) -> FastAPI: 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. diff --git a/tests/example/test_system_routes.py b/tests/example/test_system_routes.py index 3004ab3..3076e09 100644 --- a/tests/example/test_system_routes.py +++ b/tests/example/test_system_routes.py @@ -25,7 +25,7 @@ def test_machine_health_requires_api_key() -> None: def test_machine_health_with_api_key() -> None: client = TestClient(create_app(AppSettings(throttle_enabled=False))) - r = client.get("/machine/health", headers={"X-Api-Key": _MACHINE_KEY}) + r = client.get("/machine/health", headers={"X-NENE2-API-Key": _MACHINE_KEY}) assert r.status_code == 200 body = r.json() assert body["status"] == "ok"