mirror of
https://github.com/zulip/zulip.git
synced 2025-11-17 20:41:46 +00:00
openapi: Validate real requests and responses, not fictional mocks.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
(cherry picked from commit 029e765e20)
This commit is contained in:
committed by
Tim Abbott
parent
9f2172c0f9
commit
3bf1934598
@@ -12,6 +12,7 @@ from typing import Any, Dict, List, Literal, Mapping, Optional, Set, Tuple, Unio
|
||||
|
||||
import orjson
|
||||
from openapi_core import OpenAPI
|
||||
from openapi_core.protocols import Request, Response
|
||||
from openapi_core.testing import MockRequest, MockResponse
|
||||
from openapi_core.validation.exceptions import ValidationError as OpenAPIValidationError
|
||||
from pydantic import BaseModel
|
||||
@@ -423,10 +424,29 @@ def find_openapi_endpoint(path: str) -> Optional[str]:
|
||||
def validate_against_openapi_schema(
|
||||
content: Dict[str, Any], path: str, method: str, status_code: str
|
||||
) -> bool:
|
||||
mock_request = MockRequest("http://localhost:9991/", method, "/api/v1" + path)
|
||||
mock_response = MockResponse(
|
||||
orjson.dumps(content),
|
||||
status_code=int(status_code),
|
||||
)
|
||||
return validate_test_response(mock_request, mock_response)
|
||||
|
||||
|
||||
def validate_test_response(request: Request, response: Response) -> bool:
|
||||
"""Compare a "content" dict with the defined schema for a specific method
|
||||
in an endpoint. Return true if validated and false if skipped.
|
||||
"""
|
||||
|
||||
if request.path.startswith("/json/"):
|
||||
path = request.path[len("/json") :]
|
||||
elif request.path.startswith("/api/v1/"):
|
||||
path = request.path[len("/api/v1") :]
|
||||
else:
|
||||
return False
|
||||
assert request.method is not None
|
||||
method = request.method.lower()
|
||||
status_code = str(response.status_code)
|
||||
|
||||
# This first set of checks are primarily training wheels that we
|
||||
# hope to eliminate over time as we improve our API documentation.
|
||||
|
||||
@@ -452,14 +472,8 @@ def validate_against_openapi_schema(
|
||||
# response have been defined this should be removed.
|
||||
return True
|
||||
|
||||
mock_request = MockRequest("http://localhost:9991/", method, "/api/v1" + path)
|
||||
mock_response = MockResponse(
|
||||
# TODO: Use original response content instead of re-serializing it.
|
||||
orjson.dumps(content),
|
||||
status_code=int(status_code),
|
||||
)
|
||||
try:
|
||||
openapi_spec.spec().validate_response(mock_request, mock_response)
|
||||
openapi_spec.spec().validate_response(request, response)
|
||||
except OpenAPIValidationError as error:
|
||||
message = f"Response validation error at {method} /api/v1{path} ({status_code}):"
|
||||
message += f"\n\n{type(error).__name__}: {error}"
|
||||
@@ -529,10 +543,33 @@ def validate_request(
|
||||
status_code: str,
|
||||
intentionally_undocumented: bool = False,
|
||||
) -> None:
|
||||
# Some JSON endpoints have different parameters compared to
|
||||
# their `/api/v1` counterparts.
|
||||
if json_url and (url, method) in SKIP_JSON:
|
||||
return
|
||||
assert isinstance(data, dict)
|
||||
mock_request = MockRequest(
|
||||
"http://localhost:9991/",
|
||||
method,
|
||||
"/api/v1" + url,
|
||||
headers=http_headers,
|
||||
args={k: str(v) for k, v in data.items()},
|
||||
)
|
||||
validate_test_request(mock_request, status_code, intentionally_undocumented)
|
||||
|
||||
|
||||
def validate_test_request(
|
||||
request: Request,
|
||||
status_code: str,
|
||||
intentionally_undocumented: bool = False,
|
||||
) -> None:
|
||||
assert request.method is not None
|
||||
method = request.method.lower()
|
||||
if request.path.startswith("/json/"):
|
||||
url = request.path[len("/json") :]
|
||||
# Some JSON endpoints have different parameters compared to
|
||||
# their `/api/v1` counterparts.
|
||||
if (url, method) in SKIP_JSON:
|
||||
return
|
||||
else:
|
||||
assert request.path.startswith("/api/v1/")
|
||||
url = request.path[len("/api/v1") :]
|
||||
|
||||
# TODO: Add support for file upload endpoints that lack the /json/
|
||||
# or /api/v1/ prefix.
|
||||
@@ -550,16 +587,8 @@ def validate_request(
|
||||
|
||||
# Now using the openapi_core APIs, validate the request schema
|
||||
# against the OpenAPI documentation.
|
||||
assert isinstance(data, dict)
|
||||
mock_request = MockRequest(
|
||||
"http://localhost:9991/",
|
||||
method,
|
||||
"/api/v1" + url,
|
||||
headers=http_headers,
|
||||
args={k: str(v) for k, v in data.items()},
|
||||
)
|
||||
try:
|
||||
openapi_spec.spec().validate_request(mock_request)
|
||||
openapi_spec.spec().validate_request(request)
|
||||
except OpenAPIValidationError as error:
|
||||
# Show a block error message explaining the options for fixing it.
|
||||
msg = f"""
|
||||
|
||||
Reference in New Issue
Block a user