Mercurial > p > roundup > code
comparison test/rest_common.py @ 5705:457fc482e6b1
Method PUT: ignore specification of protected properties which can not
be set. Filtering them out of the payload list. This lets the result
of a get using:
class/id?@protected=true&@verbose=0
be used as input to a PUT operation without having to strip the
protected properties.
Note this does not raise an error if the PUT protected property is
different from the value in the db. If the property is different but
the etag/if-match passes, the user attempted to set the protected
property and this should result in an error, but will not with this
patch.
Method DELETE class/id/attribute: raise error when trying to delete
protected or required attribute/property. Raise UsageError
when attribute doesn't exist.
Method PATCH class/id:
raise error when trying to replace/remove protected attribute/property
raise error when trying to remove required attribute/property
Catch KeyError at top level and turn into 400 error.
If payload has an attribute/property that does not exist, raise
UsageError which becomes a 400 error.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Thu, 11 Apr 2019 20:54:39 -0400 |
| parents | 4aae822e2cb4 |
| children | dfca6136dd7b |
comparison
equal
deleted
inserted
replaced
| 5704:aa13a517cc63 | 5705:457fc482e6b1 |
|---|---|
| 2 import os | 2 import os |
| 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.exceptions import * | |
| 7 from roundup import password, hyperdb | 8 from roundup import password, hyperdb |
| 8 from roundup.rest import RestfulInstance, calculate_etag | 9 from roundup.rest import RestfulInstance, calculate_etag |
| 9 from roundup.backends import list_backends | 10 from roundup.backends import list_backends |
| 10 from roundup.cgi import client | 11 from roundup.cgi import client |
| 11 from roundup.anypy.strings import b2s, s2b | 12 from roundup.anypy.strings import b2s, s2b |
| 54 | 55 |
| 55 self.db.issue.addprop(tx_Source=hyperdb.String()) | 56 self.db.issue.addprop(tx_Source=hyperdb.String()) |
| 56 self.db.issue.addprop(anint=hyperdb.Integer()) | 57 self.db.issue.addprop(anint=hyperdb.Integer()) |
| 57 self.db.issue.addprop(afloat=hyperdb.Number()) | 58 self.db.issue.addprop(afloat=hyperdb.Number()) |
| 58 self.db.issue.addprop(abool=hyperdb.Boolean()) | 59 self.db.issue.addprop(abool=hyperdb.Boolean()) |
| 60 self.db.issue.addprop(requireme=hyperdb.String(required=True)) | |
| 59 self.db.msg.addprop(tx_Source=hyperdb.String()) | 61 self.db.msg.addprop(tx_Source=hyperdb.String()) |
| 60 | 62 |
| 61 self.db.post_init() | 63 self.db.post_init() |
| 62 | 64 |
| 63 thisdir = os.path.dirname(__file__) | 65 thisdir = os.path.dirname(__file__) |
| 141 self.assertEqual(self.dummy_client.response_code, 200) | 143 self.assertEqual(self.dummy_client.response_code, 200) |
| 142 self.assertEqual(results['data']['data'], 'joe') | 144 self.assertEqual(results['data']['data'], 'joe') |
| 143 | 145 |
| 144 def testOutputFormat(self): | 146 def testOutputFormat(self): |
| 145 """ test of @fields and @verbose implementation """ | 147 """ test of @fields and @verbose implementation """ |
| 146 | |
| 147 from roundup.exceptions import UsageError | |
| 148 | 148 |
| 149 self.maxDiff = 4000 | 149 self.maxDiff = 4000 |
| 150 # create sample data | 150 # create sample data |
| 151 try: | 151 try: |
| 152 self.db.status.create(name='open') | 152 self.db.status.create(name='open') |
| 618 | 618 |
| 619 def notestEtagGeneration(self): | 619 def notestEtagGeneration(self): |
| 620 ''' Make sure etag generation is stable | 620 ''' Make sure etag generation is stable |
| 621 | 621 |
| 622 FIXME need to mock somehow date.Date() when creating | 622 FIXME need to mock somehow date.Date() when creating |
| 623 the target to be mocked. The differening dates makes | 623 the target to be mocked. The differing dates makes |
| 624 this test impossible. | 624 this test impossible. |
| 625 ''' | 625 ''' |
| 626 newuser = self.db.user.create( | 626 newuser = self.db.user.create( |
| 627 username='john', | 627 username='john', |
| 628 password=password.Password('random1'), | 628 password=password.Password('random1'), |
| 983 self.assertEqual(self.dummy_client.response_code, 200) | 983 self.assertEqual(self.dummy_client.response_code, 200) |
| 984 self.assertEqual(results['data']['attributes']['title'], | 984 self.assertEqual(results['data']['attributes']['title'], |
| 985 'foo bar') | 985 'foo bar') |
| 986 del(self.headers) | 986 del(self.headers) |
| 987 | 987 |
| 988 def testPut(self): | 988 def testPutElement(self): |
| 989 """ | 989 """ |
| 990 Change joe's 'realname' | 990 Change joe's 'realname' |
| 991 Check if we can't change admin's detail | 991 Check if we can't change admin's detail |
| 992 """ | 992 """ |
| 993 # fail to change Joe's realname via attribute uri | 993 # fail to change Joe's realname via attribute uri |
| 1024 self.assertEqual(self.dummy_client.response_code, 200) | 1024 self.assertEqual(self.dummy_client.response_code, 200) |
| 1025 self.assertEqual(results['data']['data'], 'Joe Doe Doe') | 1025 self.assertEqual(results['data']['data'], 'Joe Doe Doe') |
| 1026 del(self.headers) | 1026 del(self.headers) |
| 1027 | 1027 |
| 1028 # Reset joe's 'realname'. etag in body | 1028 # Reset joe's 'realname'. etag in body |
| 1029 # Also try to set protected items. The protected items should | |
| 1030 # be ignored on put_element to make it easy to get the item | |
| 1031 # with all fields, change one field and put the result without | |
| 1032 # having to filter out protected items. | |
| 1029 form = cgi.FieldStorage() | 1033 form = cgi.FieldStorage() |
| 1030 etag = calculate_etag(self.db.user.getnode(self.joeid)) | 1034 etag = calculate_etag(self.db.user.getnode(self.joeid)) |
| 1031 form.list = [ | 1035 form.list = [ |
| 1036 cgi.MiniFieldStorage('creator', '3'), | |
| 1032 cgi.MiniFieldStorage('realname', 'Joe Doe'), | 1037 cgi.MiniFieldStorage('realname', 'Joe Doe'), |
| 1033 cgi.MiniFieldStorage('@etag', etag) | 1038 cgi.MiniFieldStorage('@etag', etag) |
| 1034 ] | 1039 ] |
| 1035 results = self.server.put_element('user', self.joeid, form) | 1040 results = self.server.put_element('user', self.joeid, form) |
| 1036 self.assertEqual(self.dummy_client.response_code, 200) | 1041 self.assertEqual(self.dummy_client.response_code, 200) |
| 1037 results = self.server.get_element('user', self.joeid, self.empty_form) | 1042 results = self.server.get_element('user', self.joeid, self.empty_form) |
| 1038 self.assertEqual(self.dummy_client.response_code, 200) | 1043 self.assertEqual(self.dummy_client.response_code, 200) |
| 1039 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe') | 1044 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe') |
| 1040 | 1045 |
| 1041 # check we can't change admin's details | 1046 # We are joe, so check we can't change admin's details |
| 1042 results = self.server.put_element('user', '1', form) | 1047 results = self.server.put_element('user', '1', form) |
| 1043 self.assertEqual(self.dummy_client.response_code, 403) | 1048 self.assertEqual(self.dummy_client.response_code, 403) |
| 1044 self.assertEqual(results['error']['status'], 403) | 1049 self.assertEqual(results['error']['status'], 403) |
| 1050 | |
| 1051 # Try to reset joe's 'realname' and add a broken prop. | |
| 1052 # This should result in no change to the name and | |
| 1053 # a 400 UsageError stating prop does not exist. | |
| 1054 form = cgi.FieldStorage() | |
| 1055 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 1056 form.list = [ | |
| 1057 cgi.MiniFieldStorage('JustKidding', '3'), | |
| 1058 cgi.MiniFieldStorage('realname', 'Joe Doe'), | |
| 1059 cgi.MiniFieldStorage('@etag', etag) | |
| 1060 ] | |
| 1061 results = self.server.put_element('user', self.joeid, form) | |
| 1062 expected= {'error': {'status': 400, | |
| 1063 'msg': UsageError('Property JustKidding not ' | |
| 1064 'found in class user')}} | |
| 1065 self.assertEqual(results['error']['status'], | |
| 1066 expected['error']['status']) | |
| 1067 self.assertEqual(type(results['error']['msg']), | |
| 1068 type(expected['error']['msg'])) | |
| 1069 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1070 results = self.server.get_element('user', self.joeid, self.empty_form) | |
| 1071 self.assertEqual(self.dummy_client.response_code, 200) | |
| 1072 self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe') | |
| 1073 | |
| 1074 def testPutAttribute(self): | |
| 1075 # put protected property | |
| 1076 # make sure we don't have permission issues | |
| 1077 self.db.setCurrentUser('admin') | |
| 1078 form = cgi.FieldStorage() | |
| 1079 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 1080 form.list = [ | |
| 1081 cgi.MiniFieldStorage('data', '3'), | |
| 1082 cgi.MiniFieldStorage('@etag', etag) | |
| 1083 ] | |
| 1084 results = self.server.put_attribute( | |
| 1085 'user', self.joeid, 'creator', form | |
| 1086 ) | |
| 1087 expected= {'error': {'status': 400, 'msg': | |
| 1088 UsageError('\'"creator", "actor", "creation" and ' | |
| 1089 '"activity" are reserved\'')}} | |
| 1090 print(results) | |
| 1091 self.assertEqual(results['error']['status'], | |
| 1092 expected['error']['status']) | |
| 1093 self.assertEqual(type(results['error']['msg']), | |
| 1094 type(expected['error']['msg'])) | |
| 1095 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1096 | |
| 1097 # put invalid property | |
| 1098 # make sure we don't have permission issues | |
| 1099 self.db.setCurrentUser('admin') | |
| 1100 form = cgi.FieldStorage() | |
| 1101 etag = calculate_etag(self.db.user.getnode(self.joeid)) | |
| 1102 form.list = [ | |
| 1103 cgi.MiniFieldStorage('data', '3'), | |
| 1104 cgi.MiniFieldStorage('@etag', etag) | |
| 1105 ] | |
| 1106 results = self.server.put_attribute( | |
| 1107 'user', self.joeid, 'youMustBeKiddingMe', form | |
| 1108 ) | |
| 1109 expected= {'error': {'status': 400, | |
| 1110 'msg': UsageError("'youMustBeKiddingMe' " | |
| 1111 "is not a property of user")}} | |
| 1112 print(results) | |
| 1113 self.assertEqual(results['error']['status'], | |
| 1114 expected['error']['status']) | |
| 1115 self.assertEqual(type(results['error']['msg']), | |
| 1116 type(expected['error']['msg'])) | |
| 1117 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1045 | 1118 |
| 1046 def testPost(self): | 1119 def testPost(self): |
| 1047 """ | 1120 """ |
| 1048 Post a new issue with title: foo | 1121 Post a new issue with title: foo |
| 1049 Verify the information of the created issue | 1122 Verify the information of the created issue |
| 1147 | 1220 |
| 1148 def testDeleteAttributeUri(self): | 1221 def testDeleteAttributeUri(self): |
| 1149 """ | 1222 """ |
| 1150 Test Delete an attribute | 1223 Test Delete an attribute |
| 1151 """ | 1224 """ |
| 1225 self.maxDiff = 4000 | |
| 1152 # create a new issue with userid 1 in the nosy list | 1226 # create a new issue with userid 1 in the nosy list |
| 1153 issue_id = self.db.issue.create(title='foo', nosy=['1']) | 1227 issue_id = self.db.issue.create(title='foo', nosy=['1']) |
| 1154 | 1228 |
| 1155 # No etag, so this should return 412 - Precondition Failed | 1229 # No etag, so this should return 412 - Precondition Failed |
| 1156 # With no changes | 1230 # With no changes |
| 1188 self.assertEqual(self.dummy_client.response_code, 200) | 1262 self.assertEqual(self.dummy_client.response_code, 200) |
| 1189 self.assertEqual(len(results['attributes']['nosy']), 0) | 1263 self.assertEqual(len(results['attributes']['nosy']), 0) |
| 1190 self.assertListEqual(results['attributes']['nosy'], []) | 1264 self.assertListEqual(results['attributes']['nosy'], []) |
| 1191 self.assertEqual(results['attributes']['title'], None) | 1265 self.assertEqual(results['attributes']['title'], None) |
| 1192 | 1266 |
| 1267 # delete protected property | |
| 1268 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 1269 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 1270 results = self.server.delete_attribute( | |
| 1271 'issue', issue_id, 'creator', form | |
| 1272 ) | |
| 1273 expected= {'error': { | |
| 1274 'status': 400, | |
| 1275 'msg': UsageError("Attribute 'creator' can not be updated for class issue.") | |
| 1276 }} | |
| 1277 | |
| 1278 self.assertEqual(results['error']['status'], | |
| 1279 expected['error']['status']) | |
| 1280 self.assertEqual(type(results['error']['msg']), | |
| 1281 type(expected['error']['msg'])) | |
| 1282 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1283 | |
| 1284 # delete required property | |
| 1285 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 1286 form.list.append(cgi.MiniFieldStorage('@etag', etag)) | |
| 1287 results = self.server.delete_attribute( | |
| 1288 'issue', issue_id, 'requireme', form | |
| 1289 ) | |
| 1290 expected= {'error': {'status': 400, | |
| 1291 'msg': UsageError("Attribute 'requireme' is " | |
| 1292 "required by class issue and can not be deleted.")}} | |
| 1293 print(results) | |
| 1294 self.assertEqual(results['error']['status'], | |
| 1295 expected['error']['status']) | |
| 1296 self.assertEqual(type(results['error']['msg']), | |
| 1297 type(expected['error']['msg'])) | |
| 1298 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1299 | |
| 1193 def testPatchAdd(self): | 1300 def testPatchAdd(self): |
| 1194 """ | 1301 """ |
| 1195 Test Patch op 'Add' | 1302 Test Patch op 'Add' |
| 1196 """ | 1303 """ |
| 1197 # create a new issue with userid 1 in the nosy list | 1304 # create a new issue with userid 1 in the nosy list |
| 1265 self.assertEqual(self.dummy_client.response_code, 200) | 1372 self.assertEqual(self.dummy_client.response_code, 200) |
| 1266 self.assertEqual(results['attributes']['status'], '3') | 1373 self.assertEqual(results['attributes']['status'], '3') |
| 1267 self.assertEqual(len(results['attributes']['nosy']), 1) | 1374 self.assertEqual(len(results['attributes']['nosy']), 1) |
| 1268 self.assertListEqual(results['attributes']['nosy'], ['2']) | 1375 self.assertListEqual(results['attributes']['nosy'], ['2']) |
| 1269 | 1376 |
| 1377 # try to set a protected prop. It should fail. | |
| 1378 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 1379 form = cgi.FieldStorage() | |
| 1380 form.list = [ | |
| 1381 cgi.MiniFieldStorage('@op', 'replace'), | |
| 1382 cgi.MiniFieldStorage('creator', '2'), | |
| 1383 cgi.MiniFieldStorage('@etag', etag) | |
| 1384 ] | |
| 1385 results = self.server.patch_element('issue', issue_id, form) | |
| 1386 expected= {'error': {'status': 400, | |
| 1387 'msg': KeyError('"creator", "actor", "creation" and "activity" are reserved',)}} | |
| 1388 print(results) | |
| 1389 self.assertEqual(results['error']['status'], | |
| 1390 expected['error']['status']) | |
| 1391 self.assertEqual(type(results['error']['msg']), | |
| 1392 type(expected['error']['msg'])) | |
| 1393 self.assertEqual(str(results['error']['msg']), | |
| 1394 str(expected['error']['msg'])) | |
| 1395 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1396 | |
| 1270 def testPatchRemoveAll(self): | 1397 def testPatchRemoveAll(self): |
| 1271 """ | 1398 """ |
| 1272 Test Patch Action 'Remove' | 1399 Test Patch Action 'Remove' |
| 1273 """ | 1400 """ |
| 1274 # create a new issue with userid 1 and 2 in the nosy list | 1401 # create a new issue with userid 1 and 2 in the nosy list |
| 1308 results = results['data'] | 1435 results = results['data'] |
| 1309 self.assertEqual(self.dummy_client.response_code, 200) | 1436 self.assertEqual(self.dummy_client.response_code, 200) |
| 1310 self.assertEqual(results['attributes']['title'], None) | 1437 self.assertEqual(results['attributes']['title'], None) |
| 1311 self.assertEqual(len(results['attributes']['nosy']), 0) | 1438 self.assertEqual(len(results['attributes']['nosy']), 0) |
| 1312 self.assertEqual(results['attributes']['nosy'], []) | 1439 self.assertEqual(results['attributes']['nosy'], []) |
| 1440 | |
| 1441 # try to remove a protected prop. It should fail. | |
| 1442 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 1443 form = cgi.FieldStorage() | |
| 1444 form.list = [ | |
| 1445 cgi.MiniFieldStorage('@op', 'remove'), | |
| 1446 cgi.MiniFieldStorage('creator', '2'), | |
| 1447 cgi.MiniFieldStorage('@etag', etag) | |
| 1448 ] | |
| 1449 results = self.server.patch_element('issue', issue_id, form) | |
| 1450 expected= {'error': {'status': 400, | |
| 1451 'msg': KeyError('"creator", "actor", "creation" and "activity" are reserved',)}} | |
| 1452 print(results) | |
| 1453 self.assertEqual(results['error']['status'], | |
| 1454 expected['error']['status']) | |
| 1455 self.assertEqual(type(results['error']['msg']), | |
| 1456 type(expected['error']['msg'])) | |
| 1457 self.assertEqual(str(results['error']['msg']), | |
| 1458 str(expected['error']['msg'])) | |
| 1459 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1460 | |
| 1461 # try to remove a required prop. it should fail | |
| 1462 etag = calculate_etag(self.db.issue.getnode(issue_id)) | |
| 1463 form.list = [ | |
| 1464 cgi.MiniFieldStorage('@op', 'remove'), | |
| 1465 cgi.MiniFieldStorage('requireme', ''), | |
| 1466 cgi.MiniFieldStorage('@etag', etag) | |
| 1467 ] | |
| 1468 results = self.server.patch_element('issue', issue_id, form) | |
| 1469 expected= {'error': {'status': 400, | |
| 1470 'msg': UsageError("Attribute 'requireme' is required by class issue and can not be removed.") | |
| 1471 }} | |
| 1472 print(results) | |
| 1473 self.assertEqual(results['error']['status'], | |
| 1474 expected['error']['status']) | |
| 1475 self.assertEqual(type(results['error']['msg']), | |
| 1476 type(expected['error']['msg'])) | |
| 1477 self.assertEqual(str(results['error']['msg']), | |
| 1478 str(expected['error']['msg'])) | |
| 1479 self.assertEqual(self.dummy_client.response_code, 400) | |
| 1313 | 1480 |
| 1314 def testPatchAction(self): | 1481 def testPatchAction(self): |
| 1315 """ | 1482 """ |
| 1316 Test Patch Action 'Action' | 1483 Test Patch Action 'Action' |
| 1317 """ | 1484 """ |
