comparison test/rest_common.py @ 7587:8f29e4ea05ce

fix: issue2551278 - datetime.datetime.utcnow deprecation. Replace calls with equivalent that produces timezone aware dates rather than naive dates. Also some flake8 fixes for test/rest_common.py.
author John Rouillard <rouilj@ieee.org>
date Tue, 25 Jul 2023 16:30:10 -0400
parents 978285986b2c
children be6cb2e0d471
comparison
equal deleted inserted replaced
7586:859c57bc8d91 7587:8f29e4ea05ce
1 import pytest 1 import pytest
2 import unittest 2 import unittest
3 import os
4 import shutil 3 import shutil
5 import errno 4 import errno
6 5
7 from time import sleep 6 from time import sleep
8 from datetime import datetime, timedelta 7 from datetime import datetime, timedelta
8 from roundup.anypy.cgi_ import cgi
9 from roundup.anypy.datetime_ import utcnow
9 from roundup.test.tx_Source_detector import init as tx_Source_init 10 from roundup.test.tx_Source_detector import init as tx_Source_init
10 from roundup.anypy.cgi_ import cgi 11
11 12
12 try: 13 try:
13 from datetime import timezone 14 from datetime import timezone
14 myutc = timezone.utc 15 myutc = timezone.utc
15 except ImportError: 16 except ImportError:
16 # python 2 17 # python 2
17 from datetime import tzinfo 18 from datetime import tzinfo
18 ZERO = timedelta(0) 19 ZERO = timedelta(0)
20
19 class UTC(tzinfo): 21 class UTC(tzinfo):
20 """UTC""" 22 """UTC"""
21 def utcoffset(self, dt): 23 def utcoffset(self, dt):
22 return ZERO 24 return ZERO
23 25
24 def tzname(self, dt): 26 def tzname(self, dt):
25 return "UTC" 27 return "UTC"
26 28
27 def dst(self, dt): 29 def dst(self, dt):
28 return ZERO 30 return ZERO
29 31
30 myutc = UTC() 32 myutc = UTC()
31 33
32 from roundup.cgi.exceptions import * 34 from roundup.cgi.exceptions import *
33 from roundup.hyperdb import HyperdbValueError 35 from roundup.hyperdb import HyperdbValueError
34 from roundup.exceptions import * 36 from roundup.exceptions import *
35 from roundup import password, hyperdb 37 from roundup import password, hyperdb
36 from roundup.rest import RestfulInstance, calculate_etag 38 from roundup.rest import RestfulInstance, calculate_etag
37 from roundup.backends import list_backends
38 from roundup.cgi import client 39 from roundup.cgi import client
39 from roundup.anypy.strings import b2s, s2b, us2u 40 from roundup.anypy.strings import b2s, s2b, us2u
40 import random 41 import random
41 42
42 from roundup.backends.sessions_dbm import OneTimeKeys 43 from roundup.backends.sessions_dbm import OneTimeKeys
43 from roundup.anypy.dbm_ import anydbm, whichdb 44 from roundup.anypy.dbm_ import whichdb
44 45
45 from .db_test_base import setupTracker 46 from .db_test_base import setupTracker
46 47
47 from roundup.test.mocknull import MockNull 48 from roundup.test.mocknull import MockNull
48 49
54 try: 55 try:
55 import jwt 56 import jwt
56 skip_jwt = lambda func, *args, **kwargs: func 57 skip_jwt = lambda func, *args, **kwargs: func
57 except ImportError: 58 except ImportError:
58 from .pytest_patcher import mark_class 59 from .pytest_patcher import mark_class
59 jwt=None 60 jwt = None
60 skip_jwt = mark_class(pytest.mark.skip( 61 skip_jwt = mark_class(pytest.mark.skip(
61 reason='Skipping JWT tests: jwt library not available')) 62 reason='Skipping JWT tests: jwt library not available'))
62 63
63 NEEDS_INSTANCE = 1 64 NEEDS_INSTANCE = 1
64 65
65 66
66 class TestCase(): 67 class TestCase():
67 68
107 p = self.db.security.addPermission(name='Retire', klass='issue') 108 p = self.db.security.addPermission(name='Retire', klass='issue')
108 self.db.security.addPermissionToRole('User', p) 109 self.db.security.addPermissionToRole('User', p)
109 110
110 # add set of roles for testing jwt's. 111 # add set of roles for testing jwt's.
111 self.db.security.addRole(name="User:email", 112 self.db.security.addRole(name="User:email",
112 description="allow email by jwt") 113 description="allow email by jwt")
113 # allow the jwt to access everybody's email addresses. 114 # allow the jwt to access everybody's email addresses.
114 # this makes it easier to differentiate between User and 115 # this makes it easier to differentiate between User and
115 # User:email roles by accessing the /rest/data/user 116 # User:email roles by accessing the /rest/data/user
116 # endpoint 117 # endpoint
117 jwt_perms = self.db.security.addPermission(name='View', 118 jwt_perms = self.db.security.addPermission(
118 klass='user', 119 name='View',
119 properties=('id', 'realname', 'address', 'username'), 120 klass='user',
120 description="Allow jwt access to email", 121 properties=('id', 'realname', 'address', 'username'),
121 props_only=False) 122 description="Allow jwt access to email",
123 props_only=False)
122 self.db.security.addPermissionToRole("User:email", jwt_perms) 124 self.db.security.addPermissionToRole("User:email", jwt_perms)
123 self.db.security.addPermissionToRole("User:email", "Rest Access") 125 self.db.security.addPermissionToRole("User:email", "Rest Access")
124 126
125 # add set of roles for testing jwt's. 127 # add set of roles for testing jwt's.
126 # this is like the user:email role, but it missing access to the rest endpoint. 128 # this is like the user:email role, but it missing access to the rest endpoint.
127 self.db.security.addRole(name="User:emailnorest", 129 self.db.security.addRole(name="User:emailnorest",
128 description="allow email by jwt") 130 description="allow email by jwt")
129 jwt_perms = self.db.security.addPermission(name='View', 131 jwt_perms = self.db.security.addPermission(
130 klass='user', 132 name='View',
131 properties=('id', 'realname', 'address', 'username'), 133 klass='user',
132 description="Allow jwt access to email but forget to allow rest", 134 properties=('id', 'realname', 'address', 'username'),
133 props_only=False) 135 description="Allow jwt access to email but forget to allow rest",
136 props_only=False)
134 self.db.security.addPermissionToRole("User:emailnorest", jwt_perms) 137 self.db.security.addPermissionToRole("User:emailnorest", jwt_perms)
135
136 138
137 if jwt: 139 if jwt:
138 # must be 32 chars in length minimum (I think this is at least 140 # must be 32 chars in length minimum (I think this is at least
139 # 256 bits of data) 141 # 256 bits of data)
140 142
141 secret = "TestingTheJwtSecretTestingTheJwtSecret" 143 secret = "TestingTheJwtSecretTestingTheJwtSecret"
142 self.db.config['WEB_JWT_SECRET'] = secret 144 self.db.config['WEB_JWT_SECRET'] = secret
143 145
144 # generate all timestamps in UTC. 146 # generate all timestamps in UTC.
145 base_datetime = datetime(1970,1,1, tzinfo=myutc) 147 base_datetime = datetime(1970, 1, 1, tzinfo=myutc)
146 148
147 # A UTC timestamp for now. 149 # A UTC timestamp for now.
148 dt = datetime.now(myutc) 150 dt = datetime.now(myutc)
149 now_ts = int((dt - base_datetime).total_seconds()) 151 now_ts = int((dt - base_datetime).total_seconds())
150 152
156 dt = dt - timedelta(seconds=120) 158 dt = dt - timedelta(seconds=120)
157 expired_ts = int((dt - base_datetime).total_seconds()) 159 expired_ts = int((dt - base_datetime).total_seconds())
158 160
159 # claims match what cgi/client.py::determine_user 161 # claims match what cgi/client.py::determine_user
160 # is looking for 162 # is looking for
161 claim= { 'sub': self.db.getuid(), 163 claim = {'sub': self.db.getuid(),
162 'iss': self.db.config.TRACKER_WEB, 164 'iss': self.db.config.TRACKER_WEB,
163 'aud': self.db.config.TRACKER_WEB, 165 'aud': self.db.config.TRACKER_WEB,
164 'roles': [ 'User' ], 166 'roles': ['User'],
165 'iat': now_ts, 167 'iat': now_ts,
166 'exp': plus1min_ts, 168 'exp': plus1min_ts}
167 }
168 169
169 # in version 2.0.0 and newer jwt.encode returns string 170 # in version 2.0.0 and newer jwt.encode returns string
170 # not bytestring. So we have to skip b2s conversion 171 # not bytestring. So we have to skip b2s conversion
171 172
172 if version.parse(jwt.__version__) >= version.parse('2.0.0'): 173 if version.parse(jwt.__version__) >= version.parse('2.0.0'):
173 tostr = lambda x: x 174 tostr = lambda x: x
174 else: 175 else:
175 tostr = b2s 176 tostr = b2s
176 177
177 self.jwt = {} 178 self.jwt = {}
178 self.claim = {} 179 self.claim = {}
179 # generate invalid claim with expired timestamp 180 # generate invalid claim with expired timestamp
180 self.claim['expired'] = copy(claim) 181 self.claim['expired'] = copy(claim)
181 self.claim['expired']['exp'] = expired_ts 182 self.claim['expired']['exp'] = expired_ts
182 self.jwt['expired'] = tostr(jwt.encode(self.claim['expired'], secret, 183 self.jwt['expired'] = tostr(jwt.encode(
183 algorithm='HS256')) 184 self.claim['expired'], secret,
184 185 algorithm='HS256'))
186
185 # generate valid claim with user role 187 # generate valid claim with user role
186 self.claim['user'] = copy(claim) 188 self.claim['user'] = copy(claim)
187 self.claim['user']['exp'] = plus1min_ts 189 self.claim['user']['exp'] = plus1min_ts
188 self.jwt['user'] = tostr(jwt.encode(self.claim['user'], secret, 190 self.jwt['user'] = tostr(jwt.encode(
189 algorithm='HS256')) 191 self.claim['user'], secret,
192 algorithm='HS256'))
190 # generate invalid claim bad issuer 193 # generate invalid claim bad issuer
191 self.claim['badiss'] = copy(claim) 194 self.claim['badiss'] = copy(claim)
192 self.claim['badiss']['iss'] = "http://someissuer/bugs" 195 self.claim['badiss']['iss'] = "http://someissuer/bugs"
193 self.jwt['badiss'] = tostr(jwt.encode(self.claim['badiss'], secret, 196 self.jwt['badiss'] = tostr(jwt.encode(
194 algorithm='HS256')) 197 self.claim['badiss'], secret,
198 algorithm='HS256'))
195 # generate invalid claim bad aud(ience) 199 # generate invalid claim bad aud(ience)
196 self.claim['badaud'] = copy(claim) 200 self.claim['badaud'] = copy(claim)
197 self.claim['badaud']['aud'] = "http://someaudience/bugs" 201 self.claim['badaud']['aud'] = "http://someaudience/bugs"
198 self.jwt['badaud'] = tostr(jwt.encode(self.claim['badaud'], secret, 202 self.jwt['badaud'] = tostr(jwt.encode(
199 algorithm='HS256')) 203 self.claim['badaud'], secret,
204 algorithm='HS256'))
200 # generate invalid claim bad sub(ject) 205 # generate invalid claim bad sub(ject)
201 self.claim['badsub'] = copy(claim) 206 self.claim['badsub'] = copy(claim)
202 self.claim['badsub']['sub'] = str("99") 207 self.claim['badsub']['sub'] = str("99")
203 self.jwt['badsub'] = tostr(jwt.encode(self.claim['badsub'], secret, 208 self.jwt['badsub'] = tostr(
204 algorithm='HS256')) 209 jwt.encode(self.claim['badsub'], secret,
210 algorithm='HS256'))
205 # generate invalid claim bad roles 211 # generate invalid claim bad roles
206 self.claim['badroles'] = copy(claim) 212 self.claim['badroles'] = copy(claim)
207 self.claim['badroles']['roles'] = [ "badrole1", "badrole2" ] 213 self.claim['badroles']['roles'] = ["badrole1", "badrole2"]
208 self.jwt['badroles'] = tostr(jwt.encode(self.claim['badroles'], secret, 214 self.jwt['badroles'] = tostr(jwt.encode(
209 algorithm='HS256')) 215 self.claim['badroles'], secret,
216 algorithm='HS256'))
210 # generate valid claim with limited user:email role 217 # generate valid claim with limited user:email role
211 self.claim['user:email'] = copy(claim) 218 self.claim['user:email'] = copy(claim)
212 self.claim['user:email']['roles'] = [ "user:email" ] 219 self.claim['user:email']['roles'] = ["user:email"]
213 self.jwt['user:email'] = tostr(jwt.encode(self.claim['user:email'], secret, 220 self.jwt['user:email'] = tostr(jwt.encode(
214 algorithm='HS256')) 221 self.claim['user:email'], secret,
222 algorithm='HS256'))
215 223
216 # generate valid claim with limited user:emailnorest role 224 # generate valid claim with limited user:emailnorest role
217 self.claim['user:emailnorest'] = copy(claim) 225 self.claim['user:emailnorest'] = copy(claim)
218 self.claim['user:emailnorest']['roles'] = [ "user:emailnorest" ] 226 self.claim['user:emailnorest']['roles'] = ["user:emailnorest"]
219 self.jwt['user:emailnorest'] = tostr(jwt.encode(self.claim['user:emailnorest'], secret, 227 self.jwt['user:emailnorest'] = tostr(jwt.encode(
220 algorithm='HS256')) 228 self.claim['user:emailnorest'], secret,
229 algorithm='HS256'))
221 230
222 self.db.tx_Source = 'web' 231 self.db.tx_Source = 'web'
223 232
224 self.db.issue.addprop(tx_Source=hyperdb.String()) 233 self.db.issue.addprop(tx_Source=hyperdb.String())
225 self.db.issue.addprop(anint=hyperdb.Integer()) 234 self.db.issue.addprop(anint=hyperdb.Integer())
269 shutil.rmtree(self.dirname) 278 shutil.rmtree(self.dirname)
270 except OSError as error: 279 except OSError as error:
271 if error.errno not in (errno.ENOENT, errno.ESRCH): 280 if error.errno not in (errno.ENOENT, errno.ESRCH):
272 raise 281 raise
273 282
274 def get_header (self, header, not_found=None): 283 def get_header(self, header, not_found=None):
275 try: 284 try:
276 return self.headers[header.lower()] 285 return self.headers[header.lower()]
277 except (AttributeError, KeyError, TypeError): 286 except (AttributeError, KeyError, TypeError):
278 if header.upper() in self.client_env: 287 if header.upper() in self.client_env:
279 return self.client_env[header.upper()] 288 return self.client_env[header.upper()]
303 self.create_stati() 312 self.create_stati()
304 self.db.issue.create( 313 self.db.issue.create(
305 title='foo1', 314 title='foo1',
306 status=self.db.status.lookup('open'), 315 status=self.db.status.lookup('open'),
307 priority=self.db.priority.lookup('normal'), 316 priority=self.db.priority.lookup('normal'),
308 nosy = [ "1", "2" ] 317 nosy=["1", "2"]
309 ) 318 )
310 issue_open_norm = self.db.issue.create( 319 issue_open_norm = self.db.issue.create(
311 title='foo2', 320 title='foo2',
312 status=self.db.status.lookup('open'), 321 status=self.db.status.lookup('open'),
313 priority=self.db.priority.lookup('normal'), 322 priority=self.db.priority.lookup('normal'),
314 assignedto = "3" 323 assignedto="3"
315 ) 324 )
316 issue_open_crit = self.db.issue.create( 325 issue_open_crit = self.db.issue.create(
317 title='foo5', 326 title='foo5',
318 status=self.db.status.lookup('open'), 327 status=self.db.status.lookup('open'),
319 priority=self.db.priority.lookup('critical') 328 priority=self.db.priority.lookup('critical')
369 """ 378 """
370 Retrieve all issues with an 'o' in status 379 Retrieve all issues with an 'o' in status
371 sort by status.name (not order) 380 sort by status.name (not order)
372 """ 381 """
373 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/' 382 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/'
374 #self.maxDiff=None 383 # self.maxDiff=None
375 self.create_sampledata() 384 self.create_sampledata()
376 self.db.issue.set('2', status=self.db.status.lookup('closed')) 385 self.db.issue.set('2', status=self.db.status.lookup('closed'))
377 self.db.issue.set('3', status=self.db.status.lookup('chatting')) 386 self.db.issue.set('3', status=self.db.status.lookup('chatting'))
378 expected={'data': 387 expected = {'data':
379 {'@total_size': 2, 388 {'@total_size': 2,
380 'collection': [ 389 'collection': [
381 { 'id': '2', 390 {'id': '2',
382 'link': base_path + 'issue/2', 391 'link': base_path + 'issue/2',
383 'assignedto.issue': None, 392 'assignedto.issue': None,
384 'status': 393 'status':
385 { 'id': '10', 394 {'id': '10',
386 'link': base_path + 'status/10' 395 'link': base_path + 'status/10'
387 } 396 }
388 }, 397 },
389 { 'id': '1', 398 {'id': '1',
390 'link': base_path + 'issue/1', 399 'link': base_path + 'issue/1',
391 'assignedto.issue': None, 400 'assignedto.issue': None,
392 'status': 401 'status':
393 { 'id': '9', 402 {'id': '9',
394 'link': base_path + 'status/9' 403 'link': base_path + 'status/9'
395 } 404 }
396 }, 405 },
397 ]} 406 ]}
398 } 407 }
399 form = cgi.FieldStorage() 408 form = cgi.FieldStorage()
400 form.list = [ 409 form.list = [
401 cgi.MiniFieldStorage('status.name', 'o'), 410 cgi.MiniFieldStorage('status.name', 'o'),
402 cgi.MiniFieldStorage('@fields', 'status,assignedto.issue'), 411 cgi.MiniFieldStorage('@fields', 'status,assignedto.issue'),
403 cgi.MiniFieldStorage('@sort', 'status.name'), 412 cgi.MiniFieldStorage('@sort', 'status.name'),
409 """ 418 """
410 Mess up the names of various properties and make sure we get a 400 419 Mess up the names of various properties and make sure we get a 400
411 and a somewhat useful error message. 420 and a somewhat useful error message.
412 """ 421 """
413 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/' 422 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/'
414 #self.maxDiff=None 423 # self.maxDiff=None
415 self.create_sampledata() 424 self.create_sampledata()
416 self.db.issue.set('2', status=self.db.status.lookup('closed')) 425 self.db.issue.set('2', status=self.db.status.lookup('closed'))
417 self.db.issue.set('3', status=self.db.status.lookup('chatting')) 426 self.db.issue.set('3', status=self.db.status.lookup('chatting'))
418 expected = [ 427 expected = [
419 {'error': {'msg': KeyError('Unknown property: assignedto.isse',), 428 {'error': {'msg': KeyError('Unknown property: assignedto.isse',),
469 478
470 def testGetExactMatch(self): 479 def testGetExactMatch(self):
471 """ Retrieve all issues with an exact title 480 """ Retrieve all issues with an exact title
472 """ 481 """
473 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/' 482 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/'
474 #self.maxDiff=None 483 # self.maxDiff=None
475 self.create_sampledata() 484 self.create_sampledata()
476 self.db.issue.set('2', title='This is an exact match') 485 self.db.issue.set('2', title='This is an exact match')
477 self.db.issue.set('3', title='This is an exact match') 486 self.db.issue.set('3', title='This is an exact match')
478 self.db.issue.set('1', title='This is AN exact match') 487 self.db.issue.set('1', title='This is AN exact match')
479 expected={'data': 488 expected = {'data':
480 {'@total_size': 2, 489 {'@total_size': 2,
481 'collection': [ 490 'collection': [
482 { 'id': '2', 491 {'id': '2',
483 'link': base_path + 'issue/2', 492 'link': base_path + 'issue/2',
484 }, 493 },
485 { 'id': '3', 494 {'id': '3',
486 'link': base_path + 'issue/3', 495 'link': base_path + 'issue/3',
487 }, 496 },
488 ]} 497 ]}
489 } 498 }
490 form = cgi.FieldStorage() 499 form = cgi.FieldStorage()
491 form.list = [ 500 form.list = [
492 cgi.MiniFieldStorage('title:', 'This is an exact match'), 501 cgi.MiniFieldStorage('title:', 'This is an exact match'),
493 cgi.MiniFieldStorage('@sort', 'status.name'), 502 cgi.MiniFieldStorage('@sort', 'status.name'),
494 ] 503 ]
500 509
501 self.maxDiff = 4000 510 self.maxDiff = 4000
502 self.create_sampledata() 511 self.create_sampledata()
503 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/' 512 base_path = self.db.config['TRACKER_WEB'] + 'rest/data/issue/'
504 513
505
506 # Check formating for issues status=open; @fields and verbose tests 514 # Check formating for issues status=open; @fields and verbose tests
507 form = cgi.FieldStorage() 515 form = cgi.FieldStorage()
508 form.list = [ 516 form.list = [
509 cgi.MiniFieldStorage('status', 'open'), 517 cgi.MiniFieldStorage('status', 'open'),
510 cgi.MiniFieldStorage('@fields', 'nosy,status,creator'), 518 cgi.MiniFieldStorage('@fields', 'nosy,status,creator'),
511 cgi.MiniFieldStorage('@verbose', '2') 519 cgi.MiniFieldStorage('@verbose', '2')
512 ] 520 ]
513 521
514 expected={'data': 522 expected = {'data':
515 {'@total_size': 3, 523 {'@total_size': 3,
516 'collection': [ { 524 'collection': [ {
517 'creator': {'id': '3', 525 'creator': {'id': '3',
518 'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/user/3', 526 'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/user/3',
519 'username': 'joe'}, 527 'username': 'joe'},
520 'status': {'id': '9', 528 'status': {'id': '9',
521 'name': 'open', 529 'name': 'open',
522 'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/status/9'}, 530 'link': 'http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data/status/9'},
523 'id': '1', 531 'id': '1',
524 'nosy': [ 532 'nosy': [
525 {'username': 'admin', 533 {'username': 'admin',
1092 except AttributeError: 1100 except AttributeError:
1093 # if dir attribute doesn't exist the primary db is not 1101 # if dir attribute doesn't exist the primary db is not
1094 # sqlite or anydbm. So don't need to exercise code. 1102 # sqlite or anydbm. So don't need to exercise code.
1095 pass 1103 pass
1096 1104
1097 start_time = datetime.utcnow() 1105 start_time = utcnow()
1098 # don't set an accept header; json should be the default 1106 # don't set an accept header; json should be the default
1099 # use up all our allowed api calls 1107 # use up all our allowed api calls
1100 for i in range(calls_per_interval): 1108 for i in range(calls_per_interval):
1101 # i is 0 ... calls_per_interval 1109 # i is 0 ... calls_per_interval
1102 self.client_error_message = [] 1110 self.client_error_message = []
1103 self.server.client.env.update({'REQUEST_METHOD': 'GET'}) 1111 self.server.client.env.update({'REQUEST_METHOD': 'GET'})
1104 results = self.server.dispatch('GET', 1112 results = self.server.dispatch('GET',
1105 "/rest/data/user/%s/realname"%self.joeid, 1113 "/rest/data/user/%s/realname"%self.joeid,
1106 self.empty_form) 1114 self.empty_form)
1107 1115
1108 loop_time = datetime.utcnow() 1116 loop_time = utcnow()
1109 self.assertLess((loop_time-start_time).total_seconds(), 1117 self.assertLess((loop_time-start_time).total_seconds(),
1110 int(wait_time_str), 1118 int(wait_time_str),
1111 "Test system is too slow to complete test as configured") 1119 "Test system is too slow to complete test as configured")
1112 1120
1113 # is successful 1121 # is successful
1146 self.assertEqual( 1154 self.assertEqual(
1147 str(self.server.client.additional_headers["Retry-After"]), 1155 str(self.server.client.additional_headers["Retry-After"]),
1148 wait_time_str) # check as string 1156 wait_time_str) # check as string
1149 1157
1150 print("Reset:", self.server.client.additional_headers["X-RateLimit-Reset"]) 1158 print("Reset:", self.server.client.additional_headers["X-RateLimit-Reset"])
1151 print("Now realtime pre-sleep:", datetime.utcnow()) 1159 print("Now realtime pre-sleep:", utcnow())
1152 # sleep as requested so we can do another login 1160 # sleep as requested so we can do another login
1153 sleep(float(wait_time_str) + 0.1) 1161 sleep(float(wait_time_str) + 0.1)
1154 print("Now realtime post-sleep:", datetime.utcnow()) 1162 print("Now realtime post-sleep:", utcnow())
1155 1163
1156 # this should succeed 1164 # this should succeed
1157 self.server.client.additional_headers.clear() 1165 self.server.client.additional_headers.clear()
1158 results = self.server.dispatch('GET', 1166 results = self.server.dispatch('GET',
1159 "/rest/data/user/%s/realname"%self.joeid, 1167 "/rest/data/user/%s/realname"%self.joeid,
1160 self.empty_form) 1168 self.empty_form)
1161 print(results) 1169 print(results)
1162 print("Reset:", self.server.client.additional_headers["X-RateLimit-Reset-date"]) 1170 print("Reset:", self.server.client.additional_headers["X-RateLimit-Reset-date"])
1163 print("Now realtime:", datetime.utcnow()) 1171 print("Now realtime:", utcnow())
1164 print("Now ts header:", self.server.client.additional_headers["Now"]) 1172 print("Now ts header:", self.server.client.additional_headers["Now"])
1165 print("Now date header:", self.server.client.additional_headers["Now-date"]) 1173 print("Now date header:", self.server.client.additional_headers["Now-date"])
1166 1174
1167 self.assertEqual(self.server.client.response_code, 200) 1175 self.assertEqual(self.server.client.response_code, 200)
1168 1176

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