diff --git a/CHANGES.txt b/CHANGES.txt index b09231b7..f5f87cab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -41,6 +41,8 @@ Fixed: for client.py-Client::_serve_file(). (John Rouillard) - issue2551381 - roundup-server parses URI's with multiple '?" incorrectly. (John Rouillard) +- issue2551382 - invalid @verbose, @page_* values in rest uri's + generate 409 not 400 error. (John Rouillard) Features: diff --git a/roundup/rest.py b/roundup/rest.py index 12b55cef..771eb976 100644 --- a/roundup/rest.py +++ b/roundup/rest.py @@ -811,10 +811,18 @@ def get_collection(self, class_name, input_payload): value = form_field.value if key.startswith("@page_"): # serve the paging purpose key = key[6:] - value = int(value) + try: + value = int(value) + except ValueError as e: + raise UsageError("When using @page_%s: %s" % + (key, e.args[0])) page[key] = value elif key == "@verbose": - verbose = int(value) + try: + verbose = int(value) + except ValueError as e: + raise UsageError("When using @verbose: %s" % + (e.args[0])) elif key in ["@fields", "@attrs"]: f = value.split(",") if len(f) == 1: @@ -1129,7 +1137,11 @@ def get_element(self, class_name, item_id, input_payload): # used only if no @fields/@attrs protected = value.lower() == "true" elif key == "@verbose": - verbose = int(value) + try: + verbose = int(value) + except ValueError as e: + raise UsageError("When using @verbose: %s" % + (e.args[0])) result = {} if props is None: diff --git a/test/test_liveserver.py b/test/test_liveserver.py index de603302..cd62cd38 100644 --- a/test/test_liveserver.py +++ b/test/test_liveserver.py @@ -20,6 +20,37 @@ skip_requests = mark_class(pytest.mark.skip( reason='Skipping liveserver tests: requests library not available')) +try: + import hypothesis + skip_hypothesis = lambda func, *args, **kwargs: func + + # ruff: noqa: E402 + from hypothesis import example, given, settings + from hypothesis.strategies import binary, characters, none, one_of, sampled_from, text + +except ImportError: + from .pytest_patcher import mark_class + skip_hypothesis = mark_class(pytest.mark.skip( + reason='Skipping hypothesis liveserver tests: hypothesis library not available')) + + # define a dummy decorator that can take args + def noop_decorators_with_args(*args, **kwargs): + def noop_decorators(func): + def internal(): + pass + return internal + return noop_decorators + + # define a dummy strategy + def noop_strategy(*args, **kwargs): + pass + + # define the decorator functions + example = given = settings = noop_decorators_with_args + # and stratgies using in decorators + binary = characters = none = one_of = sampled_from = text = noop_strategy + + try: import brotli skip_brotli = lambda func, *args, **kwargs: func @@ -149,11 +180,9 @@ def create_app(self): # doesn't support the max bytes to read argument. return RequestDispatcher(self.dirname) - -@skip_requests -class BaseTestCases(WsgiSetup): - """Class with all tests to run against wsgi server. Is reused when - wsgi server is started with various feature flags +class ClientSetup(): + """ Utility programs for the client querying a server. + Just a login session at the moment but more to come I am sure. """ def create_login_session(self, username="admin", password="sekrit", @@ -176,6 +205,63 @@ def create_login_session(self, username="admin", password="sekrit", return session return session, response + +@skip_hypothesis +class FuzzGetUrls(WsgiSetup, ClientSetup): + + _max_examples = 100 + + @given(sampled_from(['@verbose', '@page_size', '@page_index']), + one_of(characters(),text(min_size=1))) + @settings(max_examples=_max_examples, + deadline=10000) # 10000ms + def test_class_url_param_accepting_integer_values(self, param, value): + """Tests all integer args for rest url. @page_* is the + same code for all *. + """ + session, _response = self.create_login_session() + url = '%s/rest/data/status' % (self.url_base()) + query = '%s=%s' % (param, value) + f = session.get(url, params=query) + try: + if int(value) >= 0: + self.assertEqual(f.status_code, 200) + except ValueError: + if value in ['#', '&']: + self.assertEqual(f.status_code, 200) + else: + # invalid value for param + self.assertEqual(f.status_code, 400) + + @given(sampled_from(['@verbose']), + one_of(characters(),text(min_size=1))) + @settings(max_examples=_max_examples, + deadline=10000) # 10000ms + def test_element_url_param_accepting_integer_values(self, param, value): + """Tests all integer args for rest url. @page_* is the + same code for all *. + """ + session, _response = self.create_login_session() + url = '%s/rest/data/status/1' % (self.url_base()) + query = '%s=%s' % (param, value) + f = session.get(url, params=query) + try: + if int(value) >= 0: + self.assertEqual(f.status_code, 200) + except ValueError: + if value in ['#', '&']: + self.assertEqual(f.status_code, 200) + else: + # invalid value for param + self.assertEqual(f.status_code, 400) + + +@skip_requests +class BaseTestCases(WsgiSetup, ClientSetup): + """Class with all tests to run against wsgi server. Is reused when + wsgi server is started with various feature flags + """ + def test_cookie_attributes(self): session, _response = self.create_login_session()