diff --git a/.gitignore b/.gitignore index 231fd24..50a3a94 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ build/ .eggs/ tags +__pycache__/ +*.pyc diff --git a/poorwsgi/wsgi.py b/poorwsgi/wsgi.py index 605a579..7383bcd 100644 --- a/poorwsgi/wsgi.py +++ b/poorwsgi/wsgi.py @@ -33,6 +33,10 @@ # check, if there is define filter in uri re_filter = re.compile(r'<(\w+)(:[^>]+)?>') +# check for invalid route definitions with spaces +# Matches: <{space}name, , +re_invalid_filter = re.compile(r'<\s+\w+|<\w+\s+[:|>]|<\w+:\s+\w+|<\w+:[^>]+\s+>') + # Supported authorization algorithms AUTH_DIGEST_ALGORITHMS = { 'MD5': md5, @@ -792,6 +796,16 @@ def set_route(self, uri: str, fun: Callable, app.set_route('/use/post', user_create, METHOD_POST) """ + # Check for invalid spaces in route filter definitions + if re_invalid_filter.search(uri): + msg = ( + f"Invalid route definition '{uri}': " + "Route filter definitions must not contain spaces. " + "Use '' format without any spaces. " + "Examples: '', '', ''" + ) + raise ValueError(msg) + if re_filter.search(uri): r_uri = re_filter.sub(self.__regex, uri) + '$' converters = tuple((g[0], self.__converter(g[1])) diff --git a/tests/test_route_validation.py b/tests/test_route_validation.py new file mode 100644 index 0000000..56ddf1d --- /dev/null +++ b/tests/test_route_validation.py @@ -0,0 +1,215 @@ +"""Unit tests for route filter validation.""" +import pytest + +from poorwsgi.wsgi import Application +from poorwsgi.state import METHOD_GET + + +def test_valid_route_no_filter(): + """Test that routes without filters work correctly.""" + app = Application('test_valid_no_filter') + + @app.route('/api/users') + def handler(req): + return 'ok' + + assert '/api/users' in app.routes + + +def test_valid_route_with_int_filter(): + """Test that routes with :int filter work correctly.""" + app = Application('test_valid_int') + + @app.route('/api/users/') + def handler(req, id): # noqa: A002 + return 'ok' + + # Route with filter should be in regular_routes + assert len(app.regular_routes) > 0 + + +def test_valid_route_with_word_filter(): + """Test that routes with :word filter work correctly.""" + app = Application('test_valid_word') + + @app.route('/api/users/') + def handler(req, name): + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_valid_route_with_float_filter(): + """Test that routes with :float filter work correctly.""" + app = Application('test_valid_float') + + @app.route('/api/values/') + def handler(req, value): + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_valid_route_with_uuid_filter(): + """Test that routes with :uuid filter work correctly.""" + app = Application('test_valid_uuid') + + @app.route('/api/objects/') + def handler(req, id): # noqa: A002 + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_valid_route_with_hex_filter(): + """Test that routes with :hex filter work correctly.""" + app = Application('test_valid_hex') + + @app.route('/api/codes/') + def handler(req, code): + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_valid_route_with_multiple_filters(): + """Test that routes with multiple filters work correctly.""" + app = Application('test_valid_multiple') + + @app.route('/api//') + def handler(req, entity, id): # noqa: A002 + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_invalid_route_space_after_open_bracket(): + """Test that space after < is rejected.""" + app = Application('test_invalid_space_after_open') + + with pytest.raises(ValueError, match=r'Invalid route definition.*must not contain spaces'): + @app.route('/api/< id:int>') + def handler(req, id): # noqa: A002 + return 'ok' + + +def test_invalid_route_space_before_close_bracket(): + """Test that space before > is rejected.""" + app = Application('test_invalid_space_before_close') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/') + def handler(req, id): # noqa: A002 + return 'ok' + + +def test_invalid_route_space_after_name(): + """Test that space after parameter name is rejected.""" + app = Application('test_invalid_space_after_name') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/') + def handler(req, id): # noqa: A002 + return 'ok' + + +def test_invalid_route_space_after_colon(): + """Test that space after : is rejected.""" + app = Application('test_invalid_space_after_colon') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/') + def handler(req, id): # noqa: A002 + return 'ok' + + +def test_invalid_route_multiple_spaces(): + """Test that multiple spaces are rejected.""" + app = Application('test_invalid_multiple_spaces') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/< id : int >') + def handler(req, id): # noqa: A002 + return 'ok' + + +def test_error_message_provides_examples(): + """Test that error message includes helpful examples.""" + app = Application('test_error_message') + + with pytest.raises(ValueError, match='Invalid route definition') as exc_info: + @app.route('/api/') + def handler(req, id): # noqa: A002 + return 'ok' + + error_message = str(exc_info.value) + assert '' in error_message + assert '' in error_message + assert '' in error_message + + +def test_set_route_with_space_rejection(): + """Test that set_route method also rejects spaces.""" + app = Application('test_set_route_space') + + def handler(req, id): # noqa: A002 + return 'ok' + + with pytest.raises(ValueError, match='Invalid route definition'): + app.set_route('/api/< id:int>', handler, METHOD_GET) + + +def test_route_with_no_filter_and_angle_brackets(): + """Test that routes without filters but with <> in path work.""" + app = Application('test_no_filter') + + @app.route('/api/') + def handler(req, id): # noqa: A002 + return 'ok' + + # Route with but no filter should work + assert len(app.regular_routes) > 0 + + +def test_custom_filter_no_spaces(): + """Test that custom filters work without spaces.""" + app = Application('test_custom_filter') + app.set_filter('custom', r'[a-z]+') + + @app.route('/api/') + def handler(req, value): + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_custom_filter_with_space_rejection(): + """Test that custom filters reject spaces.""" + app = Application('test_custom_filter_space') + app.set_filter('custom', r'[a-z]+') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/') + def handler(req, value): + return 'ok' + + +def test_re_filter_no_spaces(): + """Test that :re: filter works without spaces.""" + app = Application('test_re_filter') + + @app.route('/api/') + def handler(req, value): + return 'ok' + + assert len(app.regular_routes) > 0 + + +def test_re_filter_with_space_rejection(): + """Test that :re: filter rejects spaces before >.""" + app = Application('test_re_filter_space') + + with pytest.raises(ValueError, match='Invalid route definition'): + @app.route('/api/') + def handler(req, value): + return 'ok'