comparison test/test_liveserver.py @ 6525:c505c774a94d

Mutiple changes to REST code. Requesting an invalid attribut via rest/data/class/id/attrib used to return a 405, it now returns a 400 and a better error message. /rest/ response scans the registered endpoints rather than using a hard coded description. So new endpoints added in interfaces.py are listed. Fix a number of Allow headers that were listing invalid methods. Also when invalid method is used, report valid methods in response. Extract methods from Route list. Fix Access-Control-Allow-Methods. Add X-Requested-With to Access-Control-Allow-Headers. Add decorator openapi_doc to add openapi annotations for the rest endpoints. Added a couple of examples. Returning this info to a client is still a work in progress.
author John Rouillard <rouilj@ieee.org>
date Sun, 07 Nov 2021 01:04:43 -0500
parents 1fc765ef6379
children 3c8322e3fe25
comparison
equal deleted inserted replaced
6524:f961dbbc3573 6525:c505c774a94d
93 self.assertEqual(f.status_code, 200) 93 self.assertEqual(f.status_code, 200)
94 self.assertTrue(b'Roundup' in f.content) 94 self.assertTrue(b'Roundup' in f.content)
95 self.assertTrue(b'Creator' in f.content) 95 self.assertTrue(b'Creator' in f.content)
96 96
97 97
98 def test_rest_invalid_method_collection(self):
99 # use basic auth for rest endpoint
100 f = requests.put(self.url_base() + '/rest/data/user',
101 auth=('admin', 'sekrit'),
102 headers = {'content-type': "",
103 'x-requested-with': "rest"})
104 import pdb; pdb.set_trace()
105 print(f.status_code)
106 print(f.headers)
107 print(f.content)
108
109 self.assertEqual(f.status_code, 405)
110 expected = { 'Access-Control-Allow-Origin': '*',
111 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
112 'Allow': 'DELETE, GET, OPTIONS, POST',
113 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
114 }
115
116 print(f.headers)
117 # use dict comprehension to remove fields like date,
118 # content-length etc. from f.headers.
119 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
120
121 content = json.loads(f.content)
122
123 exp_content = "Method PUT not allowed. Allowed: DELETE, GET, OPTIONS, POST"
124 self.assertEqual(exp_content, content['error']['msg'])
125
98 def test_http_options(self): 126 def test_http_options(self):
99 """ options returns an unimplemented error for this case.""" 127 """ options returns an unimplemented error for this case."""
100 128
101 # do not send content-type header for options 129 # do not send content-type header for options
102 f = requests.options(self.url_base() + '/', 130 f = requests.options(self.url_base() + '/',
112 print(f.status_code) 140 print(f.status_code)
113 print(f.headers) 141 print(f.headers)
114 142
115 self.assertEqual(f.status_code, 204) 143 self.assertEqual(f.status_code, 204)
116 expected = { 'Access-Control-Allow-Origin': '*', 144 expected = { 'Access-Control-Allow-Origin': '*',
117 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 145 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
118 'Allow': 'OPTIONS, GET', 146 'Allow': 'OPTIONS, GET',
119 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 147 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
120 } 148 }
121 149
122 # use dict comprehension to remove fields like date, 150 # use dict comprehension to remove fields like date,
123 # content-length etc. from f.headers. 151 # content-length etc. from f.headers.
124 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 152 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
132 print(f.status_code) 160 print(f.status_code)
133 print(f.headers) 161 print(f.headers)
134 162
135 self.assertEqual(f.status_code, 204) 163 self.assertEqual(f.status_code, 204)
136 expected = { 'Access-Control-Allow-Origin': '*', 164 expected = { 'Access-Control-Allow-Origin': '*',
137 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 165 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
138 'Allow': 'OPTIONS, GET', 166 'Allow': 'OPTIONS, GET',
139 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 167 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
140 } 168 }
141 169
142 # use dict comprehension to remove fields like date, 170 # use dict comprehension to remove fields like date,
143 # content-length etc. from f.headers. 171 # content-length etc. from f.headers.
144 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 172 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
151 print(f.status_code) 179 print(f.status_code)
152 print(f.headers) 180 print(f.headers)
153 181
154 self.assertEqual(f.status_code, 204) 182 self.assertEqual(f.status_code, 204)
155 expected = { 'Access-Control-Allow-Origin': '*', 183 expected = { 'Access-Control-Allow-Origin': '*',
156 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 184 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
157 'Allow': 'OPTIONS, GET, POST', 185 'Allow': 'OPTIONS, GET, POST',
158 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 186 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
159 } 187 }
160 188
161 # use dict comprehension to remove fields like date, 189 # use dict comprehension to remove fields like date,
162 # content-length etc. from f.headers. 190 # content-length etc. from f.headers.
163 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 191 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
171 print(f.status_code) 199 print(f.status_code)
172 print(f.headers) 200 print(f.headers)
173 201
174 self.assertEqual(f.status_code, 204) 202 self.assertEqual(f.status_code, 204)
175 expected = { 'Access-Control-Allow-Origin': '*', 203 expected = { 'Access-Control-Allow-Origin': '*',
176 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 204 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
177 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH', 205 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
178 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 206 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
179 } 207 }
180 208
181 # use dict comprehension to remove fields like date, 209 # use dict comprehension to remove fields like date,
182 # content-length etc. from f.headers. 210 # content-length etc. from f.headers.
183 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 211 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
190 print(f.status_code) 218 print(f.status_code)
191 print(f.headers) 219 print(f.headers)
192 220
193 self.assertEqual(f.status_code, 204) 221 self.assertEqual(f.status_code, 204)
194 expected = { 'Access-Control-Allow-Origin': '*', 222 expected = { 'Access-Control-Allow-Origin': '*',
195 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 223 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
196 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH', 224 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
197 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 225 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
198 } 226 }
199 227
200 # use dict comprehension to remove fields like date, 228 # use dict comprehension to remove fields like date,
201 # content-length etc. from f.headers. 229 # content-length etc. from f.headers.
202 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 230 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
380 print(f.headers) 408 print(f.headers)
381 409
382 self.assertEqual(f.status_code, 200) 410 self.assertEqual(f.status_code, 200)
383 expected = { 'Content-Type': 'application/json', 411 expected = { 'Content-Type': 'application/json',
384 'Access-Control-Allow-Origin': '*', 412 'Access-Control-Allow-Origin': '*',
385 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 413 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
386 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 414 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
387 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 415 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
388 'Content-Encoding': 'gzip', 416 'Content-Encoding': 'gzip',
389 'Vary': 'Accept-Encoding', 417 'Vary': 'Accept-Encoding',
390 } 418 }
391 419
392 content_str = '''{ "data": { 420 content_str = '''{ "data": {
425 'Accept-Encoding': 'gzip, foo', 453 'Accept-Encoding': 'gzip, foo',
426 'Accept': '*/*'}) 454 'Accept': '*/*'})
427 print(f.status_code) 455 print(f.status_code)
428 print(f.headers) 456 print(f.headers)
429 457
430 # ERROR: attribute error turns into 405, not sure that's right.
431 # NOTE: not compressed payload too small 458 # NOTE: not compressed payload too small
432 self.assertEqual(f.status_code, 405) 459 self.assertEqual(f.status_code, 400)
433 expected = { 'Content-Type': 'application/json', 460 expected = { 'Content-Type': 'application/json',
434 'Access-Control-Allow-Origin': '*', 461 'Access-Control-Allow-Origin': '*',
435 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 462 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
436 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 463 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
437 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 464 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
438 } 465 }
439 466
440 content = { "error": 467 content = { "error":
441 { 468 {
442 "status": 405, 469 "status": 400,
443 "msg": "'foo'" 470 "msg": "Invalid attribute foo"
444 } 471 }
445 } 472 }
446 473
447 json_dict = json.loads(b2s(f.content)) 474 json_dict = json.loads(b2s(f.content))
448 self.assertDictEqual(json_dict, content) 475 self.assertDictEqual(json_dict, content)
507 print(f.headers) 534 print(f.headers)
508 535
509 self.assertEqual(f.status_code, 200) 536 self.assertEqual(f.status_code, 200)
510 expected = { 'Content-Type': 'application/json', 537 expected = { 'Content-Type': 'application/json',
511 'Access-Control-Allow-Origin': '*', 538 'Access-Control-Allow-Origin': '*',
512 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 539 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
513 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 540 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
514 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 541 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
515 'Content-Encoding': 'br', 542 'Content-Encoding': 'br',
516 'Vary': 'Accept-Encoding', 543 'Vary': 'Accept-Encoding',
517 } 544 }
518 545
519 content_str = '''{ "data": { 546 content_str = '''{ "data": {
553 auth=('admin', 'sekrit'), 580 auth=('admin', 'sekrit'),
554 headers = {'Accept-Encoding': 'br, foo', 581 headers = {'Accept-Encoding': 'br, foo',
555 'Accept': '*/*'}) 582 'Accept': '*/*'})
556 print(f.status_code) 583 print(f.status_code)
557 print(f.headers) 584 print(f.headers)
558 # ERROR: attribute error turns into 405, not sure that's right. 585
559 # Note: not compressed payload too small 586 # Note: not compressed payload too small
560 self.assertEqual(f.status_code, 405) 587 self.assertEqual(f.status_code, 400)
561 expected = { 'Content-Type': 'application/json', 588 expected = { 'Content-Type': 'application/json',
562 'Access-Control-Allow-Origin': '*', 589 'Access-Control-Allow-Origin': '*',
563 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 590 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
564 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 591 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
565 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 592 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
566 } 593 }
567 594
568 content = { "error": 595 content = { "error":
569 { 596 {
570 "status": 405, 597 "status": 400,
571 "msg": "'foo'" 598 "msg": "Invalid attribute foo"
572 } 599 }
573 } 600 }
574 json_dict = json.loads(b2s(f.content)) 601 json_dict = json.loads(b2s(f.content))
575 602
576 self.assertDictEqual(json_dict, content) 603 self.assertDictEqual(json_dict, content)
653 print(f.headers) 680 print(f.headers)
654 681
655 self.assertEqual(f.status_code, 200) 682 self.assertEqual(f.status_code, 200)
656 expected = { 'Content-Type': 'application/json', 683 expected = { 'Content-Type': 'application/json',
657 'Access-Control-Allow-Origin': '*', 684 'Access-Control-Allow-Origin': '*',
658 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 685 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
659 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 686 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
660 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 687 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
661 'Content-Encoding': 'zstd', 688 'Content-Encoding': 'zstd',
662 'Vary': 'Accept-Encoding', 689 'Vary': 'Accept-Encoding',
663 } 690 }
664 691
665 content_str = '''{ "data": { 692 content_str = '''{ "data": {
701 'Accept-Encoding': 'zstd, foo', 728 'Accept-Encoding': 'zstd, foo',
702 'Accept': '*/*'}) 729 'Accept': '*/*'})
703 print(f.status_code) 730 print(f.status_code)
704 print(f.headers) 731 print(f.headers)
705 732
706 # ERROR: attribute error turns into 405, not sure that's right.
707 # Note: not compressed, payload too small 733 # Note: not compressed, payload too small
708 self.assertEqual(f.status_code, 405) 734 self.assertEqual(f.status_code, 400)
709 expected = { 'Content-Type': 'application/json', 735 expected = { 'Content-Type': 'application/json',
710 'Access-Control-Allow-Origin': '*', 736 'Access-Control-Allow-Origin': '*',
711 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-HTTP-Method-Override', 737 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
712 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 738 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
713 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, PUT, DELETE, PATCH', 739 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
714 } 740 }
715 741
716 content = { "error": 742 content = { "error":
717 { 743 {
718 "status": 405, 744 "status": 400,
719 "msg": "'foo'" 745 "msg": "Invalid attribute foo"
720 } 746 }
721 } 747 }
722 748
723 json_dict = json.loads(b2s(f.content)) 749 json_dict = json.loads(b2s(f.content))
724 self.assertDictEqual(json_dict, content) 750 self.assertDictEqual(json_dict, content)

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