comparison test/rest_common.py @ 6325:1a15089c2e49 issue2550923_computed_property

Merge trunk into branch
author John Rouillard <rouilj@ieee.org>
date Sat, 06 Feb 2021 20:15:26 -0500
parents ec853cef2f09
children 6a69584d117e
comparison
equal deleted inserted replaced
6319:20e77c3ce6f6 6325:1a15089c2e49
66 66
67 backend = None 67 backend = None
68 url_pfx = 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/' 68 url_pfx = 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/'
69 69
70 def setUp(self): 70 def setUp(self):
71 from packaging import version
72
71 self.dirname = '_test_rest' 73 self.dirname = '_test_rest'
72 # set up and open a tracker 74 # set up and open a tracker
73 # Set optimize=True as code under test (Client.main()::determine_user) 75 # Set optimize=True as code under test (Client.main()::determine_user)
74 # will close and re-open the database on user changes. This wipes 76 # will close and re-open the database on user changes. This wipes
75 # out additions to the schema needed for testing. 77 # out additions to the schema needed for testing.
160 'aud': self.db.config.TRACKER_WEB, 162 'aud': self.db.config.TRACKER_WEB,
161 'roles': [ 'User' ], 163 'roles': [ 'User' ],
162 'iat': now_ts, 164 'iat': now_ts,
163 'exp': plus1min_ts, 165 'exp': plus1min_ts,
164 } 166 }
167
168 # in version 2.0.0 and newer jwt.encode returns string
169 # not bytestring. So we have to skip b2s conversion
165 170
171 if version.parse(jwt.__version__) >= version.parse('2.0.0'):
172 tostr = lambda x: x
173 else:
174 tostr = b2s
175
166 self.jwt = {} 176 self.jwt = {}
167 self.claim = {} 177 self.claim = {}
168 # generate invalid claim with expired timestamp 178 # generate invalid claim with expired timestamp
169 self.claim['expired'] = copy(claim) 179 self.claim['expired'] = copy(claim)
170 self.claim['expired']['exp'] = expired_ts 180 self.claim['expired']['exp'] = expired_ts
171 self.jwt['expired'] = b2s(jwt.encode(self.claim['expired'], secret, 181 self.jwt['expired'] = tostr(jwt.encode(self.claim['expired'], secret,
172 algorithm='HS256')) 182 algorithm='HS256'))
173 183
174 # generate valid claim with user role 184 # generate valid claim with user role
175 self.claim['user'] = copy(claim) 185 self.claim['user'] = copy(claim)
176 self.claim['user']['exp'] = plus1min_ts 186 self.claim['user']['exp'] = plus1min_ts
177 self.jwt['user'] = b2s(jwt.encode(self.claim['user'], secret, 187 self.jwt['user'] = tostr(jwt.encode(self.claim['user'], secret,
178 algorithm='HS256')) 188 algorithm='HS256'))
179 # generate invalid claim bad issuer 189 # generate invalid claim bad issuer
180 self.claim['badiss'] = copy(claim) 190 self.claim['badiss'] = copy(claim)
181 self.claim['badiss']['iss'] = "http://someissuer/bugs" 191 self.claim['badiss']['iss'] = "http://someissuer/bugs"
182 self.jwt['badiss'] = b2s(jwt.encode(self.claim['badiss'], secret, 192 self.jwt['badiss'] = tostr(jwt.encode(self.claim['badiss'], secret,
183 algorithm='HS256')) 193 algorithm='HS256'))
184 # generate invalid claim bad aud(ience) 194 # generate invalid claim bad aud(ience)
185 self.claim['badaud'] = copy(claim) 195 self.claim['badaud'] = copy(claim)
186 self.claim['badaud']['aud'] = "http://someaudience/bugs" 196 self.claim['badaud']['aud'] = "http://someaudience/bugs"
187 self.jwt['badaud'] = b2s(jwt.encode(self.claim['badaud'], secret, 197 self.jwt['badaud'] = tostr(jwt.encode(self.claim['badaud'], secret,
188 algorithm='HS256')) 198 algorithm='HS256'))
189 # generate invalid claim bad sub(ject) 199 # generate invalid claim bad sub(ject)
190 self.claim['badsub'] = copy(claim) 200 self.claim['badsub'] = copy(claim)
191 self.claim['badsub']['sub'] = str("99") 201 self.claim['badsub']['sub'] = str("99")
192 self.jwt['badsub'] = b2s(jwt.encode(self.claim['badsub'], secret, 202 self.jwt['badsub'] = tostr(jwt.encode(self.claim['badsub'], secret,
193 algorithm='HS256')) 203 algorithm='HS256'))
194 # generate invalid claim bad roles 204 # generate invalid claim bad roles
195 self.claim['badroles'] = copy(claim) 205 self.claim['badroles'] = copy(claim)
196 self.claim['badroles']['roles'] = [ "badrole1", "badrole2" ] 206 self.claim['badroles']['roles'] = [ "badrole1", "badrole2" ]
197 self.jwt['badroles'] = b2s(jwt.encode(self.claim['badroles'], secret, 207 self.jwt['badroles'] = tostr(jwt.encode(self.claim['badroles'], secret,
198 algorithm='HS256')) 208 algorithm='HS256'))
199 # generate valid claim with limited user:email role 209 # generate valid claim with limited user:email role
200 self.claim['user:email'] = copy(claim) 210 self.claim['user:email'] = copy(claim)
201 self.claim['user:email']['roles'] = [ "user:email" ] 211 self.claim['user:email']['roles'] = [ "user:email" ]
202 self.jwt['user:email'] = b2s(jwt.encode(self.claim['user:email'], secret, 212 self.jwt['user:email'] = tostr(jwt.encode(self.claim['user:email'], secret,
203 algorithm='HS256')) 213 algorithm='HS256'))
204 214
205 # generate valid claim with limited user:emailnorest role 215 # generate valid claim with limited user:emailnorest role
206 self.claim['user:emailnorest'] = copy(claim) 216 self.claim['user:emailnorest'] = copy(claim)
207 self.claim['user:emailnorest']['roles'] = [ "user:emailnorest" ] 217 self.claim['user:emailnorest']['roles'] = [ "user:emailnorest" ]
208 self.jwt['user:emailnorest'] = b2s(jwt.encode(self.claim['user:emailnorest'], secret, 218 self.jwt['user:emailnorest'] = tostr(jwt.encode(self.claim['user:emailnorest'], secret,
209 algorithm='HS256')) 219 algorithm='HS256'))
210 220
211 self.db.tx_Source = 'web' 221 self.db.tx_Source = 'web'
212 222
213 self.db.issue.addprop(tx_Source=hyperdb.String()) 223 self.db.issue.addprop(tx_Source=hyperdb.String())
1320 self.assertEqual(json_dict['data']['attributes']\ 1330 self.assertEqual(json_dict['data']['attributes']\
1321 ['assignedto']['link'], 1331 ['assignedto']['link'],
1322 "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/user/2") 1332 "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/user/2")
1323 1333
1324 1334
1335 def testDispatchDelete(self):
1336 """
1337 run Delete through rest dispatch().
1338 """
1339
1340 # TEST #0
1341 # Delete class raises unauthorized error
1342 # simulate: /rest/data/issue
1343 env = { "REQUEST_METHOD": "DELETE"
1344 }
1345 headers={"accept": "application/json; version=1",
1346 }
1347 self.headers=headers
1348 self.server.client.request.headers.get=self.get_header
1349 results = self.server.dispatch(env["REQUEST_METHOD"],
1350 "/rest/data/issue",
1351 self.empty_form)
1352
1353 print(results)
1354 self.assertEqual(self.server.client.response_code, 403)
1355 json_dict = json.loads(b2s(results))
1356
1357 self.assertEqual(json_dict['error']['msg'],
1358 "Deletion of a whole class disabled")
1359
1360
1361 def testDispatchBadContent(self):
1362 """
1363 runthrough rest dispatch() with bad content_type patterns.
1364 """
1365
1366 # simulate: /rest/data/issue
1367 body=b'{ "title": "Joe Doe has problems", \
1368 "nosy": [ "1", "3" ], \
1369 "assignedto": "2", \
1370 "abool": true, \
1371 "afloat": 2.3, \
1372 "anint": 567890 \
1373 }'
1374 env = { "CONTENT_TYPE": "application/jzot",
1375 "CONTENT_LENGTH": len(body),
1376 "REQUEST_METHOD": "POST"
1377 }
1378
1379 headers={"accept": "application/json; version=1",
1380 "content-type": env['CONTENT_TYPE'],
1381 "content-length": env['CONTENT_LENGTH'],
1382 }
1383
1384 self.headers=headers
1385 # we need to generate a FieldStorage the looks like
1386 # FieldStorage(None, None, 'string') rather than
1387 # FieldStorage(None, None, [])
1388 body_file=BytesIO(body) # FieldStorage needs a file
1389 form = client.BinaryFieldStorage(body_file,
1390 headers=headers,
1391 environ=env)
1392 self.server.client.request.headers.get=self.get_header
1393 results = self.server.dispatch(env["REQUEST_METHOD"],
1394 "/rest/data/issue",
1395 form)
1396
1397 print(results)
1398 self.assertEqual(self.server.client.response_code, 415)
1399 json_dict = json.loads(b2s(results))
1400 self.assertEqual(json_dict['error']['msg'],
1401 "Unable to process input of type application/jzot")
1402
1403 # Test GET as well. I am not sure if this should pass or not.
1404 # Arguably GET doesn't use any form/json input but....
1405 results = self.server.dispatch('GET',
1406 "/rest/data/issue",
1407 form)
1408 print(results)
1409 self.assertEqual(self.server.client.response_code, 415)
1410
1411
1412
1413 def testDispatchBadAccept(self):
1414 # simulate: /rest/data/issue expect failure unknown accept settings
1415 body=b'{ "title": "Joe Doe has problems", \
1416 "nosy": [ "1", "3" ], \
1417 "assignedto": "2", \
1418 "abool": true, \
1419 "afloat": 2.3, \
1420 "anint": 567890 \
1421 }'
1422 env = { "CONTENT_TYPE": "application/json",
1423 "CONTENT_LENGTH": len(body),
1424 "REQUEST_METHOD": "POST"
1425 }
1426
1427 headers={"accept": "application/zot; version=1; q=0.5",
1428 "content-type": env['CONTENT_TYPE'],
1429 "content-length": env['CONTENT_LENGTH'],
1430 }
1431
1432 self.headers=headers
1433 # we need to generate a FieldStorage the looks like
1434 # FieldStorage(None, None, 'string') rather than
1435 # FieldStorage(None, None, [])
1436 body_file=BytesIO(body) # FieldStorage needs a file
1437 form = client.BinaryFieldStorage(body_file,
1438 headers=headers,
1439 environ=env)
1440 self.server.client.request.headers.get=self.get_header
1441 results = self.server.dispatch(env["REQUEST_METHOD"],
1442 "/rest/data/issue",
1443 form)
1444
1445 print(results)
1446 self.assertEqual(self.server.client.response_code, 406)
1447 self.assertIn(b"Requested content type 'application/zot; version=1; q=0.5' is not available.\nAcceptable types: */*, application/json", results)
1448
1449 # simulate: /rest/data/issue works, multiple acceptable output, one
1450 # is valid
1451 env = { "CONTENT_TYPE": "application/json",
1452 "CONTENT_LENGTH": len(body),
1453 "REQUEST_METHOD": "POST"
1454 }
1455
1456 headers={"accept": "application/zot; version=1; q=0.75, "
1457 "application/json; version=1; q=0.5",
1458 "content-type": env['CONTENT_TYPE'],
1459 "content-length": env['CONTENT_LENGTH'],
1460 }
1461
1462 self.headers=headers
1463 # we need to generate a FieldStorage the looks like
1464 # FieldStorage(None, None, 'string') rather than
1465 # FieldStorage(None, None, [])
1466 body_file=BytesIO(body) # FieldStorage needs a file
1467 form = client.BinaryFieldStorage(body_file,
1468 headers=headers,
1469 environ=env)
1470 self.server.client.request.headers.get=self.get_header
1471 results = self.server.dispatch(env["REQUEST_METHOD"],
1472 "/rest/data/issue",
1473 form)
1474
1475 print(results)
1476 self.assertEqual(self.server.client.response_code, 201)
1477 json_dict = json.loads(b2s(results))
1478 # ERROR this should be 1. What's happening is that the code
1479 # for handling 406 error code runs through everything and creates
1480 # the item. Then it throws a 406 after the work is done when it
1481 # realizes it can't respond as requested. So the 406 post above
1482 # creates issue 1 and this one creates issue 2.
1483 self.assertEqual(json_dict['data']['id'], "2")
1484
1485
1486 # test 3 accept is empty. This triggers */* so passes
1487 headers={"accept": "",
1488 "content-type": env['CONTENT_TYPE'],
1489 "content-length": env['CONTENT_LENGTH'],
1490 }
1491
1492 self.headers=headers
1493 # we need to generate a FieldStorage the looks like
1494 # FieldStorage(None, None, 'string') rather than
1495 # FieldStorage(None, None, [])
1496 body_file=BytesIO(body) # FieldStorage needs a file
1497 form = client.BinaryFieldStorage(body_file,
1498 headers=headers,
1499 environ=env)
1500 self.server.client.request.headers.get=self.get_header
1501 results = self.server.dispatch(env["REQUEST_METHOD"],
1502 "/rest/data/issue",
1503 form)
1504
1505 print(results)
1506 self.assertEqual(self.server.client.response_code, 201)
1507 json_dict = json.loads(b2s(results))
1508 # This is one more than above. Will need to be fixed
1509 # When error above is fixed.
1510 self.assertEqual(json_dict['data']['id'], "3")
1511
1512 # test 4 accept is random junk.
1513 headers={"accept": "Xyzzy I am not a mime, type;",
1514 "content-type": env['CONTENT_TYPE'],
1515 "content-length": env['CONTENT_LENGTH'],
1516 }
1517
1518 self.headers=headers
1519 # we need to generate a FieldStorage the looks like
1520 # FieldStorage(None, None, 'string') rather than
1521 # FieldStorage(None, None, [])
1522 body_file=BytesIO(body) # FieldStorage needs a file
1523 form = client.BinaryFieldStorage(body_file,
1524 headers=headers,
1525 environ=env)
1526 self.server.client.request.headers.get=self.get_header
1527 results = self.server.dispatch(env["REQUEST_METHOD"],
1528 "/rest/data/issue",
1529 form)
1530
1531 print(results)
1532 self.assertEqual(self.server.client.response_code, 406)
1533 json_dict = json.loads(b2s(results))
1534 self.assertIn('Unable to parse Accept Header. Invalid media type: Xyzzy I am not a mime. Acceptable types: */* application/json', json_dict['error']['msg'])
1535
1536 # test 5 accept mimetype is ok, param is not
1537 headers={"accept": "*/*; foo",
1538 "content-type": env['CONTENT_TYPE'],
1539 "content-length": env['CONTENT_LENGTH'],
1540 }
1541
1542 self.headers=headers
1543 # we need to generate a FieldStorage the looks like
1544 # FieldStorage(None, None, 'string') rather than
1545 # FieldStorage(None, None, [])
1546 body_file=BytesIO(body) # FieldStorage needs a file
1547 form = client.BinaryFieldStorage(body_file,
1548 headers=headers,
1549 environ=env)
1550 self.server.client.request.headers.get=self.get_header
1551 results = self.server.dispatch(env["REQUEST_METHOD"],
1552 "/rest/data/issue",
1553 form)
1554
1555 print(results)
1556 self.assertEqual(self.server.client.response_code, 406)
1557 json_dict = json.loads(b2s(results))
1558 self.assertIn('Unable to parse Accept Header. Invalid param: foo. Acceptable types: */* application/json', json_dict['error']['msg'])
1559
1325 def testStatsGen(self): 1560 def testStatsGen(self):
1326 # check stats being returned by put and get ops 1561 # check stats being returned by put and get ops
1327 # using dispatch which parses the @stats query param 1562 # using dispatch which parses the @stats query param
1328 1563
1329 # find correct py2/py3 list comparison ignoring order 1564 # find correct py2/py3 list comparison ignoring order
2032 self.assertEqual(self.server.client.response_code, 200) 2267 self.assertEqual(self.server.client.response_code, 200)
2033 self.assertEqual(self.server.client.additional_headers['Content-Type'], 2268 self.assertEqual(self.server.client.additional_headers['Content-Type'],
2034 "application/xml") 2269 "application/xml")
2035 ''' 2270 '''
2036 2271
2272 # TEST #8
2273 # invalid api version
2274 # application/json is selected with invalid version
2275 self.server.client.request.headers.get=self.get_header
2276 headers={"accept": "application/json; version=99"
2277 }
2278 self.headers=headers
2279 with self.assertRaises(UsageError) as ctx:
2280 results = self.server.dispatch('GET',
2281 "/rest/data/status/1",
2282 self.empty_form)
2283 print(results)
2284 self.assertEqual(self.server.client.response_code, 200)
2285 self.assertEqual(ctx.exception.args[0],
2286 "Unrecognized version: 99. See /rest without "
2287 "specifying version for supported versions.")
2288
2037 def testMethodOverride(self): 2289 def testMethodOverride(self):
2038 # TEST #1 2290 # TEST #1
2039 # Use GET, PUT, PATCH to tunnel DELETE expect error 2291 # Use GET, PUT, PATCH to tunnel DELETE expect error
2040 2292
2041 body=b'{ "order": 5 }' 2293 body=b'{ "order": 5 }'
2516 {'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/file1/'}) 2768 {'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/file1/'})
2517 2769
2518 # File content is only shown with verbose=3 2770 # File content is only shown with verbose=3
2519 form = cgi.FieldStorage() 2771 form = cgi.FieldStorage()
2520 form.list = [ 2772 form.list = [
2521 cgi.MiniFieldStorage('@verbose', '3') 2773 cgi.MiniFieldStorage('@verbose', '3'),
2774 cgi.MiniFieldStorage('@protected', 'true')
2522 ] 2775 ]
2523 results = self.server.get_element('file', fileid, form) 2776 results = self.server.get_element('file', fileid, form)
2524 results = results['data'] 2777 results = results['data']
2525 self.assertEqual(self.dummy_client.response_code, 200) 2778 self.assertEqual(self.dummy_client.response_code, 200)
2526 self.assertEqual(results['attributes']['content'], 'hello\r\nthere') 2779 self.assertEqual(results['attributes']['content'], 'hello\r\nthere')
2780 self.assertIn('creator', results['attributes']) # added by @protected
2781 self.assertEqual(results['attributes']['creator']['username'], "joe")
2527 2782
2528 def testAuthDeniedPut(self): 2783 def testAuthDeniedPut(self):
2529 """ 2784 """
2530 Test unauthorized PUT request 2785 Test unauthorized PUT request
2531 """ 2786 """
3020 self.assertEqual(self.dummy_client.response_code, 200) 3275 self.assertEqual(self.dummy_client.response_code, 200)
3021 3276
3022 # verify the result 3277 # verify the result
3023 self.assertTrue(not self.db.issue.is_retired(issue_id)) 3278 self.assertTrue(not self.db.issue.is_retired(issue_id))
3024 3279
3280 def testPatchBadAction(self):
3281 """
3282 Test Patch Action 'Unknown'
3283 """
3284 # create a new issue with userid 1 and 2 in the nosy list
3285 issue_id = self.db.issue.create(title='foo')
3286
3287 # execute action retire
3288 form = cgi.FieldStorage()
3289 etag = calculate_etag(self.db.issue.getnode(issue_id),
3290 self.db.config['WEB_SECRET_KEY'])
3291 form.list = [
3292 cgi.MiniFieldStorage('@op', 'action'),
3293 cgi.MiniFieldStorage('@action_name', 'unknown'),
3294 cgi.MiniFieldStorage('@etag', etag)
3295 ]
3296 results = self.server.patch_element('issue', issue_id, form)
3297 self.assertEqual(self.dummy_client.response_code, 400)
3298 # verify the result, note order of allowed elements changes
3299 # for python2/3 so just check prefix.
3300 self.assertIn('action "unknown" is not supported, allowed: ',
3301 results['error']['msg'].args[0])
3302
3025 def testPatchRemove(self): 3303 def testPatchRemove(self):
3026 """ 3304 """
3027 Test Patch Action 'Remove' only some element from a list 3305 Test Patch Action 'Remove' only some element from a list
3028 """ 3306 """
3029 # create a new issue with userid 1, 2, 3 in the nosy list 3307 # create a new issue with userid 1, 2, 3 in the nosy list

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