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)

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