deer-flow/backend/tests/test_gateway_docs_toggle.py
sunsine 0691c4dda3
fix(security): allow disabling API docs in production via GATEWAY_ENABLE_DOCS (#2651)
* fix(security): allow disabling API docs in production via GATEWAY_ENABLE_DOCS

Expose /docs, /redoc, and /openapi.json only when GATEWAY_ENABLE_DOCS=true
(default). Setting GATEWAY_ENABLE_DOCS=false disables all three endpoints,
preventing unauthorized API surface discovery in production deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(security): add unit tests and docs for GATEWAY_ENABLE_DOCS

Add 7 tests covering default behavior, env var parsing (case-insensitive,
fail-closed), endpoint visibility, and health endpoint independence.
Update CONFIGURATION.md and CLAUDE.md with the new toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(security): apply ruff formatting to gateway app.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-04-30 10:58:32 +08:00

125 lines
4.3 KiB
Python

"""Tests for GATEWAY_ENABLE_DOCS configuration toggle.
Verifies that Swagger UI (/docs), ReDoc (/redoc), and the OpenAPI schema
(/openapi.json) can be disabled via the GATEWAY_ENABLE_DOCS environment
variable for production deployments.
"""
from __future__ import annotations
import os
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
def _reset_gateway_config():
"""Reset the cached gateway config so env changes take effect."""
import app.gateway.config as cfg
cfg._gateway_config = None
@pytest.fixture(autouse=True)
def _clean_config():
"""Ensure gateway config cache is cleared before and after each test."""
_reset_gateway_config()
yield
_reset_gateway_config()
# ---------------------------------------------------------------------------
# Config parsing
# ---------------------------------------------------------------------------
def test_enable_docs_defaults_to_true():
"""When GATEWAY_ENABLE_DOCS is not set, enable_docs should be True."""
with patch.dict(os.environ, {}, clear=False):
if "GATEWAY_ENABLE_DOCS" in os.environ:
del os.environ["GATEWAY_ENABLE_DOCS"]
_reset_gateway_config()
from app.gateway.config import get_gateway_config
config = get_gateway_config()
assert config.enable_docs is True
def test_enable_docs_false():
"""GATEWAY_ENABLE_DOCS=false should disable docs."""
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
_reset_gateway_config()
from app.gateway.config import get_gateway_config
config = get_gateway_config()
assert config.enable_docs is False
def test_enable_docs_case_insensitive():
"""GATEWAY_ENABLE_DOCS is case-insensitive (FALSE, False, false)."""
for value in ("FALSE", "False", "false"):
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": value}):
_reset_gateway_config()
from app.gateway.config import get_gateway_config
config = get_gateway_config()
assert config.enable_docs is False, f"Expected False for GATEWAY_ENABLE_DOCS={value}"
def test_enable_docs_unexpected_value_disables():
"""Any non-'true' value should disable docs (fail-closed)."""
for value in ("0", "no", "off", "anything"):
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": value}):
_reset_gateway_config()
from app.gateway.config import get_gateway_config
config = get_gateway_config()
assert config.enable_docs is False, f"Expected False for GATEWAY_ENABLE_DOCS={value}"
# ---------------------------------------------------------------------------
# App-level endpoint visibility
# ---------------------------------------------------------------------------
def test_docs_endpoints_available_by_default():
"""With enable_docs=True (default), /docs, /redoc, /openapi.json return 200."""
with patch.dict(os.environ, {}, clear=False):
if "GATEWAY_ENABLE_DOCS" in os.environ:
del os.environ["GATEWAY_ENABLE_DOCS"]
_reset_gateway_config()
from app.gateway.app import create_app
app = create_app()
client = TestClient(app)
assert client.get("/docs").status_code == 200
assert client.get("/redoc").status_code == 200
assert client.get("/openapi.json").status_code == 200
def test_docs_endpoints_disabled_when_false():
"""With GATEWAY_ENABLE_DOCS=false, /docs, /redoc, /openapi.json return 404."""
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
_reset_gateway_config()
from app.gateway.app import create_app
app = create_app()
client = TestClient(app)
assert client.get("/docs").status_code == 404
assert client.get("/redoc").status_code == 404
assert client.get("/openapi.json").status_code == 404
def test_health_still_works_when_docs_disabled():
"""Disabling docs should NOT affect /health or other normal endpoints."""
with patch.dict(os.environ, {"GATEWAY_ENABLE_DOCS": "false"}):
_reset_gateway_config()
from app.gateway.app import create_app
app = create_app()
client = TestClient(app)
resp = client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "healthy"