From 75f9dd731dde3fa8743f789cdeb53a6de2ce57ca Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Wed, 1 Nov 2023 06:51:26 +0100 Subject: [PATCH] Fix checking parameter type Workaround for a bug in flask-pydantic. Avoids the following exception causing server to respond with HTTP 500: Traceback (most recent call last): File "/venv/lib64/python3.11/site-packages/flask_pydantic/core.py", line 197, in wrapper b = body_model(**body_params) ^^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: resultsdb.parsers.api_v3.BrewResultParams() argument after ** must be a mapping, not str During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/venv/lib64/python3.11/site-packages/flask/app.py", line 2190, in wsgi_app response = self.full_dispatch_request() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/venv/lib64/python3.11/site-packages/flask/app.py", line 1486, in full_dispatch_request rv = self.handle_user_exception(e) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/venv/lib64/python3.11/site-packages/flask/app.py", line 1484, in full_dispatch_request rv = self.dispatch_request() ^^^^^^^^^^^^^^^^^^^^^^^ File "/venv/lib64/python3.11/site-packages/flask/app.py", line 1469, in dispatch_request return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/venv/lib64/python3.11/site-packages/flask_pyoidc/flask_pyoidc.py", line 463, in wrapper return view_func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/venv/lib64/python3.11/site-packages/flask_pydantic/core.py", line 204, in wrapper raise JsonBodyParsingError() flask_pydantic.exceptions.JsonBodyParsingError Server will respond instead with HTTP 400 and the following JSON body: { "validation_error": { "body_params": [{ "loc": ["__root__"], "msg": "value is not a valid dict", "type": "type_error.dict" }] } } JIRA: RHELWF-10108 --- resultsdb/controllers/api_v3.py | 17 ++++- testing/test_api_v3.py | 110 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/resultsdb/controllers/api_v3.py b/resultsdb/controllers/api_v3.py index e2c41b0..9f36cac 100644 --- a/resultsdb/controllers/api_v3.py +++ b/resultsdb/controllers/api_v3.py @@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, render_template from flask import current_app as app from flask_pydantic import validate +from pydantic import BaseModel from resultsdb.models import db from resultsdb.authorization import match_testcase_permissions, verify_authorization @@ -21,6 +22,20 @@ api = Blueprint("api_v3", __name__) +def ensure_dict_input(cls): + """ + Wraps Pydantic model to ensure that the input type is dict. + + This is a workaround for a bug in flask-pydantic that causes validation to + fail with unexpected exception. + """ + + class EnsureJsonObject(BaseModel): + __root__: cls + + return EnsureJsonObject + + def permissions(): return app.config.get("PERMISSIONS", []) @@ -68,7 +83,7 @@ def create_endpoint(params_class, oidc, provider): @oidc.token_auth(provider) @validate() - def create(body: params_class): + def create(body: ensure_dict_input(params_class)): return create_result(body) def get_schema(): diff --git a/testing/test_api_v3.py b/testing/test_api_v3.py index 5c1f6fa..c171c2c 100644 --- a/testing/test_api_v3.py +++ b/testing/test_api_v3.py @@ -318,3 +318,113 @@ def test_api_v3_consistency(params_class, client): assert r.status_code == 200, r.text assert f"POST /api/v3/results/{artifact_type}s" in r.text assert f'#' in r.text + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_bad_param_type_int(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + r = client.post(f"/api/v3/results/{artifact_type}s", json=0) + assert r.status_code == 400, r.text + assert r.json == { + "validation_error": { + "body_params": [ + { + "loc": ["__root__"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + } + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_bad_param_type_str(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + r = client.post(f"/api/v3/results/{artifact_type}s", json="BAD") + assert r.status_code == 400, r.text + assert r.json == { + "validation_error": { + "body_params": [ + { + "loc": ["__root__"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + } + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_bad_param_type_null(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + r = client.post( + f"/api/v3/results/{artifact_type}s", content_type="application/json", data="null" + ) + assert r.status_code == 400, r.text + assert r.json == { + "validation_error": { + "body_params": [ + { + "loc": ["__root__"], + "msg": "none is not an allowed value", + "type": "type_error.none.not_allowed", + } + ] + } + } + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_bad_param_invalid_json(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + r = client.post(f"/api/v3/results/{artifact_type}s", content_type="application/json", data="{") + assert r.status_code == 400, r.text + assert r.json == {"message": "Bad request"} + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_example(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + example = params_class.example().dict() + r = client.post(f"/api/v3/results/{artifact_type}s", json=example) + assert r.status_code == 201, r.text + + +@pytest.mark.parametrize("params_class", RESULTS_PARAMS_CLASSES) +def test_api_v3_missing_param(params_class, client): + """ + Passing unexpected JSON type must propagate an error to the user. + """ + artifact_type = params_class.artifact_type() + example = params_class.example().dict() + del example["outcome"] + r = client.post(f"/api/v3/results/{artifact_type}s", json=example) + assert r.status_code == 400, r.text + assert r.json == { + "validation_error": { + "body_params": [ + { + "loc": ["__root__", "outcome"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + }