changeset 8213:14e92a595828

fix(web) issue2551382 - 409 not 400 errors returned invalid integer values for @verbose, @page_* values in rest uri's generated a 409 (Update Conflict) error not a generic 400 error. Found it when I was working on adding fuzz testing to check error handling for query parameters in REST url's. This also ads the tests in test_liveserver that found the error. Also refactored tst_liveserver to allow resuse of session login method for the new fuzz testing class as well.
author John Rouillard <rouilj@ieee.org>
date Sun, 15 Dec 2024 01:57:42 -0500
parents 79b9343794f5
children 55b0abde56ab
files CHANGES.txt roundup/rest.py test/test_liveserver.py
diffstat 3 files changed, 108 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sun Dec 15 01:35:51 2024 -0500
+++ b/CHANGES.txt	Sun Dec 15 01:57:42 2024 -0500
@@ -41,6 +41,8 @@
   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:
 
--- a/roundup/rest.py	Sun Dec 15 01:35:51 2024 -0500
+++ b/roundup/rest.py	Sun Dec 15 01:57:42 2024 -0500
@@ -811,10 +811,18 @@
             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 @@
                 # 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:
--- a/test/test_liveserver.py	Sun Dec 15 01:35:51 2024 -0500
+++ b/test/test_liveserver.py	Sun Dec 15 01:57:42 2024 -0500
@@ -21,6 +21,37 @@
         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
 except ImportError:
@@ -149,11 +180,9 @@
             # 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 @@
             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()
 

Roundup Issue Tracker: http://roundup-tracker.org/