Mercurial > p > roundup > code
comparison test/rest_common.py @ 5643:a60cbbcc9309
Added support for accepting application/json payload in addition to
the existing application/x-www-form-urlencoded.
The key for this is that the third element of the FieldStorage is a
string as opposed to a list. So the code checks for the string and
that the Content-Type is exactly application/json. I do a string match
for the Content-Type.
This code also adds testing for the dispatch method of
RestfulInstance. It tests dispatch using GET, PUT, POST, PATCH
methods with json and form data payloads. Existing tests bypass the
dispatch method.
It moves check for pretty printing till after the input payload is
checked to see if it's json. So you can set pretty in the json payload
if wanted.
Adds a new class: SimulateFieldStorageFromJson. This class
emulates the calling interface of FieldStorage. The json payload
is parsed into this class. Then the new object is passed off to the
code that expects a FieldStorage class. Note that this may or may not
work for file uploads, but for issue creation, setting properties,
patching objects, it seems to work.
Also refactored/replaced the etag header checks to use a more generic
method that will work for any header (e.g. Content-Type).
Future enhancements are to parse the full form of the Content-Type
mime type so something like: application/vnd.roundup.v1+json will also
work. Also the SimulateFieldStorageFromJson could be used to represent
XML format input, if so need to rename the class dropping
FromJson. But because of the issues with native xml parsers in python
parsing untrusted data, we may not want to go that route.
curl examples for my tracker is:
curl -s -u user:pass -X POST --header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--data '{"title": "foo bar", "fyi": "text", "private": "true", "priority": "high" }' \
-w "http status: %{http_code}\n" \
"https://example.net/demo/rest/data/issue"
{
"data": {
"link": "https://example.net/demo/rest/data/issue/2229",
"id": "2229"
}
}
http status: 201
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 10 Mar 2019 17:35:25 -0400 |
| parents | f576957cbb1f |
| children | 7f4d19867123 |
comparison
equal
deleted
inserted
replaced
| 5642:bd681700e556 | 5643:a60cbbcc9309 |
|---|---|
| 11 import random | 11 import random |
| 12 | 12 |
| 13 from .db_test_base import setupTracker | 13 from .db_test_base import setupTracker |
| 14 | 14 |
| 15 from .mocknull import MockNull | 15 from .mocknull import MockNull |
| 16 | |
| 17 from roundup.anypy.strings import StringIO | |
| 18 import json | |
| 16 | 19 |
| 17 NEEDS_INSTANCE = 1 | 20 NEEDS_INSTANCE = 1 |
| 18 | 21 |
| 19 | 22 |
| 20 class TestCase(): | 23 class TestCase(): |
| 63 'PATH_INFO': 'http://localhost/rounduptest/rest/', | 66 'PATH_INFO': 'http://localhost/rounduptest/rest/', |
| 64 'HTTP_HOST': 'localhost', | 67 'HTTP_HOST': 'localhost', |
| 65 'TRACKER_NAME': 'rounduptest' | 68 'TRACKER_NAME': 'rounduptest' |
| 66 } | 69 } |
| 67 self.dummy_client = client.Client(self.instance, MockNull(), env, [], None) | 70 self.dummy_client = client.Client(self.instance, MockNull(), env, [], None) |
| 68 self.dummy_client.request.headers.getheader = self.get_etag_header | 71 self.dummy_client.request.headers.getheader = self.get_header |
| 69 self.empty_form = cgi.FieldStorage() | 72 self.empty_form = cgi.FieldStorage() |
| 70 | 73 |
| 71 self.server = RestfulInstance(self.dummy_client, self.db) | 74 self.server = RestfulInstance(self.dummy_client, self.db) |
| 72 | 75 |
| 73 def tearDown(self): | 76 def tearDown(self): |
| 76 shutil.rmtree(self.dirname) | 79 shutil.rmtree(self.dirname) |
| 77 except OSError as error: | 80 except OSError as error: |
| 78 if error.errno not in (errno.ENOENT, errno.ESRCH): | 81 if error.errno not in (errno.ENOENT, errno.ESRCH): |
| 79 raise | 82 raise |
| 80 | 83 |
| 81 def get_etag_header (self, header, not_found=None): | 84 def get_header (self, header, not_found=None): |
| 82 try: | 85 try: |
| 83 return self.etag_header | 86 return self.headers[header.lower()] |
| 84 except AttributeError: | 87 except (AttributeError, KeyError): |
| 85 return None | 88 return not_found |
| 86 | 89 |
| 87 def testGet(self): | 90 def testGet(self): |
| 88 """ | 91 """ |
| 89 Retrieve all three users | 92 Retrieve all three users |
| 90 obtain data for 'joe' | 93 obtain data for 'joe' |
| 352 ''' | 355 ''' |
| 353 for mode in ('header', 'etag', 'both', | 356 for mode in ('header', 'etag', 'both', |
| 354 'brokenheader', 'brokenetag', 'none'): | 357 'brokenheader', 'brokenetag', 'none'): |
| 355 try: | 358 try: |
| 356 # clean up any old header | 359 # clean up any old header |
| 357 del(self.etag_header) | 360 del(self.headers) |
| 358 except AttributeError: | 361 except AttributeError: |
| 359 pass | 362 pass |
| 360 | 363 |
| 361 form = cgi.FieldStorage() | 364 form = cgi.FieldStorage() |
| 362 etag = calculate_etag(self.db.user.getnode(self.joeid)) | 365 etag = calculate_etag(self.db.user.getnode(self.joeid)) |
| 364 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), | 367 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), |
| 365 ] | 368 ] |
| 366 | 369 |
| 367 if mode == 'header': | 370 if mode == 'header': |
| 368 print "Mode = %s"%mode | 371 print "Mode = %s"%mode |
| 369 self.etag_header = etag | 372 self.headers = {'etag': etag} |
| 370 elif mode == 'etag': | 373 elif mode == 'etag': |
| 371 print "Mode = %s"%mode | 374 print "Mode = %s"%mode |
| 372 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | 375 form.list.append(cgi.MiniFieldStorage('@etag', etag)) |
| 373 elif mode == 'both': | 376 elif mode == 'both': |
| 374 print "Mode = %s"%mode | 377 print "Mode = %s"%mode |
| 375 self.etag_header = etag | 378 self.headers = {'etag': etag} |
| 376 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | 379 form.list.append(cgi.MiniFieldStorage('@etag', etag)) |
| 377 elif mode == 'brokenheader': | 380 elif mode == 'brokenheader': |
| 378 print "Mode = %s"%mode | 381 print "Mode = %s"%mode |
| 379 self.etag_header = 'bad' | 382 self.headers = {'etag': 'bad'} |
| 380 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | 383 form.list.append(cgi.MiniFieldStorage('@etag', etag)) |
| 381 elif mode == 'brokenetag': | 384 elif mode == 'brokenetag': |
| 382 print "Mode = %s"%mode | 385 print "Mode = %s"%mode |
| 383 self.etag_header = etag | 386 self.headers = {'etag': etag} |
| 384 form.list.append(cgi.MiniFieldStorage('@etag', 'bad')) | 387 form.list.append(cgi.MiniFieldStorage('@etag', 'bad')) |
| 385 elif mode == 'none': | 388 elif mode == 'none': |
| 386 print "Mode = %s"%mode | 389 print "Mode = %s"%mode |
| 387 else: | 390 else: |
| 388 self.fail("unknown mode found") | 391 self.fail("unknown mode found") |
| 393 if mode not in ('brokenheader', 'brokenetag', 'none'): | 396 if mode not in ('brokenheader', 'brokenetag', 'none'): |
| 394 self.assertEqual(self.dummy_client.response_code, 200) | 397 self.assertEqual(self.dummy_client.response_code, 200) |
| 395 else: | 398 else: |
| 396 self.assertEqual(self.dummy_client.response_code, 412) | 399 self.assertEqual(self.dummy_client.response_code, 412) |
| 397 | 400 |
| 401 def testDispatch(self): | |
| 402 """ | |
| 403 run changes through rest dispatch(). This also tests | |
| 404 sending json payload through code as dispatch is the | |
| 405 code that changes json payload into something we can | |
| 406 process. | |
| 407 """ | |
| 408 # Set joe's 'realname' using json data. | |
| 409 # simulate: /rest/data/user/<id>/realname | |
| 410 # use etag in header | |
| 411 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 412 body='{ "data": "Joe Doe 1" }' | |
| 413 env = { "CONTENT_TYPE": "application/json", | |
| 414 "CONTENT_LENGTH": len(body), | |
| 415 "REQUEST_METHOD": "PUT" | |
| 416 } | |
| 417 headers={"accept": "application/json", | |
| 418 "content-type": env['CONTENT_TYPE'], | |
| 419 "etag": etag | |
| 420 } | |
| 421 self.headers=headers | |
| 422 # we need to generate a FieldStorage the looks like | |
| 423 # FieldStorage(None, None, 'string') rather than | |
| 424 # FieldStorage(None, None, []) | |
| 425 body_file=StringIO(body) # FieldStorage needs a file | |
| 426 form = cgi.FieldStorage(body_file, | |
| 427 headers=headers, | |
| 428 environ=env) | |
| 429 self.server.client.request.headers.getheader=self.get_header | |
| 430 results = self.server.dispatch('PUT', | |
| 431 "/rest/data/user/%s/realname"%self.joeid, | |
| 432 form) | |
| 433 | |
| 434 self.assertEqual(self.server.client.response_code, 200) | |
| 435 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 436 self.assertEqual(self.dummy_client.response_code, 200) | |
| 437 self.assertEqual(results['data']['attributes']['realname'], | |
| 438 'Joe Doe 1') | |
| 439 del(self.headers) | |
| 440 | |
| 441 # Set joe's 'realname' using json data. | |
| 442 # simulate: /rest/data/user/<id>/realname | |
| 443 # use etag in payload | |
| 444 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 445 body='{ "@etag": "%s", "data": "Joe Doe 2" }'%etag | |
| 446 env = { "CONTENT_TYPE": "application/json", | |
| 447 "CONTENT_LENGTH": len(body), | |
| 448 "REQUEST_METHOD": "PUT" | |
| 449 } | |
| 450 headers={"accept": "application/json", | |
| 451 "content-type": env['CONTENT_TYPE'] | |
| 452 } | |
| 453 self.headers=headers | |
| 454 body_file=StringIO(body) # FieldStorage needs a file | |
| 455 form = cgi.FieldStorage(body_file, | |
| 456 headers=headers, | |
| 457 environ=env) | |
| 458 self.server.client.request.headers.getheader=self.get_header | |
| 459 results = self.server.dispatch('PUT', | |
| 460 "/rest/data/user/%s/realname"%self.joeid, | |
| 461 form) | |
| 462 | |
| 463 self.assertEqual(self.server.client.response_code, 200) | |
| 464 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 465 self.assertEqual(self.dummy_client.response_code, 200) | |
| 466 self.assertEqual(results['data']['attributes']['realname'], | |
| 467 'Joe Doe 2') | |
| 468 del(self.headers) | |
| 469 | |
| 470 # change Joe's realname via a normal web form | |
| 471 # This generates a FieldStorage that looks like: | |
| 472 # FieldStorage(None, None, []) | |
| 473 # use etag from header | |
| 474 # | |
| 475 # also use a GET on the uri via the dispatch to get | |
| 476 # the results from the db. | |
| 477 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 478 headers={"etag": etag, | |
| 479 "accept": "application/json", | |
| 480 } | |
| 481 form = cgi.FieldStorage() | |
| 482 form.list = [ | |
| 483 cgi.MiniFieldStorage('data', 'Joe Doe'), | |
| 484 ] | |
| 485 self.headers = headers | |
| 486 self.server.client.request.headers.getheader = self.get_header | |
| 487 results = self.server.dispatch('PUT', | |
| 488 "/rest/data/user/%s/realname"%self.joeid, | |
| 489 form) | |
| 490 self.assertEqual(self.dummy_client.response_code, 200) | |
| 491 results = self.server.dispatch('GET', | |
| 492 "/rest/data/user/%s/realname"%self.joeid, | |
| 493 self.empty_form) | |
| 494 self.assertEqual(self.dummy_client.response_code, 200) | |
| 495 json_dict = json.loads(results) | |
| 496 | |
| 497 self.assertEqual(json_dict['data']['data'], 'Joe Doe') | |
| 498 self.assertEqual(json_dict['data']['link'], | |
| 499 "http://tracker.example/cgi-bin/" | |
| 500 "roundup.cgi/bugs/rest/data/user/3/realname") | |
| 501 self.assertEqual(json_dict['data']['type'], "<type 'str'>") | |
| 502 self.assertEqual(json_dict['data']["id"], "3") | |
| 503 del(self.headers) | |
| 504 | |
| 505 | |
| 506 # PATCH joe's email address with json | |
| 507 # save address so we can use it later | |
| 508 stored_results = self.server.get_element('user', self.joeid, | |
| 509 self.empty_form) | |
| 510 self.assertEqual(self.dummy_client.response_code, 200) | |
| 511 | |
| 512 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 513 body='{ "address": "demo2@example.com", "@etag": "%s"}'%etag | |
| 514 env = { "CONTENT_TYPE": "application/json", | |
| 515 "CONTENT_LENGTH": len(body), | |
| 516 "REQUEST_METHOD": "PATCH" | |
| 517 } | |
| 518 headers={"accept": "application/json", | |
| 519 "content-type": env['CONTENT_TYPE'] | |
| 520 } | |
| 521 self.headers=headers | |
| 522 body_file=StringIO(body) # FieldStorage needs a file | |
| 523 form = cgi.FieldStorage(body_file, | |
| 524 headers=headers, | |
| 525 environ=env) | |
| 526 self.server.client.request.headers.getheader=self.get_header | |
| 527 results = self.server.dispatch('PATCH', | |
| 528 "/rest/data/user/%s"%self.joeid, | |
| 529 form) | |
| 530 | |
| 531 self.assertEqual(self.server.client.response_code, 200) | |
| 532 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 533 self.assertEqual(self.dummy_client.response_code, 200) | |
| 534 self.assertEqual(results['data']['attributes']['address'], | |
| 535 'demo2@example.com') | |
| 536 | |
| 537 # and set it back | |
| 538 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 539 body='{ "address": "%s", "@etag": "%s"}'%( | |
| 540 stored_results['data']['attributes']['address'], | |
| 541 etag) | |
| 542 # reuse env and headers from prior test. | |
| 543 body_file=StringIO(body) # FieldStorage needs a file | |
| 544 form = cgi.FieldStorage(body_file, | |
| 545 headers=headers, | |
| 546 environ=env) | |
| 547 self.server.client.request.headers.getheader=self.get_header | |
| 548 results = self.server.dispatch('PATCH', | |
| 549 "/rest/data/user/%s"%self.joeid, | |
| 550 form) | |
| 551 | |
| 552 self.assertEqual(self.server.client.response_code, 200) | |
| 553 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 554 self.assertEqual(self.dummy_client.response_code, 200) | |
| 555 self.assertEqual(results['data']['attributes']['address'], | |
| 556 'random@home.org') | |
| 557 del(self.headers) | |
| 558 | |
| 559 # POST to create new issue | |
| 560 body='{ "title": "foo bar", "priority": "critical" }' | |
| 561 | |
| 562 env = { "CONTENT_TYPE": "application/json", | |
| 563 "CONTENT_LENGTH": len(body), | |
| 564 "REQUEST_METHOD": "POST" | |
| 565 } | |
| 566 headers={"accept": "application/json", | |
| 567 "content-type": env['CONTENT_TYPE'] | |
| 568 } | |
| 569 self.headers=headers | |
| 570 body_file=StringIO(body) # FieldStorage needs a file | |
| 571 form = cgi.FieldStorage(body_file, | |
| 572 headers=headers, | |
| 573 environ=env) | |
| 574 print form | |
| 575 self.server.client.request.headers.getheader=self.get_header | |
| 576 results = self.server.dispatch('POST', | |
| 577 "/rest/data/issue", | |
| 578 form) | |
| 579 | |
| 580 self.assertEqual(self.server.client.response_code, 201) | |
| 581 json_dict = json.loads(results) | |
| 582 issue_id=json_dict['data']['id'] | |
| 583 results = self.server.get_element('issue', | |
| 584 str(issue_id), # must be a string not unicode | |
| 585 self.empty_form) | |
| 586 self.assertEqual(self.dummy_client.response_code, 200) | |
| 587 self.assertEqual(results['data']['attributes']['title'], | |
| 588 'foo bar') | |
| 589 del(self.headers) | |
| 398 | 590 |
| 399 def testPut(self): | 591 def testPut(self): |
| 400 """ | 592 """ |
| 401 Change joe's 'realname' | 593 Change joe's 'realname' |
| 402 Check if we can't change admin's detail | 594 Check if we can't change admin's detail |
| 415 'user', self.joeid, 'realname', self.empty_form | 607 'user', self.joeid, 'realname', self.empty_form |
| 416 ) | 608 ) |
| 417 self.assertEqual(self.dummy_client.response_code, 200) | 609 self.assertEqual(self.dummy_client.response_code, 200) |
| 418 self.assertEqual(results['data']['data'], 'Joe Random') | 610 self.assertEqual(results['data']['data'], 'Joe Random') |
| 419 | 611 |
| 420 # change Joe's realname via attribute uri | 612 # change Joe's realname via attribute uri - etag in header |
| 421 form = cgi.FieldStorage() | 613 form = cgi.FieldStorage() |
| 422 etag = calculate_etag(self.db.user.getnode(self.joeid)) | 614 etag = calculate_etag(self.db.user.getnode(self.joeid)) |
| 423 form.list = [ | 615 form.list = [ |
| 424 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), | 616 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), |
| 425 ] | 617 ] |
| 426 | 618 |
| 427 self.etag_header = etag # use etag in header | 619 self.headers = {'etag': etag } # use etag in header |
| 428 results = self.server.put_attribute( | 620 results = self.server.put_attribute( |
| 429 'user', self.joeid, 'realname', form | 621 'user', self.joeid, 'realname', form |
| 430 ) | 622 ) |
| 431 self.assertEqual(self.dummy_client.response_code, 200) | 623 self.assertEqual(self.dummy_client.response_code, 200) |
| 432 results = self.server.get_attribute( | 624 results = self.server.get_attribute( |
| 433 'user', self.joeid, 'realname', self.empty_form | 625 'user', self.joeid, 'realname', self.empty_form |
| 434 ) | 626 ) |
| 435 self.assertEqual(self.dummy_client.response_code, 200) | 627 self.assertEqual(self.dummy_client.response_code, 200) |
| 436 self.assertEqual(results['data']['data'], 'Joe Doe Doe') | 628 self.assertEqual(results['data']['data'], 'Joe Doe Doe') |
| 437 del(self.etag_header) | 629 del(self.headers) |
| 438 | 630 |
| 439 # Reset joe's 'realname'. | 631 # Reset joe's 'realname'. etag in body |
| 440 form = cgi.FieldStorage() | 632 form = cgi.FieldStorage() |
| 441 etag = calculate_etag(self.db.user.getnode(self.joeid)) | 633 etag = calculate_etag(self.db.user.getnode(self.joeid)) |
| 442 form.list = [ | 634 form.list = [ |
| 443 cgi.MiniFieldStorage('realname', 'Joe Doe'), | 635 cgi.MiniFieldStorage('realname', 'Joe Doe'), |
| 444 cgi.MiniFieldStorage('@etag', etag) | 636 cgi.MiniFieldStorage('@etag', etag) |
