diff --git a/flask_parameter_validation/parameter_validation.py b/flask_parameter_validation/parameter_validation.py index 2f56502..20f72c7 100644 --- a/flask_parameter_validation/parameter_validation.py +++ b/flask_parameter_validation/parameter_validation.py @@ -3,7 +3,7 @@ import inspect import re from inspect import signature -from flask import request +from flask import request, Response from werkzeug.datastructures import ImmutableMultiDict from werkzeug.exceptions import BadRequest from .exceptions import (InvalidParameterTypeError, MissingInputError, @@ -40,8 +40,13 @@ def __call__(self, f): } fn_list[fsig] = fdocs - @functools.wraps(f) - async def nested_func(**kwargs): + def nested_func_helper(**kwargs): + """ + Validates the inputs of a Flask route or returns an error. Returns + are wrapped in a dictionary with a flag to let nested_func() know + if it should unpack the resulting dictionary of inputs as kwargs, + or just return the error message. + """ # Step 1 - Get expected input details as dict expected_inputs = signature(f).parameters @@ -54,7 +59,7 @@ async def nested_func(**kwargs): try: json_input = request.json except BadRequest: - return {"error": "Could not parse JSON."}, 400 + return {"error": ({"error": "Could not parse JSON."}, 400), "validated": False} # Step 3 - Extract list of parameters expected to be lists (otherwise all values are converted to lists) expected_list_params = [] @@ -79,18 +84,32 @@ async def nested_func(**kwargs): try: new_input = self.validate(expected, request_inputs) except (MissingInputError, ValidationError) as e: - return {"error": str(e)}, 400 + return {"error": ({"error": str(e)}, 400), "validated": False} else: try: new_input = self.validate(expected, request_inputs) except Exception as e: - return self.custom_error_handler(e) + return {"error": self.custom_error_handler(e), "validated": False} validated_inputs[expected.name] = new_input - if asyncio.iscoroutinefunction(f): - return await f(**validated_inputs) - else: - return f(**validated_inputs) + return {"inputs": validated_inputs, "validated": True} + + if asyncio.iscoroutinefunction(f): + # If the view function is async, return and await a coroutine + @functools.wraps(f) + async def nested_func(**kwargs): + validated_inputs = nested_func_helper(**kwargs) + if validated_inputs["validated"]: + return await f(**validated_inputs["inputs"]) + return validated_inputs["error"] + else: + # If the view function is not async, return a function + @functools.wraps(f) + def nested_func(**kwargs): + validated_inputs = nested_func_helper(**kwargs) + if validated_inputs["validated"]: + return f(**validated_inputs["inputs"]) + return validated_inputs["error"] nested_func.__name__ = f.__name__ return nested_func diff --git a/flask_parameter_validation/test/test_file_params.py b/flask_parameter_validation/test/test_file_params.py index 70e036b..7bf254c 100644 --- a/flask_parameter_validation/test/test_file_params.py +++ b/flask_parameter_validation/test/test_file_params.py @@ -17,6 +17,28 @@ def test_required_file(client): assert "error" in r.json +def test_required_file_decorator(client): + url = "/file/decorator/required" + # Test that we receive a success response if a file is provided + r = client.post(url, data={"v": (resources / "test.json").open("rb")}) + assert "success" in r.json + assert r.json["success"] + # Test that we receive an error if a file is not provided + r = client.post(url) + assert "error" in r.json + + +def test_required_file_async_decorator(client): + url = "/file/async_decorator/required" + # Test that we receive a success response if a file is provided + r = client.post(url, data={"v": (resources / "test.json").open("rb")}) + assert "success" in r.json + assert r.json["success"] + # Test that we receive an error if a file is not provided + r = client.post(url) + assert "error" in r.json + + def test_optional_file(client): url = "/file/optional" # Test that we receive a success response if a file is provided diff --git a/flask_parameter_validation/test/test_form_params.py b/flask_parameter_validation/test/test_form_params.py index 555da45..3683bf9 100644 --- a/flask_parameter_validation/test/test_form_params.py +++ b/flask_parameter_validation/test/test_form_params.py @@ -25,6 +25,28 @@ def test_required_str(client): assert "error" in r.json +def test_required_str_decorator(client): + url = "/form/str/decorator/required" + # Test that present input yields input value + r = client.post(url, data={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input yields error + r = client.post(url) + assert "error" in r.json + + +def test_required_str_async_decorator(client): + url = "/form/str/async_decorator/required" + # Test that present input yields input value + r = client.post(url, data={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input yields error + r = client.post(url) + assert "error" in r.json + + def test_optional_str(client): url = "/form/str/optional" # Test that missing input yields None diff --git a/flask_parameter_validation/test/test_json_params.py b/flask_parameter_validation/test/test_json_params.py index b8b5208..14231b5 100644 --- a/flask_parameter_validation/test/test_json_params.py +++ b/flask_parameter_validation/test/test_json_params.py @@ -952,6 +952,54 @@ def test_dict_default(client): assert opt == r.json["opt"] +def test_dict_default_decorator(client): + url = "/json/dict/decorator/default" + # Test that present dict yields input values + n_opt = {"e": "f"} + opt = {"g": "h"} + r = client.post(url, json={"n_opt": n_opt, "opt": opt}) + assert "n_opt" in r.json + assert "opt" in r.json + assert type(r.json["n_opt"]) is dict + assert type(r.json["opt"]) is dict + assert n_opt == r.json["n_opt"] + assert opt == r.json["opt"] + # Test that missing dict yields default values + n_opt = {"a": "b"} + opt = {"c": "d"} + r = client.post(url) + assert "n_opt" in r.json + assert "opt" in r.json + assert type(r.json["n_opt"]) is dict + assert type(r.json["opt"]) is dict + assert n_opt == r.json["n_opt"] + assert opt == r.json["opt"] + + +def test_dict_default_async_decorator(client): + url = "/json/dict/async_decorator/default" + # Test that present dict yields input values + n_opt = {"e": "f"} + opt = {"g": "h"} + r = client.post(url, json={"n_opt": n_opt, "opt": opt}) + assert "n_opt" in r.json + assert "opt" in r.json + assert type(r.json["n_opt"]) is dict + assert type(r.json["opt"]) is dict + assert n_opt == r.json["n_opt"] + assert opt == r.json["opt"] + # Test that missing dict yields default values + n_opt = {"a": "b"} + opt = {"c": "d"} + r = client.post(url) + assert "n_opt" in r.json + assert "opt" in r.json + assert type(r.json["n_opt"]) is dict + assert type(r.json["opt"]) is dict + assert n_opt == r.json["n_opt"] + assert opt == r.json["opt"] + + def test_dict_func(client): url = "/json/dict/func" # Test that dict passing func yields input value diff --git a/flask_parameter_validation/test/test_query_params.py b/flask_parameter_validation/test/test_query_params.py index 49bf2d7..e7ea85c 100644 --- a/flask_parameter_validation/test/test_query_params.py +++ b/flask_parameter_validation/test/test_query_params.py @@ -25,6 +25,28 @@ def test_required_str(client): assert "error" in r.json +def test_required_str_decorator(client): + url = "/query/str/decorator/required" + # Test that present input yields input value + r = client.get(url, query_string={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + +def test_required_str_async_decorator(client): + url = "/query/str/async_decorator/required" + # Test that present input yields input value + r = client.get(url, query_string={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + def test_optional_str(client): url = "/query/str/optional" # Test that missing input yields None @@ -37,6 +59,30 @@ def test_optional_str(client): assert r.json["v"] == "v" +def test_optional_str_decorator(client): + url = "/query/str/decorator/optional" + # Test that missing input yields None + r = client.get(url) + assert "v" in r.json + assert r.json["v"] is None + # Test that present input yields input value + r = client.get(url, query_string={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + + +def test_optional_str_async_decorator(client): + url = "/query/str/async_decorator/optional" + # Test that missing input yields None + r = client.get(url) + assert "v" in r.json + assert r.json["v"] is None + # Test that present input yields input value + r = client.get(url, query_string={"v": "v"}) + assert "v" in r.json + assert r.json["v"] == "v" + + def test_str_default(client): url = "/query/str/default" # Test that missing input for required and optional yields default values @@ -53,6 +99,38 @@ def test_str_default(client): assert r.json["n_opt"] == "b" +def test_str_default_decorator(client): + url = "/query/str/decorator/default" + # Test that missing input for required and optional yields default values + r = client.get(url) + assert "opt" in r.json + assert r.json["opt"] == "optional" + assert "n_opt" in r.json + assert r.json["n_opt"] == "not_optional" + # Test that present input for required and optional yields input values + r = client.get(url, query_string={"opt": "a", "n_opt": "b"}) + assert "opt" in r.json + assert r.json["opt"] == "a" + assert "n_opt" in r.json + assert r.json["n_opt"] == "b" + + +def test_str_default_async_decorator(client): + url = "/query/str/async_decorator/default" + # Test that missing input for required and optional yields default values + r = client.get(url) + assert "opt" in r.json + assert r.json["opt"] == "optional" + assert "n_opt" in r.json + assert r.json["n_opt"] == "not_optional" + # Test that present input for required and optional yields input values + r = client.get(url, query_string={"opt": "a", "n_opt": "b"}) + assert "opt" in r.json + assert r.json["opt"] == "a" + assert "n_opt" in r.json + assert r.json["n_opt"] == "b" + + def test_str_min_str_length(client): url = "/query/str/min_str_length" # Test that below minimum yields error @@ -68,6 +146,36 @@ def test_str_min_str_length(client): assert r.json["v"] == "aaa" +def test_str_min_str_length_decorator(client): + url = "/query/str/decorator/min_str_length" + # Test that below minimum yields error + r = client.get(url, query_string={"v": ""}) + assert "error" in r.json + # Test that at minimum yields input + r = client.get(url, query_string={"v": "aa"}) + assert "v" in r.json + assert r.json["v"] == "aa" + # Test that above minimum yields input + r = client.get(url, query_string={"v": "aaa"}) + assert "v" in r.json + assert r.json["v"] == "aaa" + + +def test_str_min_str_length_async_decorator(client): + url = "/query/str/async_decorator/min_str_length" + # Test that below minimum yields error + r = client.get(url, query_string={"v": ""}) + assert "error" in r.json + # Test that at minimum yields input + r = client.get(url, query_string={"v": "aa"}) + assert "v" in r.json + assert r.json["v"] == "aa" + # Test that above minimum yields input + r = client.get(url, query_string={"v": "aaa"}) + assert "v" in r.json + assert r.json["v"] == "aaa" + + def test_str_max_str_length(client): url = "/query/str/max_str_length" # Test that below maximum yields input @@ -83,6 +191,36 @@ def test_str_max_str_length(client): assert "error" in r.json +def test_str_max_str_length_decorator(client): + url = "/query/str/decorator/max_str_length" + # Test that below maximum yields input + r = client.get(url, query_string={"v": ""}) + assert "v" in r.json + assert r.json["v"] == "" + # Test that at maximum yields input + r = client.get(url, query_string={"v": "aa"}) + assert "v" in r.json + assert r.json["v"] == "aa" + # Test that above maximum yields error + r = client.get(url, query_string={"v": "aaa"}) + assert "error" in r.json + + +def test_str_max_str_length_async_decorator(client): + url = "/query/str/async_decorator/max_str_length" + # Test that below maximum yields input + r = client.get(url, query_string={"v": ""}) + assert "v" in r.json + assert r.json["v"] == "" + # Test that at maximum yields input + r = client.get(url, query_string={"v": "aa"}) + assert "v" in r.json + assert r.json["v"] == "aa" + # Test that above maximum yields error + r = client.get(url, query_string={"v": "aaa"}) + assert "error" in r.json + + def test_str_whitelist(client): url = "/query/str/whitelist" # Test that input within whitelist yields input @@ -97,6 +235,34 @@ def test_str_whitelist(client): assert "error" in r.json +def test_str_whitelist_decorator(client): + url = "/query/str/decorator/whitelist" + # Test that input within whitelist yields input + r = client.get(url, query_string={"v": "ABC123"}) + assert "v" in r.json + assert r.json["v"] == "ABC123" + # Test that mixed input yields error + r = client.get(url, query_string={"v": "abc123"}) + assert "error" in r.json + # Test that input outside of whitelist yields error + r = client.get(url, query_string={"v": "def456"}) + assert "error" in r.json + + +def test_str_whitelist_async_decorator(client): + url = "/query/str/async_decorator/whitelist" + # Test that input within whitelist yields input + r = client.get(url, query_string={"v": "ABC123"}) + assert "v" in r.json + assert r.json["v"] == "ABC123" + # Test that mixed input yields error + r = client.get(url, query_string={"v": "abc123"}) + assert "error" in r.json + # Test that input outside of whitelist yields error + r = client.get(url, query_string={"v": "def456"}) + assert "error" in r.json + + def test_str_blacklist(client): url = "/query/str/blacklist" # Test that input within blacklist yields error @@ -111,6 +277,34 @@ def test_str_blacklist(client): assert r.json["v"] == "def456" +def test_str_blacklist_decorator(client): + url = "/query/str/decorator/blacklist" + # Test that input within blacklist yields error + r = client.get(url, query_string={"v": "ABC123"}) + assert "error" in r.json + # Test that mixed input yields error + r = client.get(url, query_string={"v": "abc123"}) + assert "error" in r.json + # Test that input outside of blacklist yields input + r = client.get(url, query_string={"v": "def456"}) + assert "v" in r.json + assert r.json["v"] == "def456" + + +def test_str_blacklist_async_decorator(client): + url = "/query/str/async_decorator/blacklist" + # Test that input within blacklist yields error + r = client.get(url, query_string={"v": "ABC123"}) + assert "error" in r.json + # Test that mixed input yields error + r = client.get(url, query_string={"v": "abc123"}) + assert "error" in r.json + # Test that input outside of blacklist yields input + r = client.get(url, query_string={"v": "def456"}) + assert "v" in r.json + assert r.json["v"] == "def456" + + def test_str_pattern(client): url = "/query/str/pattern" # Test that input matching pattern yields input @@ -122,6 +316,28 @@ def test_str_pattern(client): assert "error" in r.json +def test_str_pattern_decorator(client): + url = "/query/str/decorator/pattern" + # Test that input matching pattern yields input + r = client.get(url, query_string={"v": "AbC123"}) + assert "v" in r.json + assert r.json["v"] == "AbC123" + # Test that input failing pattern yields error + r = client.get(url, query_string={"v": "123ABC"}) + assert "error" in r.json + + +def test_str_pattern_async_decorator(client): + url = "/query/str/async_decorator/pattern" + # Test that input matching pattern yields input + r = client.get(url, query_string={"v": "AbC123"}) + assert "v" in r.json + assert r.json["v"] == "AbC123" + # Test that input failing pattern yields error + r = client.get(url, query_string={"v": "123ABC"}) + assert "error" in r.json + + def test_str_func(client): url = "/query/str/func" # Test that input passing func yields input @@ -133,6 +349,28 @@ def test_str_func(client): assert "error" in r.json +def test_str_func_decorator(client): + url = "/query/str/decorator/func" + # Test that input passing func yields input + r = client.get(url, query_string={"v": "123"}) + assert "v" in r.json + assert r.json["v"] == "123" + # Test that input failing func yields error + r = client.get(url, query_string={"v": "abc"}) + assert "error" in r.json + + +def test_str_func_async_decorator(client): + url = "/query/str/async_decorator/func" + # Test that input passing func yields input + r = client.get(url, query_string={"v": "123"}) + assert "v" in r.json + assert r.json["v"] == "123" + # Test that input failing func yields error + r = client.get(url, query_string={"v": "abc"}) + assert "error" in r.json + + def test_str_alias(client): url = "/query/str/alias" # Test that original name yields error @@ -144,6 +382,28 @@ def test_str_alias(client): assert r.json["value"] == "abc" +def test_str_alias_decorator(client): + url = "/query/str/decorator/alias" + # Test that original name yields error + r = client.get(url, query_string={"value": "abc"}) + assert "error" in r.json + # Test that alias yields input + r = client.get(url, query_string={"v": "abc"}) + assert "value" in r.json + assert r.json["value"] == "abc" + + +def test_str_alias_async_decorator(client): + url = "/query/str/async_decorator/alias" + # Test that original name yields error + r = client.get(url, query_string={"value": "abc"}) + assert "error" in r.json + # Test that alias yields input + r = client.get(url, query_string={"v": "abc"}) + assert "value" in r.json + assert r.json["value"] == "abc" + + # Int Validation def test_required_int(client): url = "/query/int/required" @@ -159,6 +419,34 @@ def test_required_int(client): assert "error" in r.json +def test_required_int_decorator(client): + url = "/query/int/decorator/required" + # Test that present int input yields input value + r = client.get(url, query_string={"v": 1}) + assert "v" in r.json + assert r.json["v"] == 1 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-int input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_int_async_decorator(client): + url = "/query/int/async_decorator/required" + # Test that present int input yields input value + r = client.get(url, query_string={"v": 1}) + assert "v" in r.json + assert r.json["v"] == 1 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-int input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_int(client): url = "/query/int/optional" # Test that missing input yields None @@ -257,6 +545,50 @@ def test_required_bool(client): assert "error" in r.json +def test_required_bool_decorator(client): + url = "/query/bool/decorator/required" + # Test that present lowercase bool input yields input value + r = client.get(url, query_string={"v": "true"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present mixed-case bool input yields input value + r = client.get(url, query_string={"v": "TruE"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present uppercase bool input yields input value + r = client.get(url, query_string={"v": "TRUE"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-bool input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_bool_async_decorator(client): + url = "/query/bool/async_decorator/required" + # Test that present lowercase bool input yields input value + r = client.get(url, query_string={"v": "true"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present mixed-case bool input yields input value + r = client.get(url, query_string={"v": "TruE"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present uppercase bool input yields input value + r = client.get(url, query_string={"v": "TRUE"}) + assert "v" in r.json + assert r.json["v"] is True + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-bool input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_bool(client): url = "/query/bool/optional" # Test that missing input yields None @@ -296,15 +628,51 @@ def test_bool_func(client): # Test that input passing func yields input r = client.get(url, query_string={"v": True}) assert "v" in r.json - assert r.json["v"] is True - # Test that input failing func yields error - r = client.get(url, query_string={"v": False}) + assert r.json["v"] is True + # Test that input failing func yields error + r = client.get(url, query_string={"v": False}) + assert "error" in r.json + + +# Float Validation +def test_required_float(client): + url = "/query/float/required" + # Test that present float input yields input value + r = client.get(url, query_string={"v": 1.2}) + assert "v" in r.json + assert r.json["v"] == 1.2 + # Test that present int input yields float(input) value + r = client.get(url, query_string={"v": 1}) + assert "v" in r.json + assert r.json["v"] == 1.0 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-float input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_float_decorator(client): + url = "/query/float/decorator/required" + # Test that present float input yields input value + r = client.get(url, query_string={"v": 1.2}) + assert "v" in r.json + assert r.json["v"] == 1.2 + # Test that present int input yields float(input) value + r = client.get(url, query_string={"v": 1}) + assert "v" in r.json + assert r.json["v"] == 1.0 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-float input yields error + r = client.get(url, query_string={"v": "a"}) assert "error" in r.json -# Float Validation -def test_required_float(client): - url = "/query/float/required" +def test_required_float_async_decorator(client): + url = "/query/float/async_decorator/required" # Test that present float input yields input value r = client.get(url, query_string={"v": 1.2}) assert "v" in r.json @@ -382,6 +750,36 @@ def test_required_datetime(client): assert "error" in r.json +def test_required_datetime_decorator(client): + url = "/query/datetime/decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.datetime(2024, 2, 9, 3, 47, tzinfo=datetime.timezone.utc) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_datetime_async_decorator(client): + url = "/query/datetime/async_decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.datetime(2024, 2, 9, 3, 47, tzinfo=datetime.timezone.utc) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_datetime(client): url = "/query/datetime/optional" # Test that missing input yields None @@ -463,6 +861,36 @@ def test_required_date(client): assert "error" in r.json +def test_required_date_decorator(client): + url = "/query/date/decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.date(2024, 2, 9) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_date_async_decorator(client): + url = "/query/date/async_decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.date(2024, 2, 9) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_date(client): url = "/query/date/optional" # Test that missing input yields None @@ -531,6 +959,36 @@ def test_required_time(client): assert "error" in r.json +def test_required_time_decorator(client): + url = "/query/time/decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.time(23, 24, 21) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_time_async_decorator(client): + url = "/query/time/async_decorator/required" + # Test that present ISO 8601 input yields input value + v = datetime.time(23, 24, 21) + r = client.get(url, query_string={"v": v.isoformat()}) + assert "v" in r.json + assert r.json["v"] == v.isoformat() + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-ISO 8601 input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_time(client): url = "/query/time/optional" # Test that missing input yields None @@ -602,6 +1060,42 @@ def test_required_union(client): assert "error" in r.json +def test_required_union_decorator(client): + url = "/query/union/decorator/required" + # Test that present bool input yields input value + r = client.get(url, query_string={"v": True}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present int input yields input value + r = client.get(url, query_string={"v": 5541}) + assert "v" in r.json + assert r.json["v"] == 5541 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-bool/int input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + +def test_required_union_async_decorator(client): + url = "/query/union/async_decorator/required" + # Test that present bool input yields input value + r = client.get(url, query_string={"v": True}) + assert "v" in r.json + assert r.json["v"] is True + # Test that present int input yields input value + r = client.get(url, query_string={"v": 5541}) + assert "v" in r.json + assert r.json["v"] == 5541 + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + # Test that present non-bool/int input yields error + r = client.get(url, query_string={"v": "a"}) + assert "error" in r.json + + def test_optional_union(client): url = "/query/union/optional" # Test that missing input yields None @@ -679,6 +1173,49 @@ def test_required_list_str(client): r = client.get(url) assert "error" in r.json + +def test_required_list_str_decorator(client): + url = "/query/list/decorator/req_str" + # Test that present single str input yields [input value] + r = client.get(url, query_string={"v": "w"}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 1 + assert type(r.json["v"][0]) is str + assert r.json["v"][0] == "w" + # Test that present CSV str input yields [input values] + v = ["x", "y"] + r = client.get(url, query_string={"v": v}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 2 + list_assertion_helper(2, str, v, r.json["v"]) + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + +def test_required_list_str_async_decorator(client): + url = "/query/list/async_decorator/req_str" + # Test that present single str input yields [input value] + r = client.get(url, query_string={"v": "w"}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 1 + assert type(r.json["v"][0]) is str + assert r.json["v"][0] == "w" + # Test that present CSV str input yields [input values] + v = ["x", "y"] + r = client.get(url, query_string={"v": v}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 2 + list_assertion_helper(2, str, v, r.json["v"]) + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + def test_required_list_str_multiple_params(client): url = "/query/list/req_str" # Test that present single str input yields [input value] @@ -699,6 +1236,49 @@ def test_required_list_str_multiple_params(client): r = client.get(url) assert "error" in r.json + +def test_required_list_str_multiple_params_decorator(client): + url = "/query/list/decorator/req_str" + # Test that present single str input yields [input value] + r = client.get(url, query_string={"v": "w"}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 1 + assert type(r.json["v"][0]) is str + assert r.json["v"][0] == "w" + # Test that present multiple separate str inputs yields [input values] + v = ["x", "y"] + r = client.get(f"{url}?v=x&v=y") + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 2 + list_assertion_helper(2, str, v, r.json["v"]) + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + +def test_required_list_str_multiple_params_async_decorator(client): + url = "/query/list/async_decorator/req_str" + # Test that present single str input yields [input value] + r = client.get(url, query_string={"v": "w"}) + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 1 + assert type(r.json["v"][0]) is str + assert r.json["v"][0] == "w" + # Test that present multiple separate str inputs yields [input values] + v = ["x", "y"] + r = client.get(f"{url}?v=x&v=y") + assert "v" in r.json + assert type(r.json["v"]) is list + assert len(r.json["v"]) == 2 + list_assertion_helper(2, str, v, r.json["v"]) + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + + def test_required_list_int(client): url = "/query/list/req_int" # Test that present single int input yields [input value] @@ -907,6 +1487,62 @@ def test_list_default(client): list_assertion_helper(2, int, opt, r.json["opt"]) +def test_list_default_decorator(client): + url = "/query/list/decorator/default" + # Test that missing input for required and optional yields default values + n_opt = ["a", "b"] + opt = [0, 1] + r = client.get(url) + assert "n_opt" in r.json + assert type(r.json["n_opt"]) is list + assert len(r.json["n_opt"]) == 2 + list_assertion_helper(2, str, n_opt, r.json["n_opt"]) + assert "opt" in r.json + assert type(r.json["opt"]) is list + assert len(r.json["opt"]) == 2 + list_assertion_helper(2, int, opt, r.json["opt"]) + # Test that present bool input for required and optional yields [input values] + opt = [2, 3] + n_opt = ["c", "d"] + r = client.get(url, query_string={"opt": opt, "n_opt": n_opt}) + assert "n_opt" in r.json + assert type(r.json["n_opt"]) is list + assert len(r.json["n_opt"]) == 2 + list_assertion_helper(2, str, n_opt, r.json["n_opt"]) + assert "opt" in r.json + assert type(r.json["opt"]) is list + assert len(r.json["opt"]) == 2 + list_assertion_helper(2, int, opt, r.json["opt"]) + + +def test_list_default_async_decorator(client): + url = "/query/list/async_decorator/default" + # Test that missing input for required and optional yields default values + n_opt = ["a", "b"] + opt = [0, 1] + r = client.get(url) + assert "n_opt" in r.json + assert type(r.json["n_opt"]) is list + assert len(r.json["n_opt"]) == 2 + list_assertion_helper(2, str, n_opt, r.json["n_opt"]) + assert "opt" in r.json + assert type(r.json["opt"]) is list + assert len(r.json["opt"]) == 2 + list_assertion_helper(2, int, opt, r.json["opt"]) + # Test that present bool input for required and optional yields [input values] + opt = [2, 3] + n_opt = ["c", "d"] + r = client.get(url, query_string={"opt": opt, "n_opt": n_opt}) + assert "n_opt" in r.json + assert type(r.json["n_opt"]) is list + assert len(r.json["n_opt"]) == 2 + list_assertion_helper(2, str, n_opt, r.json["n_opt"]) + assert "opt" in r.json + assert type(r.json["opt"]) is list + assert len(r.json["opt"]) == 2 + list_assertion_helper(2, int, opt, r.json["opt"]) + + def test_list_func(client): url = "/query/list/func" # Test that input passing func yields input diff --git a/flask_parameter_validation/test/test_route_params.py b/flask_parameter_validation/test/test_route_params.py index b64f728..05784b7 100644 --- a/flask_parameter_validation/test/test_route_params.py +++ b/flask_parameter_validation/test/test_route_params.py @@ -25,6 +25,28 @@ def test_required_str(client): assert r.status_code == 404 +def test_required_str_decorator(client): + url = "/route/str/decorator/required" + # Test that present input yields input value + r = client.get(f"{url}/v") + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input is 404 + r = client.get(f"{url}") + assert r.status_code == 404 + + +def test_required_str_async_decorator(client): + url = "/route/str/async_decorator/required" + # Test that present input yields input value + r = client.get(f"{url}/v") + assert "v" in r.json + assert r.json["v"] == "v" + # Test that missing input is 404 + r = client.get(f"{url}") + assert r.status_code == 404 + + def test_str_min_str_length(client): url = "/route/str/min_str_length" # Test that below minimum yields error diff --git a/flask_parameter_validation/test/testing_blueprints/bool_blueprint.py b/flask_parameter_validation/test/testing_blueprints/bool_blueprint.py index 4acc129..a3d4be2 100644 --- a/flask_parameter_validation/test/testing_blueprints/bool_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/bool_blueprint.py @@ -4,6 +4,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_bool_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -19,6 +20,21 @@ def required(v: bool = ParamType()): assert type(v) is bool return jsonify({"v": v}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: bool = ParamType()): + assert type(v) is bool + return jsonify({"v": v}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: bool = ParamType()): + assert type(v) is bool + return jsonify({"v": v}) + + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[bool] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/date_blueprint.py b/flask_parameter_validation/test/testing_blueprints/date_blueprint.py index e85c4ba..0b6460b 100644 --- a/flask_parameter_validation/test/testing_blueprints/date_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/date_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_date_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -20,6 +21,20 @@ def required(v: datetime.date = ParamType()): assert type(v) is datetime.date return jsonify({"v": v.isoformat()}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: datetime.date = ParamType()): + assert type(v) is datetime.date + return jsonify({"v": v.isoformat()}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: datetime.date = ParamType()): + assert type(v) is datetime.date + return jsonify({"v": v.isoformat()}) + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[datetime.date] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/datetime_blueprint.py b/flask_parameter_validation/test/testing_blueprints/datetime_blueprint.py index ba110a6..67d205f 100644 --- a/flask_parameter_validation/test/testing_blueprints/datetime_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/datetime_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_datetime_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -20,6 +21,21 @@ def required(v: datetime.datetime = ParamType()): assert type(v) is datetime.datetime return jsonify({"v": v.isoformat()}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: datetime.datetime = ParamType()): + assert type(v) is datetime.datetime + return jsonify({"v": v.isoformat()}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: datetime.datetime = ParamType()): + assert type(v) is datetime.datetime + return jsonify({"v": v.isoformat()}) + + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[datetime.datetime] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/dict_blueprint.py b/flask_parameter_validation/test/testing_blueprints/dict_blueprint.py index 4cd6139..16d71b8 100644 --- a/flask_parameter_validation/test/testing_blueprints/dict_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/dict_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_dict_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -37,6 +38,30 @@ def default( "opt": opt }) + @decorator("/decorator/default") + @dummy_decorator + @ValidateParameters() + def decorator_default( + n_opt: dict = ParamType(default={"a": "b"}), + opt: dict = ParamType(default={"c": "d"}) + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + + @decorator("/async_decorator/default") + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_default( + n_opt: dict = ParamType(default={"a": "b"}), + opt: dict = ParamType(default={"c": "d"}) + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + def are_keys_lowercase(v): assert type(v) is dict for key in v.keys(): diff --git a/flask_parameter_validation/test/testing_blueprints/dummy_decorators.py b/flask_parameter_validation/test/testing_blueprints/dummy_decorators.py new file mode 100644 index 0000000..f4297f3 --- /dev/null +++ b/flask_parameter_validation/test/testing_blueprints/dummy_decorators.py @@ -0,0 +1,17 @@ +""" +Decorators that do nothing and test if the ValidateParameters decorator breaks +other decorators upstream. +""" +from functools import wraps + +def dummy_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +def dummy_async_decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + return await func(*args, **kwargs) + return wrapper diff --git a/flask_parameter_validation/test/testing_blueprints/file_blueprint.py b/flask_parameter_validation/test/testing_blueprints/file_blueprint.py index c3a5edb..fd75643 100644 --- a/flask_parameter_validation/test/testing_blueprints/file_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/file_blueprint.py @@ -6,6 +6,7 @@ from werkzeug.utils import secure_filename from flask_parameter_validation import ValidateParameters, File +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator resources = Path(__file__).parent.parent / 'uploads' @@ -19,6 +20,21 @@ def required(v: FileStorage = File()): assert type(v) is FileStorage return jsonify({"success": True}) + @file_bp.post("/decorator/required") + @dummy_decorator + @ValidateParameters() + def decorator_required(v: FileStorage = File()): + assert type(v) is FileStorage + return jsonify({"success": True}) + + @file_bp.post("/async_decorator/required") + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: FileStorage = File()): + assert type(v) is FileStorage + return jsonify({"success": True}) + + @file_bp.post("/optional") @ValidateParameters() def optional(v: Optional[FileStorage] = File()): diff --git a/flask_parameter_validation/test/testing_blueprints/float_blueprint.py b/flask_parameter_validation/test/testing_blueprints/float_blueprint.py index 7ce9bcc..d823cd8 100644 --- a/flask_parameter_validation/test/testing_blueprints/float_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/float_blueprint.py @@ -4,6 +4,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_float_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -19,6 +20,21 @@ def required(v: float = ParamType()): assert type(v) is float return jsonify({"v": v}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: float = ParamType()): + assert type(v) is float + return jsonify({"v": v}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: float = ParamType()): + assert type(v) is float + return jsonify({"v": v}) + + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[float] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/int_blueprint.py b/flask_parameter_validation/test/testing_blueprints/int_blueprint.py index 98415dd..c48196b 100644 --- a/flask_parameter_validation/test/testing_blueprints/int_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/int_blueprint.py @@ -4,6 +4,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_int_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -19,6 +20,20 @@ def required(v: int = ParamType()): assert type(v) is int return jsonify({"v": v}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: int = ParamType()): + assert type(v) is int + return jsonify({"v": v}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: int = ParamType()): + assert type(v) is int + return jsonify({"v": v}) + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[int] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/list_blueprint.py b/flask_parameter_validation/test/testing_blueprints/list_blueprint.py index 9ef58a0..d72a7c7 100644 --- a/flask_parameter_validation/test/testing_blueprints/list_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/list_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_list_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -21,6 +22,22 @@ def req_str(v: List[str] = ParamType()): assert type(v) is list assert type(v[0]) is str return jsonify({"v": v}) + + @decorator("/decorator/req_str") + @dummy_decorator + @ValidateParameters() + def decorator_req_str(v: List[str] = ParamType()): + assert type(v) is list + assert type(v[0]) is str + return jsonify({"v": v}) + + @decorator("/async_decorator/req_str") + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_req_str(v: List[str] = ParamType()): + assert type(v) is list + assert type(v[0]) is str + return jsonify({"v": v}) @decorator("/req_int") @ValidateParameters() @@ -85,6 +102,30 @@ def default( "opt": opt }) + @decorator("/decorator/default") + @dummy_decorator + @ValidateParameters() + def decorator_default( + n_opt: List[str] = ParamType(default=["a", "b"]), + opt: Optional[List[int]] = ParamType(default=[0, 1]) + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + + @decorator("/async_decorator/default") + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_default( + n_opt: List[str] = ParamType(default=["a", "b"]), + opt: Optional[List[int]] = ParamType(default=[0, 1]) + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + def is_len_even(v): assert type(v) is list return len(v) % 2 == 0 diff --git a/flask_parameter_validation/test/testing_blueprints/str_blueprint.py b/flask_parameter_validation/test/testing_blueprints/str_blueprint.py index 4e04fa0..1638172 100644 --- a/flask_parameter_validation/test/testing_blueprints/str_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/str_blueprint.py @@ -4,6 +4,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_str_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -88,4 +89,172 @@ def alias( ): return jsonify({"value": value}) - return str_bp + # Test Parent Decorators + + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: str = ParamType()): + assert type(v) is str + return jsonify({"v": v}) + + @decorator("/decorator/optional") # Route not currently supported by Optional + @dummy_decorator + @ValidateParameters() + def decorator_optional(v: Optional[str] = ParamType()): + return jsonify({"v": v}) + + @decorator("/decorator/default") # Route not currently supported by default + @dummy_decorator + @ValidateParameters() + def decorator_default( + n_opt: str = ParamType(default="not_optional"), + opt: Optional[str] = ParamType(default="optional") + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + + @decorator(path("/decorator/min_str_length", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_min_str_length( + v: str = ParamType(min_str_length=2) + ): + return jsonify({"v": v}) + + @decorator(path("/decorator/max_str_length", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_max_str_length( + v: str = ParamType(max_str_length=2) + ): + return jsonify({"v": v}) + + @decorator(path("/decorator/whitelist", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_whitelist( + v: str = ParamType(whitelist="ABC123") + ): + return jsonify({"v": v}) + + @decorator(path("/decorator/blacklist", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_blacklist( + v: str = ParamType(blacklist="ABC123") + ): + return jsonify({"v": v}) + + @decorator(path("/decorator/pattern", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_pattern( + v: str = ParamType(pattern="\\w{3}\\d{3}") + ): + return jsonify({"v": v}) + + @decorator(path("/decorator/func", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_func( + v: str = ParamType(func=is_digit) + ): + assert type(v) is str + return jsonify({"v": v}) + + @decorator("/decorator/alias") # Route not currently supported by alias + @dummy_decorator + @ValidateParameters() + def decorator_alias( + value: str = ParamType(alias="v") + ): + return jsonify({"value": value}) + + # Test Parent Decorators Async + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: str = ParamType()): + assert type(v) is str + return jsonify({"v": v}) + + @decorator("/async_decorator/optional") # Route not currently supported by Optional + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_optional(v: Optional[str] = ParamType()): + return jsonify({"v": v}) + + @decorator("/async_decorator/default") # Route not currently supported by default + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_default( + n_opt: str = ParamType(default="not_optional"), + opt: Optional[str] = ParamType(default="optional") + ): + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + + @decorator(path("/async_decorator/min_str_length", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_min_str_length( + v: str = ParamType(min_str_length=2) + ): + return jsonify({"v": v}) + + @decorator(path("/async_decorator/max_str_length", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_max_str_length( + v: str = ParamType(max_str_length=2) + ): + return jsonify({"v": v}) + + @decorator(path("/async_decorator/whitelist", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_whitelist( + v: str = ParamType(whitelist="ABC123") + ): + return jsonify({"v": v}) + + @decorator(path("/async_decorator/blacklist", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_blacklist( + v: str = ParamType(blacklist="ABC123") + ): + return jsonify({"v": v}) + + @decorator(path("/async_decorator/pattern", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_pattern( + v: str = ParamType(pattern="\\w{3}\\d{3}") + ): + return jsonify({"v": v}) + + @decorator(path("/async_decorator/func", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_func( + v: str = ParamType(func=is_digit) + ): + assert type(v) is str + return jsonify({"v": v}) + + @decorator("/async_decorator/alias") # Route not currently supported by alias + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_alias( + value: str = ParamType(alias="v") + ): + return jsonify({"value": value}) + + return str_bp \ No newline at end of file diff --git a/flask_parameter_validation/test/testing_blueprints/time_blueprint.py b/flask_parameter_validation/test/testing_blueprints/time_blueprint.py index 05c36ee..53afcce 100644 --- a/flask_parameter_validation/test/testing_blueprints/time_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/time_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_time_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -20,6 +21,20 @@ def required(v: datetime.time = ParamType()): assert type(v) is datetime.time return jsonify({"v": v.isoformat()}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: datetime.time = ParamType()): + assert type(v) is datetime.time + return jsonify({"v": v.isoformat()}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: datetime.time = ParamType()): + assert type(v) is datetime.time + return jsonify({"v": v.isoformat()}) + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[datetime.time] = ParamType()): diff --git a/flask_parameter_validation/test/testing_blueprints/union_blueprint.py b/flask_parameter_validation/test/testing_blueprints/union_blueprint.py index 80df5ec..8f2bd3e 100644 --- a/flask_parameter_validation/test/testing_blueprints/union_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/union_blueprint.py @@ -5,6 +5,7 @@ from flask_parameter_validation import ValidateParameters, Route from flask_parameter_validation.parameter_types.parameter import Parameter +from flask_parameter_validation.test.testing_blueprints.dummy_decorators import dummy_decorator, dummy_async_decorator def get_union_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: @@ -20,6 +21,20 @@ def required(v: Union[bool, int] = ParamType()): assert type(v) is bool or type(v) is int return jsonify({"v": v}) + @decorator(path("/decorator/required", "/")) + @dummy_decorator + @ValidateParameters() + def decorator_required(v: Union[bool, int] = ParamType()): + assert type(v) is bool or type(v) is int + return jsonify({"v": v}) + + @decorator(path("/async_decorator/required", "/")) + @dummy_async_decorator + @ValidateParameters() + async def async_decorator_required(v: Union[bool, int] = ParamType()): + assert type(v) is bool or type(v) is int + return jsonify({"v": v}) + @decorator("/optional") # Route not supported by Optional @ValidateParameters() def optional(v: Optional[Union[bool, int]] = ParamType()): diff --git a/setup.py b/setup.py index 4d81355..6d77136 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='Flask-Parameter-Validation', - version='2.3.0', + version='2.3.1', url='https://github.com/Ge0rg3/flask-parameter-validation', license='MIT', author='George Omnet',