Mercurial > p > roundup > code
comparison test/rest_common.py @ 5630:07abc8d36940
Add etag support to rest interface to prevent multiple users from
overwriting other users changes.
All GET requests for an object (issue, user, keyword etc.) or a
property of an object (e.g the title of an issue) return the etag for
the object in the ETag header as well as the @etag field in the
returned object.
All requests that change existing objects (DELETE, PUT or PATCH)
require:
1 A request include an ETag header with the etag value retrieved
for the object.
2 A submits a form that includes the field @etag that must have
the value retrieved for the object.
If an etag is not supplied by one of these methods, or any supplied
etag does not match the etag calculated at the time the DELETE, PUT or
PATCH request is made, HTTP error 412 (Precondition Failed) is
returned and no change is made. At that time the client code should
retrieve the object again, reconcile the changes and can try to send a
new update.
The etag is the md5 hash of the representation (repr()) of the object
retrieved from the database.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Fri, 01 Mar 2019 22:57:07 -0500 |
| parents | 1c4adab65faf |
| children | f576957cbb1f |
comparison
equal
deleted
inserted
replaced
| 5624:b3618882f906 | 5630:07abc8d36940 |
|---|---|
| 3 import shutil | 3 import shutil |
| 4 import errno | 4 import errno |
| 5 | 5 |
| 6 from roundup.cgi.exceptions import * | 6 from roundup.cgi.exceptions import * |
| 7 from roundup import password, hyperdb | 7 from roundup import password, hyperdb |
| 8 from roundup.rest import RestfulInstance | 8 from roundup.rest import RestfulInstance, calculate_etag |
| 9 from roundup.backends import list_backends | 9 from roundup.backends import list_backends |
| 10 from roundup.cgi import client | 10 from roundup.cgi import client |
| 11 import random | 11 import random |
| 12 | 12 |
| 13 from .db_test_base import setupTracker | 13 from .db_test_base import setupTracker |
| 14 | |
| 15 from .mocknull import MockNull | |
| 14 | 16 |
| 15 NEEDS_INSTANCE = 1 | 17 NEEDS_INSTANCE = 1 |
| 16 | 18 |
| 17 | 19 |
| 18 class TestCase(): | 20 class TestCase(): |
| 60 env = { | 62 env = { |
| 61 'PATH_INFO': 'http://localhost/rounduptest/rest/', | 63 'PATH_INFO': 'http://localhost/rounduptest/rest/', |
| 62 'HTTP_HOST': 'localhost', | 64 'HTTP_HOST': 'localhost', |
| 63 'TRACKER_NAME': 'rounduptest' | 65 'TRACKER_NAME': 'rounduptest' |
| 64 } | 66 } |
| 65 self.dummy_client = client.Client(self.instance, None, env, [], None) | 67 self.dummy_client = client.Client(self.instance, MockNull(), env, [], None) |
| 68 self.dummy_client.request.headers.getheader = self.get_etag_header | |
| 66 self.empty_form = cgi.FieldStorage() | 69 self.empty_form = cgi.FieldStorage() |
| 67 | 70 |
| 68 self.server = RestfulInstance(self.dummy_client, self.db) | 71 self.server = RestfulInstance(self.dummy_client, self.db) |
| 69 | 72 |
| 70 def tearDown(self): | 73 def tearDown(self): |
| 72 try: | 75 try: |
| 73 shutil.rmtree(self.dirname) | 76 shutil.rmtree(self.dirname) |
| 74 except OSError as error: | 77 except OSError as error: |
| 75 if error.errno not in (errno.ENOENT, errno.ESRCH): | 78 if error.errno not in (errno.ENOENT, errno.ESRCH): |
| 76 raise | 79 raise |
| 80 | |
| 81 def get_etag_header (self, header, not_found=None): | |
| 82 try: | |
| 83 return self.etag_header | |
| 84 except AttributeError: | |
| 85 return None | |
| 77 | 86 |
| 78 def testGet(self): | 87 def testGet(self): |
| 79 """ | 88 """ |
| 80 Retrieve all three users | 89 Retrieve all three users |
| 81 obtain data for 'joe' | 90 obtain data for 'joe' |
| 240 ] | 249 ] |
| 241 results = self.server.get_collection('issue', form) | 250 results = self.server.get_collection('issue', form) |
| 242 self.assertEqual(self.dummy_client.response_code, 200) | 251 self.assertEqual(self.dummy_client.response_code, 200) |
| 243 self.assertEqual(len(results['data']), 0) | 252 self.assertEqual(len(results['data']), 0) |
| 244 | 253 |
| 254 | |
| 255 def testEtagProcessing(self): | |
| 256 ''' | |
| 257 Etags can come from two places: | |
| 258 ETag http header | |
| 259 @etags value posted in the form | |
| 260 | |
| 261 Both will be checked if availble. If either one | |
| 262 fails, the etag check will fail. | |
| 263 | |
| 264 Run over header only, etag in form only, both, | |
| 265 each one broke and no etag. Use the put command | |
| 266 to triger the etag checking code. | |
| 267 ''' | |
| 268 for mode in ('header', 'etag', 'both', | |
| 269 'brokenheader', 'brokenetag', 'none'): | |
| 270 try: | |
| 271 # clean up any old header | |
| 272 del(self.etag_header) | |
| 273 except AttributeError: | |
| 274 pass | |
| 275 | |
| 276 form = cgi.FieldStorage() | |
| 277 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 278 form.list = [ | |
| 279 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), | |
| 280 ] | |
| 281 | |
| 282 if mode == 'header': | |
| 283 print "Mode = %s"%mode | |
| 284 self.etag_header = etag | |
| 285 elif mode == 'etag': | |
| 286 print "Mode = %s"%mode | |
| 287 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 288 elif mode == 'both': | |
| 289 print "Mode = %s"%mode | |
| 290 self.etag_header = etag | |
| 291 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 292 elif mode == 'brokenheader': | |
| 293 print "Mode = %s"%mode | |
| 294 self.etag_header = 'bad' | |
| 295 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 296 elif mode == 'brokenetag': | |
| 297 print "Mode = %s"%mode | |
| 298 self.etag_header = etag | |
| 299 form.list.append(cgi.MiniFieldStorage('@etag', 'bad')) | |
| 300 elif mode == 'none': | |
| 301 print "Mode = %s"%mode | |
| 302 else: | |
| 303 self.fail("unknown mode found") | |
| 304 | |
| 305 results = self.server.put_attribute( | |
| 306 'user', self.joeid, 'realname', form | |
| 307 ) | |
| 308 if mode not in ('brokenheader', 'brokenetag', 'none'): | |
| 309 self.assertEqual(self.dummy_client.response_code, 200) | |
| 310 else: | |
| 311 self.assertEqual(self.dummy_client.response_code, 412) | |
| 312 | |
| 313 | |
| 245 def testPut(self): | 314 def testPut(self): |
| 246 """ | 315 """ |
| 247 Change joe's 'realname' | 316 Change joe's 'realname' |
| 248 Check if we can't change admin's detail | 317 Check if we can't change admin's detail |
| 249 """ | 318 """ |
| 250 # change Joe's realname via attribute uri | 319 # fail to change Joe's realname via attribute uri |
| 320 # no etag | |
| 251 form = cgi.FieldStorage() | 321 form = cgi.FieldStorage() |
| 252 form.list = [ | 322 form.list = [ |
| 253 cgi.MiniFieldStorage('data', 'Joe Doe Doe') | 323 cgi.MiniFieldStorage('data', 'Joe Doe Doe') |
| 254 ] | 324 ] |
| 255 results = self.server.put_attribute( | 325 results = self.server.put_attribute( |
| 256 'user', self.joeid, 'realname', form | 326 'user', self.joeid, 'realname', form |
| 257 ) | 327 ) |
| 328 self.assertEqual(self.dummy_client.response_code, 412) | |
| 258 results = self.server.get_attribute( | 329 results = self.server.get_attribute( |
| 259 'user', self.joeid, 'realname', self.empty_form | 330 'user', self.joeid, 'realname', self.empty_form |
| 260 ) | 331 ) |
| 261 self.assertEqual(self.dummy_client.response_code, 200) | 332 self.assertEqual(self.dummy_client.response_code, 200) |
| 333 self.assertEqual(results['data']['data'], 'Joe Random') | |
| 334 | |
| 335 # change Joe's realname via attribute uri | |
| 336 form = cgi.FieldStorage() | |
| 337 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 338 form.list = [ | |
| 339 cgi.MiniFieldStorage('data', 'Joe Doe Doe'), | |
| 340 ] | |
| 341 | |
| 342 self.etag_header = etag # use etag in header | |
| 343 results = self.server.put_attribute( | |
| 344 'user', self.joeid, 'realname', form | |
| 345 ) | |
| 346 self.assertEqual(self.dummy_client.response_code, 200) | |
| 347 results = self.server.get_attribute( | |
| 348 'user', self.joeid, 'realname', self.empty_form | |
| 349 ) | |
| 350 self.assertEqual(self.dummy_client.response_code, 200) | |
| 262 self.assertEqual(results['data']['data'], 'Joe Doe Doe') | 351 self.assertEqual(results['data']['data'], 'Joe Doe Doe') |
| 352 del(self.etag_header) | |
| 263 | 353 |
| 264 # Reset joe's 'realname'. | 354 # Reset joe's 'realname'. |
| 265 form = cgi.FieldStorage() | 355 form = cgi.FieldStorage() |
| 266 form.list = [ | 356 etag = calculate_etag(self.db.user.getnode(self.joeid)) |
| 267 cgi.MiniFieldStorage('realname', 'Joe Doe') | 357 form.list = [ |
| 358 cgi.MiniFieldStorage('realname', 'Joe Doe'), | |
| 359 cgi.MiniFieldStorage('@etag', etag) | |
| 268 ] | 360 ] |
| 269 results = self.server.put_element('user', self.joeid, form) | 361 results = self.server.put_element('user', self.joeid, form) |
| 362 self.assertEqual(self.dummy_client.response_code, 200) | |
| 270 results = self.server.get_element('user', self.joeid, self.empty_form) | 363 results = self.server.get_element('user', self.joeid, self.empty_form) |
| 271 self.assertEqual(self.dummy_client.response_code, 200) | 364 self.assertEqual(self.dummy_client.response_code, 200) |
| 272 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe') | 365 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe') |
| 273 | 366 |
| 274 # check we can't change admin's details | 367 # check we can't change admin's details |
| 372 Test Delete an attribute | 465 Test Delete an attribute |
| 373 """ | 466 """ |
| 374 # create a new issue with userid 1 in the nosy list | 467 # create a new issue with userid 1 in the nosy list |
| 375 issue_id = self.db.issue.create(title='foo', nosy=['1']) | 468 issue_id = self.db.issue.create(title='foo', nosy=['1']) |
| 376 | 469 |
| 470 # No etag, so this should return 412 - Precondition Failed | |
| 471 # With no changes | |
| 472 results = self.server.delete_attribute( | |
| 473 'issue', issue_id, 'nosy', self.empty_form | |
| 474 ) | |
| 475 self.assertEqual(self.dummy_client.response_code, 412) | |
| 476 results = self.server.get_element('issue', issue_id, self.empty_form) | |
| 477 results = results['data'] | |
| 478 self.assertEqual(self.dummy_client.response_code, 200) | |
| 479 self.assertEqual(len(results['attributes']['nosy']), 1) | |
| 480 self.assertListEqual(results['attributes']['nosy'], ['1']) | |
| 481 | |
| 482 form = cgi.FieldStorage() | |
| 483 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 484 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 377 # remove the title and nosy | 485 # remove the title and nosy |
| 378 results = self.server.delete_attribute( | 486 results = self.server.delete_attribute( |
| 379 'issue', issue_id, 'title', self.empty_form | 487 'issue', issue_id, 'title', form |
| 380 ) | 488 ) |
| 381 self.assertEqual(self.dummy_client.response_code, 200) | 489 self.assertEqual(self.dummy_client.response_code, 200) |
| 382 | 490 |
| 491 del(form.list[-1]) | |
| 492 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 493 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 383 results = self.server.delete_attribute( | 494 results = self.server.delete_attribute( |
| 384 'issue', issue_id, 'nosy', self.empty_form | 495 'issue', issue_id, 'nosy', form |
| 385 ) | 496 ) |
| 386 self.assertEqual(self.dummy_client.response_code, 200) | 497 self.assertEqual(self.dummy_client.response_code, 200) |
| 387 | 498 |
| 388 # verify the result | 499 # verify the result |
| 389 results = self.server.get_element('issue', issue_id, self.empty_form) | 500 results = self.server.get_element('issue', issue_id, self.empty_form) |
| 398 Test Patch op 'Add' | 509 Test Patch op 'Add' |
| 399 """ | 510 """ |
| 400 # create a new issue with userid 1 in the nosy list | 511 # create a new issue with userid 1 in the nosy list |
| 401 issue_id = self.db.issue.create(title='foo', nosy=['1']) | 512 issue_id = self.db.issue.create(title='foo', nosy=['1']) |
| 402 | 513 |
| 403 # add userid 2 to the nosy list | 514 # fail to add userid 2 to the nosy list |
| 515 # no etag | |
| 404 form = cgi.FieldStorage() | 516 form = cgi.FieldStorage() |
| 405 form.list = [ | 517 form.list = [ |
| 406 cgi.MiniFieldStorage('op', 'add'), | 518 cgi.MiniFieldStorage('op', 'add'), |
| 407 cgi.MiniFieldStorage('nosy', '2') | 519 cgi.MiniFieldStorage('nosy', '2') |
| 408 ] | 520 ] |
| 409 results = self.server.patch_element('issue', issue_id, form) | 521 results = self.server.patch_element('issue', issue_id, form) |
| 522 self.assertEqual(self.dummy_client.response_code, 412) | |
| 523 | |
| 524 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 525 form = cgi.FieldStorage() | |
| 526 form.list = [ | |
| 527 cgi.MiniFieldStorage('op', 'add'), | |
| 528 cgi.MiniFieldStorage('nosy', '2'), | |
| 529 cgi.MiniFieldStorage('@etag', etag) | |
| 530 ] | |
| 531 results = self.server.patch_element('issue', issue_id, form) | |
| 410 self.assertEqual(self.dummy_client.response_code, 200) | 532 self.assertEqual(self.dummy_client.response_code, 200) |
| 411 | 533 |
| 412 # verify the result | 534 # verify the result |
| 413 results = self.server.get_element('issue', issue_id, self.empty_form) | 535 results = self.server.get_element('issue', issue_id, self.empty_form) |
| 414 results = results['data'] | 536 results = results['data'] |
| 421 Test Patch op 'Replace' | 543 Test Patch op 'Replace' |
| 422 """ | 544 """ |
| 423 # create a new issue with userid 1 in the nosy list and status = 1 | 545 # create a new issue with userid 1 in the nosy list and status = 1 |
| 424 issue_id = self.db.issue.create(title='foo', nosy=['1'], status='1') | 546 issue_id = self.db.issue.create(title='foo', nosy=['1'], status='1') |
| 425 | 547 |
| 426 # replace userid 2 to the nosy list and status = 3 | 548 # fail to replace userid 2 to the nosy list and status = 3 |
| 549 # no etag. | |
| 427 form = cgi.FieldStorage() | 550 form = cgi.FieldStorage() |
| 428 form.list = [ | 551 form.list = [ |
| 429 cgi.MiniFieldStorage('op', 'replace'), | 552 cgi.MiniFieldStorage('op', 'replace'), |
| 430 cgi.MiniFieldStorage('nosy', '2'), | 553 cgi.MiniFieldStorage('nosy', '2'), |
| 431 cgi.MiniFieldStorage('status', '3') | 554 cgi.MiniFieldStorage('status', '3') |
| 432 ] | 555 ] |
| 433 results = self.server.patch_element('issue', issue_id, form) | 556 results = self.server.patch_element('issue', issue_id, form) |
| 434 self.assertEqual(self.dummy_client.response_code, 200) | 557 self.assertEqual(self.dummy_client.response_code, 412) |
| 435 | 558 results = self.server.get_element('issue', issue_id, self.empty_form) |
| 559 results = results['data'] | |
| 560 self.assertEqual(self.dummy_client.response_code, 200) | |
| 561 self.assertEqual(results['attributes']['status'], '1') | |
| 562 self.assertEqual(len(results['attributes']['nosy']), 1) | |
| 563 self.assertListEqual(results['attributes']['nosy'], ['1']) | |
| 564 | |
| 565 # replace userid 2 to the nosy list and status = 3 | |
| 566 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 567 form = cgi.FieldStorage() | |
| 568 form.list = [ | |
| 569 cgi.MiniFieldStorage('op', 'replace'), | |
| 570 cgi.MiniFieldStorage('nosy', '2'), | |
| 571 cgi.MiniFieldStorage('status', '3'), | |
| 572 cgi.MiniFieldStorage('@etag', etag) | |
| 573 ] | |
| 574 results = self.server.patch_element('issue', issue_id, form) | |
| 575 self.assertEqual(self.dummy_client.response_code, 200) | |
| 436 # verify the result | 576 # verify the result |
| 437 results = self.server.get_element('issue', issue_id, self.empty_form) | 577 results = self.server.get_element('issue', issue_id, self.empty_form) |
| 438 results = results['data'] | 578 results = results['data'] |
| 439 self.assertEqual(self.dummy_client.response_code, 200) | 579 self.assertEqual(self.dummy_client.response_code, 200) |
| 440 self.assertEqual(results['attributes']['status'], '3') | 580 self.assertEqual(results['attributes']['status'], '3') |
| 446 Test Patch Action 'Remove' | 586 Test Patch Action 'Remove' |
| 447 """ | 587 """ |
| 448 # create a new issue with userid 1 and 2 in the nosy list | 588 # create a new issue with userid 1 and 2 in the nosy list |
| 449 issue_id = self.db.issue.create(title='foo', nosy=['1', '2']) | 589 issue_id = self.db.issue.create(title='foo', nosy=['1', '2']) |
| 450 | 590 |
| 451 # remove the nosy list and the title | 591 # fail to remove the nosy list and the title |
| 592 # no etag | |
| 452 form = cgi.FieldStorage() | 593 form = cgi.FieldStorage() |
| 453 form.list = [ | 594 form.list = [ |
| 454 cgi.MiniFieldStorage('op', 'remove'), | 595 cgi.MiniFieldStorage('op', 'remove'), |
| 455 cgi.MiniFieldStorage('nosy', ''), | 596 cgi.MiniFieldStorage('nosy', ''), |
| 456 cgi.MiniFieldStorage('title', '') | 597 cgi.MiniFieldStorage('title', '') |
| 457 ] | 598 ] |
| 458 results = self.server.patch_element('issue', issue_id, form) | 599 results = self.server.patch_element('issue', issue_id, form) |
| 600 self.assertEqual(self.dummy_client.response_code, 412) | |
| 601 results = self.server.get_element('issue', issue_id, self.empty_form) | |
| 602 results = results['data'] | |
| 603 self.assertEqual(self.dummy_client.response_code, 200) | |
| 604 self.assertEqual(results['attributes']['title'], 'foo') | |
| 605 self.assertEqual(len(results['attributes']['nosy']), 2) | |
| 606 self.assertEqual(results['attributes']['nosy'], ['1', '2']) | |
| 607 | |
| 608 # remove the nosy list and the title | |
| 609 form = cgi.FieldStorage() | |
| 610 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 611 form.list = [ | |
| 612 cgi.MiniFieldStorage('op', 'remove'), | |
| 613 cgi.MiniFieldStorage('nosy', ''), | |
| 614 cgi.MiniFieldStorage('title', ''), | |
| 615 cgi.MiniFieldStorage('@etag', etag) | |
| 616 ] | |
| 617 results = self.server.patch_element('issue', issue_id, form) | |
| 459 self.assertEqual(self.dummy_client.response_code, 200) | 618 self.assertEqual(self.dummy_client.response_code, 200) |
| 460 | 619 |
| 461 # verify the result | 620 # verify the result |
| 462 results = self.server.get_element('issue', issue_id, self.empty_form) | 621 results = self.server.get_element('issue', issue_id, self.empty_form) |
| 463 results = results['data'] | 622 results = results['data'] |
| 471 Test Patch Action 'Action' | 630 Test Patch Action 'Action' |
| 472 """ | 631 """ |
| 473 # create a new issue with userid 1 and 2 in the nosy list | 632 # create a new issue with userid 1 and 2 in the nosy list |
| 474 issue_id = self.db.issue.create(title='foo') | 633 issue_id = self.db.issue.create(title='foo') |
| 475 | 634 |
| 476 # execute action retire | 635 # fail to execute action retire |
| 636 # no etag | |
| 477 form = cgi.FieldStorage() | 637 form = cgi.FieldStorage() |
| 478 form.list = [ | 638 form.list = [ |
| 479 cgi.MiniFieldStorage('op', 'action'), | 639 cgi.MiniFieldStorage('op', 'action'), |
| 480 cgi.MiniFieldStorage('action_name', 'retire') | 640 cgi.MiniFieldStorage('action_name', 'retire') |
| 481 ] | 641 ] |
| 482 results = self.server.patch_element('issue', issue_id, form) | 642 results = self.server.patch_element('issue', issue_id, form) |
| 643 self.assertEqual(self.dummy_client.response_code, 412) | |
| 644 self.assertFalse(self.db.issue.is_retired(issue_id)) | |
| 645 | |
| 646 # execute action retire | |
| 647 form = cgi.FieldStorage() | |
| 648 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 649 form.list = [ | |
| 650 cgi.MiniFieldStorage('op', 'action'), | |
| 651 cgi.MiniFieldStorage('action_name', 'retire'), | |
| 652 cgi.MiniFieldStorage('@etag', etag) | |
| 653 ] | |
| 654 results = self.server.patch_element('issue', issue_id, form) | |
| 483 self.assertEqual(self.dummy_client.response_code, 200) | 655 self.assertEqual(self.dummy_client.response_code, 200) |
| 484 | 656 |
| 485 # verify the result | 657 # verify the result |
| 486 self.assertTrue(self.db.issue.is_retired(issue_id)) | 658 self.assertTrue(self.db.issue.is_retired(issue_id)) |
| 487 | 659 |
| 490 Test Patch Action 'Remove' only some element from a list | 662 Test Patch Action 'Remove' only some element from a list |
| 491 """ | 663 """ |
| 492 # create a new issue with userid 1, 2, 3 in the nosy list | 664 # create a new issue with userid 1, 2, 3 in the nosy list |
| 493 issue_id = self.db.issue.create(title='foo', nosy=['1', '2', '3']) | 665 issue_id = self.db.issue.create(title='foo', nosy=['1', '2', '3']) |
| 494 | 666 |
| 495 # remove the nosy list and the title | 667 # fail to remove the nosy list and the title |
| 668 # no etag | |
| 496 form = cgi.FieldStorage() | 669 form = cgi.FieldStorage() |
| 497 form.list = [ | 670 form.list = [ |
| 498 cgi.MiniFieldStorage('op', 'remove'), | 671 cgi.MiniFieldStorage('op', 'remove'), |
| 499 cgi.MiniFieldStorage('nosy', '1, 2'), | 672 cgi.MiniFieldStorage('nosy', '1, 2'), |
| 673 ] | |
| 674 results = self.server.patch_element('issue', issue_id, form) | |
| 675 self.assertEqual(self.dummy_client.response_code, 412) | |
| 676 results = self.server.get_element('issue', issue_id, self.empty_form) | |
| 677 results = results['data'] | |
| 678 self.assertEqual(self.dummy_client.response_code, 200) | |
| 679 self.assertEqual(len(results['attributes']['nosy']), 3) | |
| 680 self.assertEqual(results['attributes']['nosy'], ['1', '2', '3']) | |
| 681 | |
| 682 # remove the nosy list and the title | |
| 683 form = cgi.FieldStorage() | |
| 684 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 685 form.list = [ | |
| 686 cgi.MiniFieldStorage('op', 'remove'), | |
| 687 cgi.MiniFieldStorage('nosy', '1, 2'), | |
| 688 cgi.MiniFieldStorage('@etag', etag) | |
| 500 ] | 689 ] |
| 501 results = self.server.patch_element('issue', issue_id, form) | 690 results = self.server.patch_element('issue', issue_id, form) |
| 502 self.assertEqual(self.dummy_client.response_code, 200) | 691 self.assertEqual(self.dummy_client.response_code, 200) |
| 503 | 692 |
| 504 # verify the result | 693 # verify the result |
