Merge lp:~alecu/ubuntu-sso-client/timestamp-autofix into lp:ubuntu-sso-client
- timestamp-autofix
- Merge into trunk
Proposed by
Alejandro J. Cura
Status: | Merged |
---|---|
Approved by: | Alejandro J. Cura |
Approved revision: | 796 |
Merged at revision: | 802 |
Proposed branch: | lp:~alecu/ubuntu-sso-client/timestamp-autofix |
Merge into: | lp:ubuntu-sso-client |
Diff against target: |
469 lines (+310/-17) 5 files modified
ubuntu_sso/account.py (+32/-6) ubuntu_sso/tests/test_account.py (+59/-7) ubuntu_sso/tests/test_credentials.py (+3/-0) ubuntu_sso/utils/__init__.py (+64/-1) ubuntu_sso/utils/tests/test_oauth_headers.py (+152/-3) |
To merge this branch: | bzr merge lp:~alecu/ubuntu-sso-client/timestamp-autofix |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Diego Sarmentero (community) | Approve | ||
Review via email: mp+78507@code.launchpad.net |
Commit message
Do a HEAD request on the server to get accurate timestamp (LP: #692597)
Description of the change
Do a HEAD request on the server to get accurate timestamp (LP: #692597)
To post a comment you must log in.
- 796. By Alejandro J. Cura
-
fixing punctuation.
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Looks good, I have the same concerns I pointed out in the storage-protocol branch.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'ubuntu_sso/account.py' | |||
2 | --- ubuntu_sso/account.py 2011-03-16 01:36:07 +0000 | |||
3 | +++ ubuntu_sso/account.py 2011-10-06 21:50:30 +0000 | |||
4 | @@ -1,6 +1,7 @@ | |||
5 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
6 | 2 | 2 | ||
7 | 3 | # Author: Natalia Bidart <natalia.bidart@canonical.com> | 3 | # Author: Natalia Bidart <natalia.bidart@canonical.com> |
8 | 4 | # Author: Alejandro J. Cura <alecu@canonical.com> | ||
9 | 4 | # | 5 | # |
10 | 5 | # Copyright 2010 Canonical Ltd. | 6 | # Copyright 2010 Canonical Ltd. |
11 | 6 | # | 7 | # |
12 | @@ -31,6 +32,7 @@ | |||
13 | 31 | from oauth import oauth | 32 | from oauth import oauth |
14 | 32 | 33 | ||
15 | 33 | from ubuntu_sso.logger import setup_logging | 34 | from ubuntu_sso.logger import setup_logging |
16 | 35 | from ubuntu_sso.utils import timestamp_checker | ||
17 | 34 | 36 | ||
18 | 35 | 37 | ||
19 | 36 | logger = setup_logging("ubuntu_sso.account") | 38 | logger = setup_logging("ubuntu_sso.account") |
20 | @@ -39,6 +41,27 @@ | |||
21 | 39 | SSO_STATUS_ERROR = 'error' | 41 | SSO_STATUS_ERROR = 'error' |
22 | 40 | 42 | ||
23 | 41 | 43 | ||
24 | 44 | class TimestampedAuthorizer(OAuthAuthorizer): | ||
25 | 45 | """Includes a custom timestamp on OAuth signatures.""" | ||
26 | 46 | |||
27 | 47 | def __init__(self, get_timestamp, *args, **kwargs): | ||
28 | 48 | """Store the get_timestamp method, and move on.""" | ||
29 | 49 | super(TimestampedAuthorizer, self).__init__(*args, **kwargs) | ||
30 | 50 | self.get_timestamp = get_timestamp | ||
31 | 51 | |||
32 | 52 | # pylint: disable=C0103 | ||
33 | 53 | def authorizeRequest(self, absolute_uri, method, body, headers): | ||
34 | 54 | """Override authorizeRequest including the timestamp.""" | ||
35 | 55 | parameters = {"oauth_timestamp": self.get_timestamp()} | ||
36 | 56 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | ||
37 | 57 | self.consumer, self.access_token, http_url=absolute_uri, | ||
38 | 58 | parameters=parameters) | ||
39 | 59 | oauth_request.sign_request( | ||
40 | 60 | oauth.OAuthSignatureMethod_PLAINTEXT(), | ||
41 | 61 | self.consumer, self.access_token) | ||
42 | 62 | headers.update(oauth_request.to_header(self.oauth_realm)) | ||
43 | 63 | |||
44 | 64 | |||
45 | 42 | class InvalidEmailError(Exception): | 65 | class InvalidEmailError(Exception): |
46 | 43 | """The email is not valid.""" | 66 | """The email is not valid.""" |
47 | 44 | 67 | ||
48 | @@ -181,9 +204,11 @@ | |||
49 | 181 | if sso_service is None: | 204 | if sso_service is None: |
50 | 182 | oauth_token = oauth.OAuthToken(token['token'], | 205 | oauth_token = oauth.OAuthToken(token['token'], |
51 | 183 | token['token_secret']) | 206 | token['token_secret']) |
55 | 184 | authorizer = OAuthAuthorizer(token['consumer_key'], | 207 | authorizer = TimestampedAuthorizer( |
56 | 185 | token['consumer_secret'], | 208 | timestamp_checker.get_faithful_time, |
57 | 186 | oauth_token) | 209 | token['consumer_key'], |
58 | 210 | token['consumer_secret'], | ||
59 | 211 | oauth_token) | ||
60 | 187 | sso_service = self.sso_service_class(authorizer, self.service_url) | 212 | sso_service = self.sso_service_class(authorizer, self.service_url) |
61 | 188 | 213 | ||
62 | 189 | me_info = sso_service.accounts.me() | 214 | me_info = sso_service.accounts.me() |
63 | @@ -203,9 +228,10 @@ | |||
64 | 203 | token_name=token_name) | 228 | token_name=token_name) |
65 | 204 | 229 | ||
66 | 205 | oauth_token = oauth.OAuthToken(token['token'], token['token_secret']) | 230 | oauth_token = oauth.OAuthToken(token['token'], token['token_secret']) |
70 | 206 | authorizer = OAuthAuthorizer(token['consumer_key'], | 231 | authorizer = TimestampedAuthorizer(timestamp_checker.get_faithful_time, |
71 | 207 | token['consumer_secret'], | 232 | token['consumer_key'], |
72 | 208 | oauth_token) | 233 | token['consumer_secret'], |
73 | 234 | oauth_token) | ||
74 | 209 | sso_service = self.sso_service_class(authorizer, self.service_url) | 235 | sso_service = self.sso_service_class(authorizer, self.service_url) |
75 | 210 | result = sso_service.accounts.validate_email(email_token=email_token) | 236 | result = sso_service.accounts.validate_email(email_token=email_token) |
76 | 211 | logger.info('validate_email: email: %r result: %r', email, result) | 237 | logger.info('validate_email: email: %r result: %r', email, result) |
77 | 212 | 238 | ||
78 | === modified file 'ubuntu_sso/tests/test_account.py' | |||
79 | --- ubuntu_sso/tests/test_account.py 2011-09-02 13:05:06 +0000 | |||
80 | +++ ubuntu_sso/tests/test_account.py 2011-10-06 21:50:30 +0000 | |||
81 | @@ -1,6 +1,7 @@ | |||
82 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
83 | 2 | # | 2 | # |
84 | 3 | # Author: Natalia Bidart <natalia.bidart@canonical.com> | 3 | # Author: Natalia Bidart <natalia.bidart@canonical.com> |
85 | 4 | # Author: Alejandro J. Cura <alecu@canonical.com> | ||
86 | 4 | # | 5 | # |
87 | 5 | # Copyright 2010 Canonical Ltd. | 6 | # Copyright 2010 Canonical Ltd. |
88 | 6 | # | 7 | # |
89 | @@ -24,15 +25,36 @@ | |||
90 | 24 | # pylint: disable=F0401 | 25 | # pylint: disable=F0401 |
91 | 25 | from lazr.restfulclient.errors import HTTPError | 26 | from lazr.restfulclient.errors import HTTPError |
92 | 26 | # pylint: enable=F0401 | 27 | # pylint: enable=F0401 |
93 | 28 | from oauth import oauth | ||
94 | 27 | from twisted.trial.unittest import TestCase | 29 | from twisted.trial.unittest import TestCase |
95 | 28 | 30 | ||
103 | 29 | from ubuntu_sso.account import (Account, AuthenticationError, EmailTokenError, | 31 | from ubuntu_sso.account import ( |
104 | 30 | InvalidEmailError, InvalidPasswordError, NewPasswordError, SERVICE_URL, | 32 | Account, |
105 | 31 | RegistrationError, ResetPasswordTokenError, | 33 | AuthenticationError, |
106 | 32 | SSO_STATUS_OK, SSO_STATUS_ERROR) | 34 | EmailTokenError, |
107 | 33 | from ubuntu_sso.tests import (APP_NAME, CAPTCHA_ID, CAPTCHA_PATH, | 35 | InvalidEmailError, |
108 | 34 | CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN, | 36 | InvalidPasswordError, |
109 | 35 | TOKEN, TOKEN_NAME) | 37 | NewPasswordError, |
110 | 38 | SERVICE_URL, | ||
111 | 39 | RegistrationError, | ||
112 | 40 | ResetPasswordTokenError, | ||
113 | 41 | SSO_STATUS_OK, | ||
114 | 42 | SSO_STATUS_ERROR, | ||
115 | 43 | TimestampedAuthorizer, | ||
116 | 44 | ) | ||
117 | 45 | from ubuntu_sso.tests import ( | ||
118 | 46 | APP_NAME, | ||
119 | 47 | CAPTCHA_ID, | ||
120 | 48 | CAPTCHA_PATH, | ||
121 | 49 | CAPTCHA_SOLUTION, | ||
122 | 50 | EMAIL, | ||
123 | 51 | EMAIL_TOKEN, | ||
124 | 52 | NAME, | ||
125 | 53 | PASSWORD, | ||
126 | 54 | RESET_PASSWORD_TOKEN, | ||
127 | 55 | TOKEN, | ||
128 | 56 | TOKEN_NAME, | ||
129 | 57 | ) | ||
130 | 36 | 58 | ||
131 | 37 | 59 | ||
132 | 38 | CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \ | 60 | CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \ |
133 | @@ -145,6 +167,36 @@ | |||
134 | 145 | self.accounts = FakedAccounts() | 167 | self.accounts = FakedAccounts() |
135 | 146 | 168 | ||
136 | 147 | 169 | ||
137 | 170 | class TimestampedAuthorizerTestCase(TestCase): | ||
138 | 171 | """Test suite for the TimestampedAuthorizer.""" | ||
139 | 172 | |||
140 | 173 | def test_authorize_request_includes_timestamp(self): | ||
141 | 174 | """The authorizeRequest method includes the timestamp.""" | ||
142 | 175 | fromcandt_call = [] | ||
143 | 176 | fake_uri = "http://protocultura.net" | ||
144 | 177 | fake_timestamp = 1234 | ||
145 | 178 | get_fake_timestamp = lambda: fake_timestamp | ||
146 | 179 | original_oauthrequest = oauth.OAuthRequest | ||
147 | 180 | |||
148 | 181 | class FakeOAuthRequest(oauth.OAuthRequest): | ||
149 | 182 | """A Fake OAuthRequest class.""" | ||
150 | 183 | |||
151 | 184 | @staticmethod | ||
152 | 185 | def from_consumer_and_token(*args, **kwargs): | ||
153 | 186 | """A fake from_consumer_and_token.""" | ||
154 | 187 | fromcandt_call.append((args, kwargs)) | ||
155 | 188 | builder = original_oauthrequest.from_consumer_and_token | ||
156 | 189 | return builder(*args, **kwargs) | ||
157 | 190 | |||
158 | 191 | self.patch(oauth, "OAuthRequest", FakeOAuthRequest) | ||
159 | 192 | |||
160 | 193 | authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone") | ||
161 | 194 | authorizer.authorizeRequest(fake_uri, "POST", None, {}) | ||
162 | 195 | call_kwargs = fromcandt_call[0][1] | ||
163 | 196 | parameters = call_kwargs["parameters"] | ||
164 | 197 | self.assertEqual(parameters["oauth_timestamp"], fake_timestamp) | ||
165 | 198 | |||
166 | 199 | |||
167 | 148 | class AccountTestCase(TestCase): | 200 | class AccountTestCase(TestCase): |
168 | 149 | """Test suite for the SSO login processor.""" | 201 | """Test suite for the SSO login processor.""" |
169 | 150 | 202 | ||
170 | 151 | 203 | ||
171 | === modified file 'ubuntu_sso/tests/test_credentials.py' | |||
172 | --- ubuntu_sso/tests/test_credentials.py 2011-08-22 16:21:36 +0000 | |||
173 | +++ ubuntu_sso/tests/test_credentials.py 2011-10-06 21:50:30 +0000 | |||
174 | @@ -20,6 +20,7 @@ | |||
175 | 20 | 20 | ||
176 | 21 | import logging | 21 | import logging |
177 | 22 | import os | 22 | import os |
178 | 23 | import time | ||
179 | 23 | import urllib2 | 24 | import urllib2 |
180 | 24 | 25 | ||
181 | 25 | from twisted.internet import defer | 26 | from twisted.internet import defer |
182 | @@ -373,6 +374,8 @@ | |||
183 | 373 | return response | 374 | return response |
184 | 374 | 375 | ||
185 | 375 | self.patch(credentials.urllib2, 'urlopen', faked_urlopen) | 376 | self.patch(credentials.urllib2, 'urlopen', faked_urlopen) |
186 | 377 | self.patch(credentials.utils.timestamp_checker, "get_faithful_time", | ||
187 | 378 | time.time) | ||
188 | 376 | 379 | ||
189 | 377 | @defer.inlineCallbacks | 380 | @defer.inlineCallbacks |
190 | 378 | def test_ping_url_if_url_is_none(self): | 381 | def test_ping_url_if_url_is_none(self): |
191 | 379 | 382 | ||
192 | === modified file 'ubuntu_sso/utils/__init__.py' | |||
193 | --- ubuntu_sso/utils/__init__.py 2011-08-12 12:54:36 +0000 | |||
194 | +++ ubuntu_sso/utils/__init__.py 2011-10-06 21:50:30 +0000 | |||
195 | @@ -2,7 +2,7 @@ | |||
196 | 2 | 2 | ||
197 | 3 | # Author: Alejandro J. Cura <alecu@canonical.com> | 3 | # Author: Alejandro J. Cura <alecu@canonical.com> |
198 | 4 | # | 4 | # |
200 | 5 | # Copyright 2010 Canonical Ltd. | 5 | # Copyright 2010, 2011 Canonical Ltd. |
201 | 6 | # | 6 | # |
202 | 7 | # This program is free software: you can redistribute it and/or modify it | 7 | # This program is free software: you can redistribute it and/or modify it |
203 | 8 | # under the terms of the GNU General Public License version 3, as published | 8 | # under the terms of the GNU General Public License version 3, as published |
204 | @@ -19,9 +19,71 @@ | |||
205 | 19 | """Utility modules that may find use outside ubuntu_sso.""" | 19 | """Utility modules that may find use outside ubuntu_sso.""" |
206 | 20 | 20 | ||
207 | 21 | import cgi | 21 | import cgi |
208 | 22 | import time | ||
209 | 23 | import urllib2 | ||
210 | 22 | 24 | ||
211 | 23 | from oauth import oauth | 25 | from oauth import oauth |
212 | 24 | from urlparse import urlparse | 26 | from urlparse import urlparse |
213 | 27 | from twisted.web import http | ||
214 | 28 | |||
215 | 29 | from ubuntu_sso.logger import setup_logging | ||
216 | 30 | logger = setup_logging("ubuntu_sso.utils") | ||
217 | 31 | |||
218 | 32 | |||
219 | 33 | class RequestHead(urllib2.Request): | ||
220 | 34 | """A request with the method set to HEAD.""" | ||
221 | 35 | |||
222 | 36 | _request_method = "HEAD" | ||
223 | 37 | |||
224 | 38 | def get_method(self): | ||
225 | 39 | """Return the desired method.""" | ||
226 | 40 | return self._request_method | ||
227 | 41 | |||
228 | 42 | |||
229 | 43 | class SyncTimestampChecker(object): | ||
230 | 44 | """A timestamp that's regularly checked with a server.""" | ||
231 | 45 | |||
232 | 46 | CHECKING_INTERVAL = 60 * 60 # in seconds | ||
233 | 47 | ERROR_INTERVAL = 30 # in seconds | ||
234 | 48 | SERVER_URL = "http://one.ubuntu.com/" | ||
235 | 49 | |||
236 | 50 | def __init__(self): | ||
237 | 51 | """Initialize this instance.""" | ||
238 | 52 | self.next_check = time.time() | ||
239 | 53 | self.skew = 0 | ||
240 | 54 | |||
241 | 55 | def get_server_time(self): | ||
242 | 56 | """Get the time at the server.""" | ||
243 | 57 | headers = {"Cache-Control": "no-cache"} | ||
244 | 58 | request = RequestHead(self.SERVER_URL, headers=headers) | ||
245 | 59 | response = urllib2.urlopen(request) | ||
246 | 60 | date_string = response.info()["Date"] | ||
247 | 61 | timestamp = http.stringToDatetime(date_string) | ||
248 | 62 | return timestamp | ||
249 | 63 | |||
250 | 64 | def get_faithful_time(self): | ||
251 | 65 | """Get an accurate timestamp.""" | ||
252 | 66 | local_time = time.time() | ||
253 | 67 | if local_time >= self.next_check: | ||
254 | 68 | try: | ||
255 | 69 | server_time = self.get_server_time() | ||
256 | 70 | self.next_check = local_time + self.CHECKING_INTERVAL | ||
257 | 71 | self.skew = server_time - local_time | ||
258 | 72 | logger.debug("Calculated server-local time skew: %r", | ||
259 | 73 | self.skew) | ||
260 | 74 | #pylint: disable=W0703 | ||
261 | 75 | except Exception, server_error: | ||
262 | 76 | logger.debug("Error while verifying the server time skew: %r", | ||
263 | 77 | server_error) | ||
264 | 78 | self.next_check = local_time + self.ERROR_INTERVAL | ||
265 | 79 | logger.debug("Using corrected timestamp: %r", | ||
266 | 80 | http.datetimeToString(local_time + self.skew)) | ||
267 | 81 | return int(local_time + self.skew) | ||
268 | 82 | |||
269 | 83 | |||
270 | 84 | # pylint: disable=C0103 | ||
271 | 85 | timestamp_checker = SyncTimestampChecker() | ||
272 | 86 | # pylint: enable=C0103 | ||
273 | 25 | 87 | ||
274 | 26 | 88 | ||
275 | 27 | def oauth_headers(url, credentials, http_method='GET'): | 89 | def oauth_headers(url, credentials, http_method='GET'): |
276 | @@ -37,6 +99,7 @@ | |||
277 | 37 | url = url.encode('utf-8') | 99 | url = url.encode('utf-8') |
278 | 38 | _, _, _, _, query, _ = urlparse(url) | 100 | _, _, _, _, query, _ = urlparse(url) |
279 | 39 | parameters = dict(cgi.parse_qsl(query)) | 101 | parameters = dict(cgi.parse_qsl(query)) |
280 | 102 | parameters["oauth_timestamp"] = timestamp_checker.get_faithful_time() | ||
281 | 40 | 103 | ||
282 | 41 | consumer = oauth.OAuthConsumer(credentials['consumer_key'], | 104 | consumer = oauth.OAuthConsumer(credentials['consumer_key'], |
283 | 42 | credentials['consumer_secret']) | 105 | credentials['consumer_secret']) |
284 | 43 | 106 | ||
285 | === modified file 'ubuntu_sso/utils/tests/test_oauth_headers.py' | |||
286 | --- ubuntu_sso/utils/tests/test_oauth_headers.py 2011-08-12 12:54:36 +0000 | |||
287 | +++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-10-06 21:50:30 +0000 | |||
288 | @@ -1,6 +1,7 @@ | |||
289 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
290 | 2 | 2 | ||
291 | 3 | # Author: Natalia B. Bidart <natalia.bidart@canonical.com> | 3 | # Author: Natalia B. Bidart <natalia.bidart@canonical.com> |
292 | 4 | # Author: Alejandro J. Cura <alecu@canonical.com> | ||
293 | 4 | # | 5 | # |
294 | 5 | # Copyright 2011 Canonical Ltd. | 6 | # Copyright 2011 Canonical Ltd. |
295 | 6 | # | 7 | # |
296 | @@ -18,9 +19,16 @@ | |||
297 | 18 | 19 | ||
298 | 19 | """Tests for the oauth_headers helper function.""" | 20 | """Tests for the oauth_headers helper function.""" |
299 | 20 | 21 | ||
300 | 22 | import time | ||
301 | 23 | |||
302 | 24 | from twisted.application import internet, service | ||
303 | 25 | from twisted.internet import defer | ||
304 | 26 | from twisted.internet.threads import deferToThread | ||
305 | 21 | from twisted.trial.unittest import TestCase | 27 | from twisted.trial.unittest import TestCase |
306 | 28 | from twisted.web import server, resource | ||
307 | 22 | 29 | ||
309 | 23 | from ubuntu_sso.utils import oauth, oauth_headers | 30 | from ubuntu_sso import utils |
310 | 31 | from ubuntu_sso.utils import oauth, oauth_headers, SyncTimestampChecker | ||
311 | 24 | from ubuntu_sso.tests import TOKEN | 32 | from ubuntu_sso.tests import TOKEN |
312 | 25 | 33 | ||
313 | 26 | 34 | ||
314 | @@ -44,6 +52,21 @@ | |||
315 | 44 | """Test suite for the oauth_headers method.""" | 52 | """Test suite for the oauth_headers method.""" |
316 | 45 | 53 | ||
317 | 46 | url = u'http://example.com' | 54 | url = u'http://example.com' |
318 | 55 | fake_timestamp_value = 1 | ||
319 | 56 | |||
320 | 57 | @defer.inlineCallbacks | ||
321 | 58 | def setUp(self): | ||
322 | 59 | """Initialize this test suite.""" | ||
323 | 60 | yield super(SignWithCredentialsTestCase, self).setUp() | ||
324 | 61 | self.timestamp_called = False | ||
325 | 62 | |||
326 | 63 | def fake_timestamp(): | ||
327 | 64 | """A fake timestamp that records the call.""" | ||
328 | 65 | self.timestamp_called = True | ||
329 | 66 | return self.fake_timestamp_value | ||
330 | 67 | |||
331 | 68 | self.patch(utils.timestamp_checker, "get_faithful_time", | ||
332 | 69 | fake_timestamp) | ||
333 | 47 | 70 | ||
334 | 48 | def build_header(self, url, http_method='GET'): | 71 | def build_header(self, url, http_method='GET'): |
335 | 49 | """Build an Oauth header for comparison.""" | 72 | """Build an Oauth header for comparison.""" |
336 | @@ -105,5 +128,131 @@ | |||
337 | 105 | oauth_headers(url=self.url + path, credentials=TOKEN) | 128 | oauth_headers(url=self.url + path, credentials=TOKEN) |
338 | 106 | 129 | ||
339 | 107 | self.assertIn('parameters', FakedOAuthRequest.params) | 130 | self.assertIn('parameters', FakedOAuthRequest.params) |
342 | 108 | self.assertEqual(FakedOAuthRequest.params['parameters'], | 131 | params = FakedOAuthRequest.params['parameters'] |
343 | 109 | {'foo': 'bar'}) | 132 | del(params["oauth_timestamp"]) |
344 | 133 | self.assertEqual(params, {'foo': 'bar'}) | ||
345 | 134 | |||
346 | 135 | def test_oauth_headers_uses_timestamp_checker(self): | ||
347 | 136 | """The oauth_headers function uses the timestamp_checker.""" | ||
348 | 137 | oauth_headers(u"http://protocultura.net", TOKEN) | ||
349 | 138 | self.assertTrue(self.timestamp_called, | ||
350 | 139 | "the timestamp MUST be requested.") | ||
351 | 140 | |||
352 | 141 | |||
353 | 142 | class RootResource(resource.Resource): | ||
354 | 143 | """A root resource that logs the number of calls.""" | ||
355 | 144 | |||
356 | 145 | isLeaf = True | ||
357 | 146 | |||
358 | 147 | def __init__(self, *args, **kwargs): | ||
359 | 148 | """Initialize this fake instance.""" | ||
360 | 149 | resource.Resource.__init__(self, *args, **kwargs) | ||
361 | 150 | self.count = 0 | ||
362 | 151 | self.request_headers = [] | ||
363 | 152 | |||
364 | 153 | # pylint: disable=C0103 | ||
365 | 154 | def render_HEAD(self, request): | ||
366 | 155 | """Increase the counter on each render.""" | ||
367 | 156 | self.count += 1 | ||
368 | 157 | self.request_headers.append(request.requestHeaders) | ||
369 | 158 | return "" | ||
370 | 159 | |||
371 | 160 | |||
372 | 161 | class MockWebServer(object): | ||
373 | 162 | """A mock webserver for testing.""" | ||
374 | 163 | |||
375 | 164 | def __init__(self): | ||
376 | 165 | """Start up this instance.""" | ||
377 | 166 | # pylint: disable=E1101 | ||
378 | 167 | self.root = RootResource() | ||
379 | 168 | site = server.Site(self.root) | ||
380 | 169 | application = service.Application('web') | ||
381 | 170 | self.service_collection = service.IServiceCollection(application) | ||
382 | 171 | self.tcpserver = internet.TCPServer(0, site) | ||
383 | 172 | self.tcpserver.setServiceParent(self.service_collection) | ||
384 | 173 | self.service_collection.startService() | ||
385 | 174 | |||
386 | 175 | def get_url(self): | ||
387 | 176 | """Build the url for this mock server.""" | ||
388 | 177 | # pylint: disable=W0212 | ||
389 | 178 | port_num = self.tcpserver._port.getHost().port | ||
390 | 179 | return "http://localhost:%d/" % port_num | ||
391 | 180 | |||
392 | 181 | def stop(self): | ||
393 | 182 | """Shut it down.""" | ||
394 | 183 | # pylint: disable=E1101 | ||
395 | 184 | self.service_collection.stopService() | ||
396 | 185 | |||
397 | 186 | |||
398 | 187 | class FakedError(Exception): | ||
399 | 188 | """A mock, test, sample, and fake exception.""" | ||
400 | 189 | |||
401 | 190 | |||
402 | 191 | class TimestampCheckerTestCase(TestCase): | ||
403 | 192 | """Tests for the timestamp checker.""" | ||
404 | 193 | |||
405 | 194 | def setUp(self): | ||
406 | 195 | """Initialize a fake webserver.""" | ||
407 | 196 | self.ws = MockWebServer() | ||
408 | 197 | self.addCleanup(self.ws.stop) | ||
409 | 198 | self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url()) | ||
410 | 199 | |||
411 | 200 | @defer.inlineCallbacks | ||
412 | 201 | def test_returned_value_is_int(self): | ||
413 | 202 | """The returned value is an integer.""" | ||
414 | 203 | checker = SyncTimestampChecker() | ||
415 | 204 | timestamp = yield deferToThread(checker.get_faithful_time) | ||
416 | 205 | self.assertEqual(type(timestamp), int) | ||
417 | 206 | |||
418 | 207 | @defer.inlineCallbacks | ||
419 | 208 | def test_first_call_does_head(self): | ||
420 | 209 | """The first call gets the clock from our web.""" | ||
421 | 210 | checker = SyncTimestampChecker() | ||
422 | 211 | yield deferToThread(checker.get_faithful_time) | ||
423 | 212 | self.assertEqual(self.ws.root.count, 1) | ||
424 | 213 | |||
425 | 214 | @defer.inlineCallbacks | ||
426 | 215 | def test_second_call_is_cached(self): | ||
427 | 216 | """For the second call, the time is cached.""" | ||
428 | 217 | checker = SyncTimestampChecker() | ||
429 | 218 | yield deferToThread(checker.get_faithful_time) | ||
430 | 219 | yield deferToThread(checker.get_faithful_time) | ||
431 | 220 | self.assertEqual(self.ws.root.count, 1) | ||
432 | 221 | |||
433 | 222 | @defer.inlineCallbacks | ||
434 | 223 | def test_after_timeout_cache_expires(self): | ||
435 | 224 | """After some time, the cache expires.""" | ||
436 | 225 | fake_timestamp = 1 | ||
437 | 226 | self.patch(time, "time", lambda: fake_timestamp) | ||
438 | 227 | checker = SyncTimestampChecker() | ||
439 | 228 | yield deferToThread(checker.get_faithful_time) | ||
440 | 229 | fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL | ||
441 | 230 | yield deferToThread(checker.get_faithful_time) | ||
442 | 231 | self.assertEqual(self.ws.root.count, 2) | ||
443 | 232 | |||
444 | 233 | @defer.inlineCallbacks | ||
445 | 234 | def test_server_date_sends_nocache_headers(self): | ||
446 | 235 | """Getting the server date sends the no-cache headers.""" | ||
447 | 236 | checker = SyncTimestampChecker() | ||
448 | 237 | yield deferToThread(checker.get_server_time) | ||
449 | 238 | assert len(self.ws.root.request_headers) == 1 | ||
450 | 239 | headers = self.ws.root.request_headers[0] | ||
451 | 240 | result = headers.getRawHeaders("Cache-Control") | ||
452 | 241 | self.assertEqual(result, ["no-cache"]) | ||
453 | 242 | |||
454 | 243 | @defer.inlineCallbacks | ||
455 | 244 | def test_server_error_means_skew_not_updated(self): | ||
456 | 245 | """When server can't be reached, the skew is not updated.""" | ||
457 | 246 | fake_timestamp = 1 | ||
458 | 247 | self.patch(time, "time", lambda: fake_timestamp) | ||
459 | 248 | checker = SyncTimestampChecker() | ||
460 | 249 | |||
461 | 250 | def failing_get_server_time(): | ||
462 | 251 | """Let's fail while retrieving the server time.""" | ||
463 | 252 | raise FakedError() | ||
464 | 253 | |||
465 | 254 | self.patch(checker, "get_server_time", failing_get_server_time) | ||
466 | 255 | yield deferToThread(checker.get_faithful_time) | ||
467 | 256 | self.assertEqual(checker.skew, 0) | ||
468 | 257 | self.assertEqual(checker.next_check, | ||
469 | 258 | fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL) |
+1