Mercurial > p > roundup > code
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 " |
