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 """

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