Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
35cdb5a
First attempt GitHub Workflow Setup
smt5541 Feb 6, 2024
464e84a
Add tests for required and optional str, and str default and min_str_…
smt5541 Feb 6, 2024
89302fb
Fix bug in parameter.py that caused unexpected behavior with min_str_…
smt5541 Feb 6, 2024
24275fb
Reorganize Test Flask app, add tests for remaining Query str validati…
smt5541 Feb 7, 2024
f2cb2f4
Add tests for Query int validation parameters
smt5541 Feb 8, 2024
37b6d93
Fix bug that caused int validation func to receive unconverted str fr…
smt5541 Feb 8, 2024
8554955
Add regression tests to ensure that parameters are converted before b…
smt5541 Feb 8, 2024
aa623d8
Add tests for Query bool validation parameters
smt5541 Feb 8, 2024
1e53056
Add tests for Query float validation parameters
smt5541 Feb 8, 2024
1bc5774
Restructure Testing Application to allow reuse of blueprints for diff…
smt5541 Feb 9, 2024
79cc9ad
Fix Comment
smt5541 Feb 9, 2024
f8e5dbc
Add tests for Query datetime.datetime validation parameters
smt5541 Feb 9, 2024
1e8d8f0
Add tests for Query datetime.date validation parameters
smt5541 Feb 10, 2024
db2b33d
Add tests for Query datetime.time validation parameters
smt5541 Feb 10, 2024
165e840
Add tests for Query Union validation parameters
smt5541 Feb 10, 2024
d245f0d
Fix bug in Query#convert that caused Union[bool, int] to fail when in…
smt5541 Feb 10, 2024
be52c26
Add most tests for Query List validation parameters for most List chi…
smt5541 Feb 10, 2024
0a20bfb
Fix bug that prevented func passed in on a List type parameter from r…
smt5541 Feb 10, 2024
c0a8045
Add tests for Query List validation of datetime.datetime, datetime.da…
smt5541 Feb 11, 2024
8a67876
Reformat Query tests to use FlaskClient#get's query_string parameter,…
smt5541 Feb 11, 2024
1d0db58
Extend get_parameter_blueprint to allow for it to use any HTTP Verb f…
smt5541 Feb 11, 2024
4d6bcbc
Add tests for Json validation for all parameter types
smt5541 Feb 11, 2024
46ae8d8
Add tests for Form validation for all parameter types and change min_…
smt5541 Feb 11, 2024
8bbe249
Add tests for Route str validation
smt5541 Feb 11, 2024
2988f7c
Add tests for Route int validation
smt5541 Feb 11, 2024
1e37b7d
Fix bug (caused by Flask) where negative integers are not accepted in…
smt5541 Feb 11, 2024
85c3c62
Add tests for Route bool validation
smt5541 Feb 11, 2024
2dbb9d8
Add tests for Route float validation
smt5541 Feb 11, 2024
9a67d39
Add tests for Route datetime validation
smt5541 Feb 11, 2024
37ec49e
Add tests for Route date validation
smt5541 Feb 11, 2024
9f059fd
Add tests for Route time validation
smt5541 Feb 11, 2024
5505189
Add tests for Route Union validation
smt5541 Feb 11, 2024
02c3138
Add note about Route List parsing not being supported currently
smt5541 Feb 11, 2024
75dca30
Add test for ?v=x&v=y in Query List tests
smt5541 Feb 11, 2024
ab6ccb0
Add tests for File validation
smt5541 Feb 11, 2024
a71de22
Fix issue where file.content_length was incorrect when using Flask de…
smt5541 Feb 11, 2024
712d7cb
Update CI Workflow to remove github-ci branch
smt5541 Feb 11, 2024
8d28aaa
Merge pull request #1 from smt5541/github-ci
smt5541 Feb 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Flask-Parameter-Validation Unit Tests

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up Python 3
uses: actions/setup-python@v3
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
cd flask_parameter_validation/test
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with Pytest
run: |
cd flask_parameter_validation
pytest
14 changes: 11 additions & 3 deletions flask_parameter_validation/parameter_types/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
- Would originally be in Flask's request.file
- Value will be a FileStorage object
"""
import io

from werkzeug.datastructures import FileStorage

from .parameter import Parameter


Expand All @@ -21,7 +25,7 @@ def __init__(
self.min_length = min_length
self.max_length = max_length

def validate(self, value):
def validate(self, value: FileStorage):
# Content type validation
if self.content_types is not None:
# We check mimetype, as it strips charset etc.
Expand All @@ -31,15 +35,19 @@ def validate(self, value):

# Min content length validation
if self.min_length is not None:
if value.content_length < self.min_length:
origin = value.stream.tell()
if value.stream.seek(0, io.SEEK_END) < self.min_length:
raise ValueError(
f"must have a content-length at least {self.min_length}."
)
value.stream.seek(origin)

# Max content length validation
if self.max_length is not None:
if value.content_length > self.max_length:
origin = value.stream.tell()
if value.stream.seek(0, io.SEEK_END) > self.max_length:
raise ValueError(
f"must have a content-length at most {self.max_length}."
)
value.stream.seek(origin)
return True
46 changes: 27 additions & 19 deletions flask_parameter_validation/parameter_types/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,27 @@ def __init__(
self.comment = comment
self.alias = alias

def func_helper(self, v):
func_result = self.func(v)
if type(func_result) is bool:
if not func_result:
raise ValueError(
"value does not match the validator function."
)
elif type(func_result) is tuple:
if len(func_result) == 2 and type(func_result[0]) is bool and type(func_result[1]) is str:
if not func_result[0]:
raise ValueError(
func_result[1]
)
else:
raise ValueError(
f"validator function returned incorrect type: {str(type(func_result))}, should return bool or (bool, str)"
)

# Validator
def validate(self, value):
original_value_type_list = type(value) is list
if type(value) is list:
values = value
# Min list len
Expand All @@ -59,14 +78,16 @@ def validate(self, value):
raise ValueError(
f"must have have a maximum of {self.max_list_length} items."
)
if self.func is not None:
self.func_helper(value)
else:
values = [value]

# Iterate through values given (or just one, if not list)
for value in values:
# Min length
if self.min_str_length is not None:
if hasattr(value, "len") and len(value) < self.min_str_length:
if len(value) < self.min_str_length:
raise ValueError(
f"must have at least {self.min_str_length} characters."
)
Expand Down Expand Up @@ -110,24 +131,11 @@ def validate(self, value):
f"pattern does not match: {self.pattern}."
)

# Callable
if self.func is not None:
func_result = self.func(value)
if type(func_result) is bool:
if not func_result:
raise ValueError(
"value does not match the validator function."
)
elif type(func_result) is tuple:
if len(func_result) == 2 and type(func_result[0]) is bool and type(func_result[1]) is str:
if not func_result[0]:
raise ValueError(
func_result[1]
)
else:
raise ValueError(
f"validator function returned incorrect type: {str(type(func_result))}, should return bool or (bool, str)"
)
# Callable (non-list)
if self.func is not None and not original_value_type_list:
self.func_helper(value)



return True

Expand Down
11 changes: 7 additions & 4 deletions flask_parameter_validation/parameter_types/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ def convert(self, value, allowed_types):
pass
# bool conversion
if bool in allowed_types:
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
try:
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
except AttributeError:
pass

return super().convert(value, allowed_types)
27 changes: 27 additions & 0 deletions flask_parameter_validation/parameter_types/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,30 @@ class Route(Parameter):

def __init__(self, default=None, **kwargs):
super().__init__(default, **kwargs)

def convert(self, value, allowed_types):
"""Convert query parameters to corresponding types."""
if type(value) is str:
# int conversion
if int in allowed_types:
try:
value = int(value)
except ValueError:
pass
# float conversion
if float in allowed_types:
try:
value = float(value)
except ValueError:
pass
# bool conversion
if bool in allowed_types:
try:
if value.lower() == "true":
value = True
elif value.lower() == "false":
value = False
except AttributeError:
pass

return super().convert(value, allowed_types)
5 changes: 4 additions & 1 deletion flask_parameter_validation/parameter_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ def validate(self, expected_input, all_request_inputs):

# Validate parameter-specific requirements are met
try:
expected_delivery_type.validate(user_input)
if type(user_input) is list:
expected_delivery_type.validate(user_input)
else:
expected_delivery_type.validate(user_inputs[0])
except ValueError as e:
raise ValidationError(str(e), expected_name, expected_input_type)

Expand Down
Empty file.
19 changes: 19 additions & 0 deletions flask_parameter_validation/test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest
from .testing_application import create_app


@pytest.fixture(scope="session")
def app():
app = create_app()
app.config.update({"TESTING": True})
yield app


@pytest.fixture()
def client(app):
return app.test_client()


@pytest.fixture()
def runner(app):
return app.test_cli_runner()
3 changes: 3 additions & 0 deletions flask_parameter_validation/test/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask==3.0.2
../../
requests
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions flask_parameter_validation/test/resources/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Test JSON Document",
"description": "This document will be uploaded to an API route that expects image/jpeg or image/png Content-Type, with the expectation that the API will return an error."
}
71 changes: 71 additions & 0 deletions flask_parameter_validation/test/test_file_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import datetime
import filecmp
from pathlib import Path
from typing import Type, List, Optional

resources = Path(__file__).parent / 'resources'


def test_required_file(client):
url = "/file/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
r = client.post(url, data={"v": (resources / "test.json").open("rb")})
assert "success" in r.json
assert r.json["success"]
assert "file_provided" in r.json
assert r.json["file_provided"] is True
# Test that we receive an error if a file is not provided
r = client.post(url)
assert "success" in r.json
assert r.json["success"]
assert "file_provided" in r.json
assert r.json["file_provided"] is False


def test_file_content_types(client):
url = "/file/content_types"
# Test that we receive a success response if a file of correct Content-Type 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 of incorrect Content-Type is provided
r = client.post(url, data={"v": (resources / "hubble_mars_10kB.jpg").open("rb")})
assert "error" in r.json


def test_file_min_length(client):
url = "/file/min_length"
# Test that we receive a success response if a file of correct Content-Length is provided
load_path = resources / "hubble_mars_10kB.jpg"
r = client.post(url, data={"v": load_path.open("rb")})
assert "success" in r.json
assert r.json["success"]
assert "save_path" in r.json
assert filecmp.cmp(load_path, r.json["save_path"])
# Test that we receive an error if a file of incorrect Content-Length is provided
r = client.post(url, data={"v": (resources / "test.json").open("rb")})
assert "error" in r.json

def test_file_max_length(client):
url = "/file/max_length"
# Test that we receive a success response if a file of correct Content-Length is provided
load_path = resources / "hubble_mars_10kB.jpg"
r = client.post(url, data={"v": load_path.open("rb")})
assert "success" in r.json
assert r.json["success"]
assert "save_path" in r.json
assert filecmp.cmp(load_path, r.json["save_path"])
# Test that we receive an error if a file of incorrect Content-Length is provided
r = client.post(url, data={"v": (resources / "aldrin_47kB.jpg").open("rb")})
assert "error" in r.json
Loading