Files
privateGPT/tests/server/utils/test_openapi.py
2026-07-16 13:36:11 +02:00

206 lines
7.1 KiB
Python

from typing import Any
import pytest
from fastapi.testclient import TestClient
from private_gpt.settings.settings import settings
def _get_openapi_spec(test_client: TestClient) -> dict[str, Any]:
prefix_path = settings().server.root_path
response = test_client.get(f"{prefix_path}/openapi.json")
assert response.status_code == 200, (
f"Failed to fetch OpenAPI schema: {response.status_code}"
)
spec: dict[str, Any] = response.json()
return spec
def test_all_non_hidden_endpoints_have_basic_properties(
test_client: TestClient,
) -> None:
errors: list[str] = []
def check_doc(check: bool, message: str) -> None:
if not check:
errors.append(message)
spec = _get_openapi_spec(test_client)
paths: dict[str, Any] | None = spec.get("paths")
if paths is None:
pytest.fail("OpenAPI spec is missing 'paths' section")
for path, path_item in paths.items():
if not isinstance(path_item, dict):
continue
for method, operation in path_item.items():
if not isinstance(operation, dict):
continue
hidden_config: dict[str, Any] | None = operation.get("hidden")
if hidden_config and hidden_config.get("value") is True:
continue
check_doc(
"tags" in operation, f"Path {path}, method {method} is missing tags"
)
check_doc(
"summary" in operation,
f"Path {path}, method {method} is missing summary",
)
check_doc(
"description" in operation,
f"Path {path}, method {method} is missing description",
)
responses: dict[str, Any] | None = operation.get("responses")
check_doc(
responses is not None,
f"Path {path}, method {method} is missing responses",
)
if responses:
for status_code, response in responses.items():
if not isinstance(response, dict):
continue
check_doc(
"description" in response,
f"Path {path}, method {method} is missing response description for status code {status_code}",
)
response_desc: str | None = response.get("description")
check_doc(
response_desc is not None and response_desc.strip() != "",
f"Path {path}, method {method} has empty response description for status code {status_code}",
)
request_body: dict[str, Any] | None = operation.get("requestBody")
mutating_methods: list[str] = ["post", "put", "patch"]
if method.lower() in mutating_methods:
check_doc(
request_body is not None,
f"Path {path}, method {method} is missing requestBody",
)
if request_body:
check_doc(
"description" in request_body,
f"Path {path}, method {method} is missing requestBody description",
)
request_body_desc: str | None = request_body.get("description")
check_doc(
request_body_desc is not None and request_body_desc.strip() != "",
f"Path {path}, method {method} has empty requestBody description",
)
if errors:
pytest.fail("OpenAPI spec validation failed:\n" + "\n".join(errors))
def test_all_schemas_have_required_properties(test_client: TestClient) -> None:
errors: list[str] = []
def check_schema(check: bool, message: str) -> None:
if not check:
errors.append(message)
def is_multipart_form_schema(schema_name: str) -> bool:
return schema_name.startswith("Body_") and (
"_post" in schema_name.lower()
or "_put" in schema_name.lower()
or "_patch" in schema_name.lower()
)
response = test_client.get("/openapi.json")
assert response.status_code == 200, (
f"Failed to fetch OpenAPI schema: {response.status_code}"
)
spec: dict[str, Any] = response.json()
components: dict[str, Any] | None = spec.get("components")
if components is None:
pytest.fail("OpenAPI spec is missing 'components' section")
schemas: dict[str, Any] | None = components.get("schemas")
if schemas is None:
pytest.fail("OpenAPI spec is missing 'schemas' section")
for schema_name, schema_def in schemas.items():
if not isinstance(schema_def, dict):
continue
if "error" in schema_name.lower():
continue
if is_multipart_form_schema(schema_name):
# Skip multipart form body schemas - they can't have descriptions
continue
check_schema(
"description" in schema_def, f"Schema {schema_name} is missing description"
)
description: str | None = schema_def.get("description")
check_schema(
description is not None and description.strip() != "",
f"Schema {schema_name} has empty description",
)
check_schema(
"type" in schema_def
or "$ref" in schema_def
or "allOf" in schema_def
or "oneOf" in schema_def
or "anyOf" in schema_def,
f"Schema {schema_name} is missing type definition",
)
properties: dict[str, Any] | None = schema_def.get("properties")
if properties:
for prop_name, prop_def in properties.items():
if not isinstance(prop_def, dict):
continue
check_schema(
"type" in prop_def
or "$ref" in prop_def
or "allOf" in prop_def
or "oneOf" in prop_def
or "anyOf" in prop_def,
f"Schema {schema_name}, property {prop_name} is missing type definition",
)
check_schema(
"description" in prop_def,
f"Schema {schema_name}, property {prop_name} is missing description",
)
prop_description: str | None = prop_def.get("description")
check_schema(
prop_description is not None and prop_description.strip() != "",
f"Schema {schema_name}, property {prop_name} has empty description",
)
if schema_def.get("type") == "array":
items: dict[str, Any] | None = schema_def.get("items")
check_schema(
items is not None,
f"Schema {schema_name} is array type but missing 'items' definition",
)
if items:
check_schema(
"type" in items or "$ref" in items,
f"Schema {schema_name} array items missing type definition",
)
if errors:
pytest.fail("Schema validation failed:\n" + "\n".join(errors))