comparison test/rest_common.py @ 8177:2967f37e73e4

refactor: issue2551289. invalid REST Accept header stops request Sending a POST, PUT (maybe PATCH) with an accept header that is not application/json or xml (if enabled) used to complete the request before throwing a 406 error. This was wrong. Now it reports an error without dispatching/processing the requested transaction. This is the first of a series of refactors of the dispatch method to make it faster and more readable by using return early pattern and extracting methods from the code. changes: The following now return 406 errors not 400 errors invalid version specified with @apiver in URL. invalid version specified with @apiver in payload body invalid version specified in accept headers as application/vnd.roundup.test-vz+json or version property Parsing the accept header returns a 400 when presented with a parameter without an = sign or other parse error. They used to return a 406 which is wrong since the header is malformed rather than having a value I can't respond to. Some error messages were made clearer. Results in the case of an error are proper json error object rather than text/plain strings. New test added for testdetermine_output_formatBadAccept that test the new method using the same test cases as for testDispatchBadAccept. I intend to extend the test coverage for determine_output_format to cover more cases. This should be a faster unit test than for dispatch. Removed .lower() calls for accept_mime_type as the input values are taken from the values in the __accepted_content_type dict which only has lower case values.
author John Rouillard <rouilj@ieee.org>
date Sun, 08 Dec 2024 01:09:34 -0500
parents 8e310a7b5e09
children d02ce1d14acd
comparison
equal deleted inserted replaced
8176:736f769b48c8 8177:2967f37e73e4
1835 self.assertEqual(self.server.client.response_code, 415) 1835 self.assertEqual(self.server.client.response_code, 415)
1836 self.assertNotIn("Accept-Patch", 1836 self.assertNotIn("Accept-Patch",
1837 self.server.client.additional_headers) 1837 self.server.client.additional_headers)
1838 self.server.client.additional_headers = {} 1838 self.server.client.additional_headers = {}
1839 1839
1840 def testdetermine_output_formatBadAccept(self):
1841 dof = self.server.determine_output_format
1842
1843 # simulate: /rest/data/issue expect failure unknown accept settings
1844 body=b'{ "title": "Joe Doe has problems", \
1845 "nosy": [ "1", "3" ], \
1846 "assignedto": "2", \
1847 "abool": true, \
1848 "afloat": 2.3, \
1849 "anint": 567890 \
1850 }'
1851 env = { "CONTENT_TYPE": "application/json",
1852 "CONTENT_LENGTH": len(body),
1853 "REQUEST_METHOD": "POST"
1854 }
1855 self.server.client.env.update(env)
1856 headers={"accept": "application/zot; version=1; q=0.5",
1857 "content-type": env['CONTENT_TYPE'],
1858 "content-length": env['CONTENT_LENGTH'],
1859 }
1860
1861 self.headers=headers
1862 # we need to generate a FieldStorage the looks like
1863 # FieldStorage(None, None, 'string') rather than
1864 # FieldStorage(None, None, [])
1865 body_file=BytesIO(body) # FieldStorage needs a file
1866 form = client.BinaryFieldStorage(body_file,
1867 headers=headers,
1868 environ=env)
1869 self.server.client.request.headers.get=self.get_header
1870
1871 (output_type, uri, error) = dof("/rest/data/issue")
1872
1873 self.assertEqual(self.server.client.response_code, 406)
1874 self.assertIn(b"Requested content type(s) 'application/zot; version=1; q=0.5' not available.\nAcceptable mime types are: */*, application/json",
1875 s2b(error['error']['msg']))
1876
1877 # simulate: /rest/data/issue works, multiple acceptable output, one
1878 # is valid
1879 self.server.client.response_code = ""
1880 env = { "CONTENT_TYPE": "application/json",
1881 "CONTENT_LENGTH": len(body),
1882 "REQUEST_METHOD": "POST"
1883 }
1884 self.server.client.env.update(env)
1885 headers={"accept": "application/zot; version=1; q=0.75, "
1886 "application/json; version=1; q=0.5",
1887 "content-type": env['CONTENT_TYPE'],
1888 "content-length": env['CONTENT_LENGTH'],
1889 }
1890
1891 self.headers=headers
1892 # we need to generate a FieldStorage the looks like
1893 # FieldStorage(None, None, 'string') rather than
1894 # FieldStorage(None, None, [])
1895 body_file=BytesIO(body) # FieldStorage needs a file
1896 form = client.BinaryFieldStorage(body_file,
1897 headers=headers,
1898 environ=env)
1899 self.server.client.request.headers.get=self.get_header
1900 (output_type, uri, error) = dof("/rest/data/issue")
1901
1902 self.assertEqual(self.server.client.response_code, "")
1903 self.assertEqual(output_type, "json")
1904 self.assertEqual(uri, "/rest/data/issue")
1905 self.assertEqual(error, None)
1906
1907 # test 3 accept is empty. This triggers */* so passes
1908 self.server.client.response_code = ""
1909 headers={"accept": "",
1910 "content-type": env['CONTENT_TYPE'],
1911 "content-length": env['CONTENT_LENGTH'],
1912 }
1913
1914 self.headers=headers
1915 # we need to generate a FieldStorage the looks like
1916 # FieldStorage(None, None, 'string') rather than
1917 # FieldStorage(None, None, [])
1918 body_file=BytesIO(body) # FieldStorage needs a file
1919 form = client.BinaryFieldStorage(body_file,
1920 headers=headers,
1921 environ=env)
1922 self.server.client.request.headers.get=self.get_header
1923 (output_type, uri, error) = dof("/rest/data/issue")
1924
1925 self.assertEqual(self.server.client.response_code, "")
1926 self.assertEqual(output_type, "json")
1927 self.assertEqual(uri, "/rest/data/issue")
1928 self.assertEqual(error, None)
1929
1930 # test 4 accept is random junk.
1931 headers={"accept": "Xyzzy I am not a mime, type;",
1932 "content-type": env['CONTENT_TYPE'],
1933 "content-length": env['CONTENT_LENGTH'],
1934 }
1935
1936 self.headers=headers
1937 # we need to generate a FieldStorage the looks like
1938 # FieldStorage(None, None, 'string') rather than
1939 # FieldStorage(None, None, [])
1940 body_file=BytesIO(body) # FieldStorage needs a file
1941 form = client.BinaryFieldStorage(body_file,
1942 headers=headers,
1943 environ=env)
1944 self.server.client.request.headers.get=self.get_header
1945 (output_type, uri, error) = dof("/rest/data/issue")
1946
1947 self.assertEqual(self.server.client.response_code, 400)
1948 self.assertEqual(output_type, None)
1949 self.assertEqual(uri, "/rest/data/issue")
1950 self.assertIn('Unable to parse Accept Header. Invalid media type: Xyzzy I am not a mime. Acceptable types: */* application/json', error['error']['msg'])
1951
1952 # test 5 accept mimetype is ok, param is not
1953 headers={"accept": "*/*; foo",
1954 "content-type": env['CONTENT_TYPE'],
1955 "content-length": env['CONTENT_LENGTH'],
1956 }
1957
1958 self.headers=headers
1959 # we need to generate a FieldStorage the looks like
1960 # FieldStorage(None, None, 'string') rather than
1961 # FieldStorage(None, None, [])
1962 body_file=BytesIO(body) # FieldStorage needs a file
1963 form = client.BinaryFieldStorage(body_file,
1964 headers=headers,
1965 environ=env)
1966 self.server.client.request.headers.get=self.get_header
1967 (output_type, uri, error) = dof("/rest/data/issue")
1968
1969 self.assertEqual(self.server.client.response_code, 400)
1970 self.assertEqual(output_type, None)
1971 self.assertEqual(uri, "/rest/data/issue")
1972 self.assertIn('Unable to parse Accept Header. Invalid param: foo. Acceptable types: */* application/json', error['error']['msg'])
1973
1840 def testDispatchBadAccept(self): 1974 def testDispatchBadAccept(self):
1841 # simulate: /rest/data/issue expect failure unknown accept settings 1975 # simulate: /rest/data/issue expect failure unknown accept settings
1842 body=b'{ "title": "Joe Doe has problems", \ 1976 body=b'{ "title": "Joe Doe has problems", \
1843 "nosy": [ "1", "3" ], \ 1977 "nosy": [ "1", "3" ], \
1844 "assignedto": "2", \ 1978 "assignedto": "2", \
1866 environ=env) 2000 environ=env)
1867 self.server.client.request.headers.get=self.get_header 2001 self.server.client.request.headers.get=self.get_header
1868 results = self.server.dispatch(env["REQUEST_METHOD"], 2002 results = self.server.dispatch(env["REQUEST_METHOD"],
1869 "/rest/data/issue", 2003 "/rest/data/issue",
1870 form) 2004 form)
1871
1872 print(results) 2005 print(results)
2006 json_dict = json.loads(b2s(results))
1873 self.assertEqual(self.server.client.response_code, 406) 2007 self.assertEqual(self.server.client.response_code, 406)
1874 self.assertIn(b"Requested content type 'application/zot; version=1; q=0.5' is not available.\nAcceptable types: */*, application/json", results) 2008 self.assertIn("Requested content type(s) 'application/zot; version=1; q=0.5' not available.\nAcceptable mime types are: */*, application/json",
2009 json_dict['error']['msg'])
1875 2010
1876 # simulate: /rest/data/issue works, multiple acceptable output, one 2011 # simulate: /rest/data/issue works, multiple acceptable output, one
1877 # is valid 2012 # is valid
1878 env = { "CONTENT_TYPE": "application/json", 2013 env = { "CONTENT_TYPE": "application/json",
1879 "CONTENT_LENGTH": len(body), 2014 "CONTENT_LENGTH": len(body),
1900 form) 2035 form)
1901 2036
1902 print(results) 2037 print(results)
1903 self.assertEqual(self.server.client.response_code, 201) 2038 self.assertEqual(self.server.client.response_code, 201)
1904 json_dict = json.loads(b2s(results)) 2039 json_dict = json.loads(b2s(results))
1905 # ERROR this should be 1. What's happening is that the code 2040 self.assertEqual(json_dict['data']['id'], "1")
1906 # for handling 406 error code runs through everything and creates
1907 # the item. Then it throws a 406 after the work is done when it
1908 # realizes it can't respond as requested. So the 406 post above
1909 # creates issue 1 and this one creates issue 2.
1910 self.assertEqual(json_dict['data']['id'], "2")
1911 2041
1912 2042
1913 # test 3 accept is empty. This triggers */* so passes 2043 # test 3 accept is empty. This triggers */* so passes
1914 headers={"accept": "", 2044 headers={"accept": "",
1915 "content-type": env['CONTENT_TYPE'], 2045 "content-type": env['CONTENT_TYPE'],
1930 form) 2060 form)
1931 2061
1932 print(results) 2062 print(results)
1933 self.assertEqual(self.server.client.response_code, 201) 2063 self.assertEqual(self.server.client.response_code, 201)
1934 json_dict = json.loads(b2s(results)) 2064 json_dict = json.loads(b2s(results))
1935 # This is one more than above. Will need to be fixed 2065 self.assertEqual(json_dict['data']['id'], "2")
1936 # When error above is fixed.
1937 self.assertEqual(json_dict['data']['id'], "3")
1938 2066
1939 # test 4 accept is random junk. 2067 # test 4 accept is random junk.
1940 headers={"accept": "Xyzzy I am not a mime, type;", 2068 headers={"accept": "Xyzzy I am not a mime, type;",
1941 "content-type": env['CONTENT_TYPE'], 2069 "content-type": env['CONTENT_TYPE'],
1942 "content-length": env['CONTENT_LENGTH'], 2070 "content-length": env['CONTENT_LENGTH'],
1954 results = self.server.dispatch(env["REQUEST_METHOD"], 2082 results = self.server.dispatch(env["REQUEST_METHOD"],
1955 "/rest/data/issue", 2083 "/rest/data/issue",
1956 form) 2084 form)
1957 2085
1958 print(results) 2086 print(results)
1959 self.assertEqual(self.server.client.response_code, 406) 2087 self.assertEqual(self.server.client.response_code, 400)
1960 json_dict = json.loads(b2s(results)) 2088 json_dict = json.loads(b2s(results))
1961 self.assertIn('Unable to parse Accept Header. Invalid media type: Xyzzy I am not a mime. Acceptable types: */* application/json', json_dict['error']['msg']) 2089 self.assertIn('Unable to parse Accept Header. Invalid media type: Xyzzy I am not a mime. Acceptable types: */* application/json', json_dict['error']['msg'])
1962 2090
1963 # test 5 accept mimetype is ok, param is not 2091 # test 5 accept mimetype is ok, param is not
1964 headers={"accept": "*/*; foo", 2092 headers={"accept": "*/*; foo",
1978 results = self.server.dispatch(env["REQUEST_METHOD"], 2106 results = self.server.dispatch(env["REQUEST_METHOD"],
1979 "/rest/data/issue", 2107 "/rest/data/issue",
1980 form) 2108 form)
1981 2109
1982 print(results) 2110 print(results)
1983 self.assertEqual(self.server.client.response_code, 406) 2111 self.assertEqual(self.server.client.response_code, 400)
1984 json_dict = json.loads(b2s(results)) 2112 json_dict = json.loads(b2s(results))
1985 self.assertIn('Unable to parse Accept Header. Invalid param: foo. Acceptable types: */* application/json', json_dict['error']['msg']) 2113 self.assertIn('Unable to parse Accept Header. Invalid param: foo. Acceptable types: */* application/json', json_dict['error']['msg'])
1986 2114
1987 def testStatsGen(self): 2115 def testStatsGen(self):
1988 # check stats being returned by put and get ops 2116 # check stats being returned by put and get ops
2145 environ=env) 2273 environ=env)
2146 self.server.client.request.headers.get=self.get_header 2274 self.server.client.request.headers.get=self.get_header
2147 results = self.server.dispatch('PUT', 2275 results = self.server.dispatch('PUT',
2148 "/rest/data/user/%s/realname"%self.joeid, 2276 "/rest/data/user/%s/realname"%self.joeid,
2149 form) 2277 form)
2150 self.assertEqual(self.server.client.response_code, 400) 2278 self.assertEqual(self.server.client.response_code, 406)
2151 del(self.headers) 2279 del(self.headers)
2152 2280
2153 # TEST #2 2281 # TEST #2
2154 # Set joe's 'realname' using json data. 2282 # Set joe's 'realname' using json data.
2155 # simulate: /rest/data/user/<id>/realname 2283 # simulate: /rest/data/user/<id>/realname
2425 form) 2553 form)
2426 self.assertEqual(self.server.client.response_code, 406) 2554 self.assertEqual(self.server.client.response_code, 406)
2427 print(results) 2555 print(results)
2428 try: # only verify local copy not system installed copy 2556 try: # only verify local copy not system installed copy
2429 from roundup.dicttoxml import dicttoxml 2557 from roundup.dicttoxml import dicttoxml
2430 includexml = ', application/xml' 2558 includexml = ', xml'
2431 except ImportError: 2559 except ImportError:
2432 includexml = '' 2560 includexml = ''
2433 2561
2434 response="Requested content type 'jon' is not available.\n" \ 2562 json_dict = json.loads(b2s(results))
2435 "Acceptable types: */*, application/json%s\n"%includexml 2563 response= ("Content type 'jon' requested in URL is not available.\n"
2436 self.assertEqual(b2s(results), response) 2564 "Acceptable types: json%s\n") % includexml
2565 self.assertEqual(json_dict['error']['msg'], response)
2437 2566
2438 # TEST #9 2567 # TEST #9
2439 # GET: test that version can be set with accept: 2568 # GET: test that version can be set with accept:
2440 # ... ; version=z 2569 # ... ; version=z
2441 # or 2570 # or
2455 self.server.client.request.headers.get=self.get_header 2584 self.server.client.request.headers.get=self.get_header
2456 results = self.server.dispatch('GET', 2585 results = self.server.dispatch('GET',
2457 "/rest/data/issue/1", form) 2586 "/rest/data/issue/1", form)
2458 print("9a: " + b2s(results)) 2587 print("9a: " + b2s(results))
2459 json_dict = json.loads(b2s(results)) 2588 json_dict = json.loads(b2s(results))
2460 self.assertEqual(json_dict['error']['status'], 400) 2589 # note bad @apiver returns 400 not 406.
2590 self.assertEqual(json_dict['error']['status'], 406)
2461 self.assertEqual(json_dict['error']['msg'], 2591 self.assertEqual(json_dict['error']['msg'],
2462 "Unrecognized api version: L. See /rest without " 2592 "Unrecognized api version: L. See /rest without "
2463 "specifying api version for supported versions.") 2593 "specifying api version for supported versions.")
2464 2594
2465 headers={"accept": "application/json; version=z" } 2595 headers={"accept": "application/json; version=z" }
2467 self.server.client.request.headers.get=self.get_header 2597 self.server.client.request.headers.get=self.get_header
2468 results = self.server.dispatch('GET', 2598 results = self.server.dispatch('GET',
2469 "/rest/data/issue/1", form) 2599 "/rest/data/issue/1", form)
2470 print("9b: " + b2s(results)) 2600 print("9b: " + b2s(results))
2471 json_dict = json.loads(b2s(results)) 2601 json_dict = json.loads(b2s(results))
2472 self.assertEqual(json_dict['error']['status'], 400) 2602 self.assertEqual(self.server.client.response_code, 406)
2603 self.assertEqual(json_dict['error']['status'], 406)
2473 self.assertEqual(json_dict['error']['msg'], 2604 self.assertEqual(json_dict['error']['msg'],
2474 "Unrecognized api version: z. See /rest without " 2605 "Unrecognized api version: z. See /rest without "
2475 "specifying api version for supported versions.") 2606 "specifying api version for supported versions.")
2476 2607
2477 headers={"accept": "application/vnd.roundup.test-vz+json" } 2608 headers={"accept": "application/vnd.roundup.test-vz+json" }
2478 self.headers=headers 2609 self.headers=headers
2479 self.server.client.request.headers.get=self.get_header 2610 self.server.client.request.headers.get=self.get_header
2480 results = self.server.dispatch('GET', 2611 results = self.server.dispatch('GET',
2481 "/rest/data/issue/1", self.empty_form) 2612 "/rest/data/issue/1", self.empty_form)
2482 print("9c:" + b2s(results)) 2613 print("9c:" + b2s(results))
2483 self.assertEqual(self.server.client.response_code, 400) 2614 self.assertEqual(self.server.client.response_code, 406)
2484 json_dict = json.loads(b2s(results)) 2615 json_dict = json.loads(b2s(results))
2485 self.assertEqual(json_dict['error']['status'], 400) 2616 self.assertEqual(json_dict['error']['status'], 406)
2486 self.assertEqual(json_dict['error']['msg'], 2617 self.assertEqual(json_dict['error']['msg'],
2487 "Unrecognized api version: z. See /rest without " 2618 "Unrecognized api version: z. See /rest without "
2488 "specifying api version for supported versions.") 2619 "specifying api version for supported versions.")
2489 2620
2490 # verify that version priority is correct; should be version=... 2621 # verify that version priority is correct; should be version=...
2493 self.headers=headers 2624 self.headers=headers
2494 self.server.client.request.headers.get=self.get_header 2625 self.server.client.request.headers.get=self.get_header
2495 results = self.server.dispatch('GET', 2626 results = self.server.dispatch('GET',
2496 "/rest/data/issue/1", self.empty_form) 2627 "/rest/data/issue/1", self.empty_form)
2497 print("9d: " + b2s(results)) 2628 print("9d: " + b2s(results))
2498 self.assertEqual(self.server.client.response_code, 400) 2629 self.assertEqual(self.server.client.response_code, 406)
2499 json_dict = json.loads(b2s(results)) 2630 json_dict = json.loads(b2s(results))
2500 self.assertEqual(json_dict['error']['status'], 400) 2631 self.assertEqual(json_dict['error']['status'], 406)
2501 self.assertEqual(json_dict['error']['msg'], 2632 self.assertEqual(json_dict['error']['msg'],
2502 "Unrecognized api version: a. See /rest without " 2633 "Unrecognized api version: a. See /rest without "
2503 "specifying api version for supported versions.") 2634 "specifying api version for supported versions.")
2504 2635
2505 # TEST #10 2636 # TEST #10
2726 results = self.server.dispatch('GET', 2857 results = self.server.dispatch('GET',
2727 "/rest/data/status/1", 2858 "/rest/data/status/1",
2728 self.empty_form) 2859 self.empty_form)
2729 print(results) 2860 print(results)
2730 json_dict = json.loads(b2s(results)) 2861 json_dict = json.loads(b2s(results))
2731 self.assertEqual(self.server.client.response_code, 400) 2862 self.assertEqual(self.server.client.response_code, 406)
2732 self.assertEqual(self.server.client.additional_headers['Content-Type'], 2863 self.assertEqual(self.server.client.additional_headers['Content-Type'],
2733 "application/json") 2864 "application/json")
2734 self.assertEqual(json_dict['error']['msg'], 2865 self.assertEqual(json_dict['error']['msg'],
2735 "Unrecognized api version: 99. See /rest " 2866 "Unrecognized api version: 99. See /rest "
2736 "without specifying api version for " 2867 "without specifying api version for "

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