Merge lp:~nataliabidart/ubuntuone-control-panel/use-webclient into lp:ubuntuone-control-panel
- use-webclient
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||||||
Approved revision: | 265 | ||||||||
Merged at revision: | 258 | ||||||||
Proposed branch: | lp:~nataliabidart/ubuntuone-control-panel/use-webclient | ||||||||
Merge into: | lp:ubuntuone-control-panel | ||||||||
Diff against target: |
2046 lines (+478/-1084) 24 files modified
setup.py (+0/-1) ubuntuone/controlpanel/backend.py (+46/-27) ubuntuone/controlpanel/gui/__init__.py (+33/-50) ubuntuone/controlpanel/gui/qt/folders.py (+2/-7) ubuntuone/controlpanel/gui/qt/gotoweb.py (+2/-12) ubuntuone/controlpanel/gui/qt/gui.py (+3/-3) ubuntuone/controlpanel/gui/qt/main/__init__.py (+5/-14) ubuntuone/controlpanel/gui/qt/main/linux.py (+11/-3) ubuntuone/controlpanel/gui/qt/main/windows.py (+21/-0) ubuntuone/controlpanel/gui/qt/systray.py (+4/-5) ubuntuone/controlpanel/gui/qt/tests/__init__.py (+29/-21) ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py (+5/-52) ubuntuone/controlpanel/gui/qt/tests/test_signin.py (+2/-5) ubuntuone/controlpanel/gui/qt/tests/test_start.py (+89/-91) ubuntuone/controlpanel/gui/qt/tests/test_systray.py (+0/-2) ubuntuone/controlpanel/gui/tests/test_url_sign.py (+0/-102) ubuntuone/controlpanel/tests/test_backend.py (+66/-1) ubuntuone/controlpanel/tests/test_web_client.py (+107/-84) ubuntuone/controlpanel/web_client.py (+53/-61) ubuntuone/controlpanel/web_client/libsoup.py (+0/-133) ubuntuone/controlpanel/web_client/tests/__init__.py (+0/-19) ubuntuone/controlpanel/web_client/tests/test_libsoup.py (+0/-106) ubuntuone/controlpanel/web_client/tests/test_txwebclient.py (+0/-177) ubuntuone/controlpanel/web_client/txwebclient.py (+0/-108) |
||||||||
To merge this branch: | bzr merge lp:~nataliabidart/ubuntuone-control-panel/use-webclient | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alejandro J. Cura (community) | Approve | ||
Roberto Alsina (community) | Approve | ||
Review via email: mp+91676@code.launchpad.net |
Commit message
- Replaced custom webclient with the one from ubuntu-sso-client
(LP: #926311).
- Removed the dependency on qt4reactor for Linux implementation.
Description of the change
Natalia Bidart (nataliabidart) wrote : | # |
The controlpanel, in order to run on windows, will need:
https:/
Alejandro J. Cura (alecu) wrote : | # |
Code looks nice, all tests pass, tested IRL on linux.
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
The attempt to merge lp:~nataliabidart/ubuntuone-control-panel/use-webclient into lp:ubuntuone-control-panel failed. Below is the output from the failed tests.
*** Running test suite for ubuntuone/
Traceback (most recent call last):
File "/usr/bin/u1trial", line 325, in <module>
main()
File "/usr/bin/u1trial", line 305, in main
suite = trial_runner.
File "/usr/bin/u1trial", line 184, in get_suite
config[
File "/usr/bin/u1trial", line 168, in _collect_tests
module_suite = self._load_
File "/usr/bin/u1trial", line 108, in _load_unittest
module = __import__(modpath, None, None, [""])
File "/home/
from ubuntu_
ImportError: No module named tests
Preview Diff
1 | === modified file 'setup.py' | |||
2 | --- setup.py 2012-01-26 13:31:22 +0000 | |||
3 | +++ setup.py 2012-02-07 14:03:19 +0000 | |||
4 | @@ -247,7 +247,6 @@ | |||
5 | 247 | 'ubuntuone.controlpanel.gui.qt.ui', | 247 | 'ubuntuone.controlpanel.gui.qt.ui', |
6 | 248 | 'ubuntuone.controlpanel.sd_client', | 248 | 'ubuntuone.controlpanel.sd_client', |
7 | 249 | 'ubuntuone.controlpanel.utils', | 249 | 'ubuntuone.controlpanel.utils', |
8 | 250 | 'ubuntuone.controlpanel.web_client', | ||
9 | 251 | ], | 250 | ], |
10 | 252 | extra_path='ubuntuone-control-panel', | 251 | extra_path='ubuntuone-control-panel', |
11 | 253 | data_files=[ | 252 | data_files=[ |
12 | 254 | 253 | ||
13 | === modified file 'ubuntuone/controlpanel/backend.py' | |||
14 | --- ubuntuone/controlpanel/backend.py 2012-01-25 14:49:59 +0000 | |||
15 | +++ ubuntuone/controlpanel/backend.py 2012-02-07 14:03:19 +0000 | |||
16 | @@ -34,41 +34,47 @@ | |||
17 | 34 | 34 | ||
18 | 35 | from ubuntuone.controlpanel import sd_client, replication_client | 35 | from ubuntuone.controlpanel import sd_client, replication_client |
19 | 36 | from ubuntuone.controlpanel.logger import setup_logging, log_call | 36 | from ubuntuone.controlpanel.logger import setup_logging, log_call |
24 | 37 | # pylint: disable=W0611 | 37 | from ubuntuone.controlpanel.web_client import ( |
25 | 38 | from ubuntuone.controlpanel.web_client import (UnauthorizedError, | 38 | UnauthorizedError, |
26 | 39 | web_client_factory, WebClientError) | 39 | WebClient, |
27 | 40 | # pylint: enable=W0611 | 40 | WebClientError, |
28 | 41 | ) | ||
29 | 41 | 42 | ||
30 | 42 | logger = setup_logging('backend') | 43 | logger = setup_logging('backend') |
31 | 43 | 44 | ||
54 | 44 | ACCOUNT_API = "account/" | 45 | ACCOUNT_API = u"account/" |
55 | 45 | QUOTA_API = "quota/" | 46 | DEVICES_API = u"1.0/devices/" |
56 | 46 | DEVICES_API = "1.0/devices/" | 47 | DEVICE_REMOVE_API = u"1.0/devices/remove/%s/%s" |
57 | 47 | DEVICE_REMOVE_API = "1.0/devices/remove/%s/%s" | 48 | QUOTA_API = u"quota/" |
58 | 48 | DEVICE_TYPE_PHONE = "Phone" | 49 | |
59 | 49 | DEVICE_TYPE_COMPUTER = "Computer" | 50 | DEVICE_TYPE_PHONE = u"Phone" |
60 | 50 | AUTOCONNECT_KEY = 'autoconnect' | 51 | DEVICE_TYPE_COMPUTER = u"Computer" |
61 | 51 | SHOW_ALL_NOTIFICATIONS_KEY = 'show_all_notifications' | 52 | |
62 | 52 | SHARE_AUTOSUBSCRIBE_KEY = 'share_autosubscribe' | 53 | AUTOCONNECT_KEY = u'autoconnect' |
63 | 53 | UDF_AUTOSUBSCRIBE_KEY = 'udf_autosubscribe' | 54 | SHOW_ALL_NOTIFICATIONS_KEY = u'show_all_notifications' |
64 | 54 | LIMIT_BW_KEY = 'limit_bandwidth' | 55 | SHARE_AUTOSUBSCRIBE_KEY = u'share_autosubscribe' |
65 | 55 | UPLOAD_KEY = "max_upload_speed" | 56 | UDF_AUTOSUBSCRIBE_KEY = u'udf_autosubscribe' |
66 | 56 | DOWNLOAD_KEY = "max_download_speed" | 57 | LIMIT_BW_KEY = u'limit_bandwidth' |
67 | 57 | 58 | UPLOAD_KEY = u'max_upload_speed' | |
68 | 58 | FILE_SYNC_DISABLED = 'file-sync-disabled' | 59 | DOWNLOAD_KEY = u'max_download_speed' |
69 | 59 | FILE_SYNC_DISCONNECTED = 'file-sync-disconnected' | 60 | |
70 | 60 | FILE_SYNC_ERROR = 'file-sync-error' | 61 | FILE_SYNC_DISABLED = u'file-sync-disabled' |
71 | 61 | FILE_SYNC_IDLE = 'file-sync-idle' | 62 | FILE_SYNC_DISCONNECTED = u'file-sync-disconnected' |
72 | 62 | FILE_SYNC_STARTING = 'file-sync-starting' | 63 | FILE_SYNC_ERROR = u'file-sync-error' |
73 | 63 | FILE_SYNC_STOPPED = 'file-sync-stopped' | 64 | FILE_SYNC_IDLE = u'file-sync-idle' |
74 | 64 | FILE_SYNC_SYNCING = 'file-sync-syncing' | 65 | FILE_SYNC_STARTING = u'file-sync-starting' |
75 | 65 | FILE_SYNC_UNKNOWN = 'file-sync-unknown' | 66 | FILE_SYNC_STOPPED = u'file-sync-stopped' |
76 | 67 | FILE_SYNC_SYNCING = u'file-sync-syncing' | ||
77 | 68 | FILE_SYNC_UNKNOWN = u'file-sync-unknown' | ||
78 | 66 | 69 | ||
79 | 67 | MSG_KEY = 'message' | 70 | MSG_KEY = 'message' |
80 | 68 | STATUS_KEY = 'status' | 71 | STATUS_KEY = 'status' |
81 | 69 | 72 | ||
82 | 70 | CONTACTS_PKG = 'thunderbird-couchdb' | 73 | CONTACTS_PKG = 'thunderbird-couchdb' |
83 | 71 | 74 | ||
84 | 75 | UBUNTUONE_FROM_OAUTH = u'https://one.ubuntu.com/api/1.0/from_oauth/' | ||
85 | 76 | UBUNTUONE_LINK = u'https://one.ubuntu.com/' | ||
86 | 77 | |||
87 | 72 | 78 | ||
88 | 73 | def append_path_sep(path): | 79 | def append_path_sep(path): |
89 | 74 | """If 'path' does not end with the path separator, append it.""" | 80 | """If 'path' does not end with the path separator, append it.""" |
90 | @@ -135,7 +141,7 @@ | |||
91 | 135 | 141 | ||
92 | 136 | self.login_client = CredentialsManagementTool() | 142 | self.login_client = CredentialsManagementTool() |
93 | 137 | self.sd_client = sd_client.SyncDaemonClient() | 143 | self.sd_client = sd_client.SyncDaemonClient() |
95 | 138 | self.wc = web_client_factory(self.get_credentials) | 144 | self.wc = WebClient(self.get_credentials) |
96 | 139 | 145 | ||
97 | 140 | logger.info('ControlBackend: instance started.') | 146 | logger.info('ControlBackend: instance started.') |
98 | 141 | 147 | ||
99 | @@ -304,6 +310,19 @@ | |||
100 | 304 | returnValue(path) | 310 | returnValue(path) |
101 | 305 | 311 | ||
102 | 306 | @inlineCallbacks | 312 | @inlineCallbacks |
103 | 313 | def build_signed_iri(self, iri): | ||
104 | 314 | """Returned an OAuth signed iri.""" | ||
105 | 315 | credentials = None | ||
106 | 316 | if iri.startswith(UBUNTUONE_LINK): | ||
107 | 317 | credentials = yield self.get_credentials() | ||
108 | 318 | |||
109 | 319 | if credentials: | ||
110 | 320 | parameters = {'next': iri} | ||
111 | 321 | iri = yield self.wc.build_signed_iri(UBUNTUONE_FROM_OAUTH, | ||
112 | 322 | parameters) | ||
113 | 323 | returnValue(iri) | ||
114 | 324 | |||
115 | 325 | @inlineCallbacks | ||
116 | 307 | def get_credentials(self): | 326 | def get_credentials(self): |
117 | 308 | """Find credentials.""" | 327 | """Find credentials.""" |
118 | 309 | if not self._credentials: | 328 | if not self._credentials: |
119 | 310 | 329 | ||
120 | === modified file 'ubuntuone/controlpanel/gui/__init__.py' | |||
121 | --- ubuntuone/controlpanel/gui/__init__.py 2011-10-06 20:38:39 +0000 | |||
122 | +++ ubuntuone/controlpanel/gui/__init__.py 2012-02-07 14:03:19 +0000 | |||
123 | @@ -21,56 +21,55 @@ | |||
124 | 21 | 21 | ||
125 | 22 | import gettext | 22 | import gettext |
126 | 23 | 23 | ||
128 | 24 | from oauth import oauth | 24 | from ubuntuone.controlpanel.backend import UBUNTUONE_LINK |
129 | 25 | |||
130 | 25 | 26 | ||
131 | 26 | _ = gettext.gettext | 27 | _ = gettext.gettext |
132 | 27 | 28 | ||
133 | 28 | 29 | ||
135 | 29 | ERROR_COLOR = 'red' | 30 | ERROR_COLOR = u'red' |
136 | 30 | KILOBYTES = 1024 | 31 | KILOBYTES = 1024 |
137 | 31 | NO_OP = lambda *a, **kw: None | 32 | NO_OP = lambda *a, **kw: None |
138 | 32 | # http://design.canonical.com/the-toolkit/ubuntu-logo-and-circle-of-friends/ | 33 | # http://design.canonical.com/the-toolkit/ubuntu-logo-and-circle-of-friends/ |
140 | 33 | ORANGE = '#DD4814' | 34 | ORANGE = u'#DD4814' |
141 | 34 | QUOTA_THRESHOLD = 0.95 | 35 | QUOTA_THRESHOLD = 0.95 |
142 | 35 | SHARES_MIN_SIZE_FULL = 1048576 | 36 | SHARES_MIN_SIZE_FULL = 1048576 |
144 | 36 | SUCCESS_COLOR = 'green' | 37 | SUCCESS_COLOR = u'green' |
145 | 37 | 38 | ||
146 | 38 | ERROR_ICON = u'✘' | 39 | ERROR_ICON = u'✘' |
147 | 39 | SYNCING_ICON = u'⇅' | 40 | SYNCING_ICON = u'⇅' |
148 | 40 | IDLE_ICON = u'✔' | 41 | IDLE_ICON = u'✔' |
149 | 41 | 42 | ||
170 | 42 | CONTACT_ICON_NAME = 'avatar-default' | 43 | CONTACT_ICON_NAME = u'avatar-default' |
171 | 43 | FOLDER_ICON_NAME = 'folder' | 44 | FOLDER_ICON_NAME = u'folder' |
172 | 44 | SHARE_ICON_NAME = 'folder-remote' | 45 | SHARE_ICON_NAME = u'folder-remote' |
173 | 45 | MUSIC_ICON_NAME = 'audio-x-generic' | 46 | MUSIC_ICON_NAME = u'audio-x-generic' |
174 | 46 | 47 | ||
175 | 47 | CONTACTS_ICON = 'contacts.png' | 48 | CONTACTS_ICON = u'contacts.png' |
176 | 48 | FACEBOOK_LOGO = 'facebook.png' | 49 | FACEBOOK_LOGO = u'facebook.png' |
177 | 49 | FILES_ICON = 'files.png' | 50 | FILES_ICON = u'files.png' |
178 | 50 | OVERVIEW_BANNER = 'overview.png' | 51 | OVERVIEW_BANNER = u'overview.png' |
179 | 51 | TWITTER_LOGO = 'twitter.png' | 52 | TWITTER_LOGO = u'twitter.png' |
180 | 52 | MUSIC_STORE_ICON = 'music-store.png' | 53 | MUSIC_STORE_ICON = u'music-store.png' |
181 | 53 | MUSIC_STREAM_ICON = 'music-stream.png' | 54 | MUSIC_STREAM_ICON = u'music-stream.png' |
182 | 54 | NOTES_ICON = 'notes.png' | 55 | NOTES_ICON = u'notes.png' |
183 | 55 | SERVICES_CONTACTS_ICON = 'services-contacts.png' | 56 | SERVICES_CONTACTS_ICON = u'services-contacts.png' |
184 | 56 | SERVICES_FILES_EXAMPLE = 'services-files-example.png' | 57 | SERVICES_FILES_EXAMPLE = u'services-files-example.png' |
185 | 57 | SERVICES_FILES_ICON = 'services-files.png' | 58 | SERVICES_FILES_ICON = u'services-files.png' |
186 | 58 | 59 | ||
187 | 59 | FILE_URI_PREFIX = 'file://' | 60 | FILE_URI_PREFIX = u'file://' |
168 | 60 | UBUNTUONE_FROM_OAUTH = 'https://one.ubuntu.com/api/1.0/from_oauth/' | ||
169 | 61 | UBUNTUONE_LINK = 'https://one.ubuntu.com/' | ||
188 | 62 | 61 | ||
189 | 63 | CONTACTS_LINK = UBUNTUONE_LINK | 62 | CONTACTS_LINK = UBUNTUONE_LINK |
196 | 64 | EDIT_ACCOUNT_LINK = UBUNTUONE_LINK + 'account/' | 63 | EDIT_ACCOUNT_LINK = UBUNTUONE_LINK + u'account/' |
197 | 65 | EDIT_DEVICES_LINK = EDIT_ACCOUNT_LINK + 'machines/' | 64 | EDIT_DEVICES_LINK = EDIT_ACCOUNT_LINK + u'machines/' |
198 | 66 | EDIT_PROFILE_LINK = 'https://login.ubuntu.com/' | 65 | EDIT_PROFILE_LINK = u'https://login.ubuntu.com/' |
199 | 67 | EDIT_SERVICES_LINK = UBUNTUONE_LINK + 'services' | 66 | EDIT_SERVICES_LINK = UBUNTUONE_LINK + u'services' |
200 | 68 | FACEBOOK_LINK = 'http://www.facebook.com/ubuntuone/' | 67 | FACEBOOK_LINK = u'http://www.facebook.com/ubuntuone/' |
201 | 69 | GET_SUPPORT_LINK = UBUNTUONE_LINK + 'support/' | 68 | GET_SUPPORT_LINK = UBUNTUONE_LINK + u'support/' |
202 | 70 | LEARN_MORE_LINK = UBUNTUONE_LINK | 69 | LEARN_MORE_LINK = UBUNTUONE_LINK |
206 | 71 | MANAGE_FILES_LINK = UBUNTUONE_LINK + 'files/' | 70 | MANAGE_FILES_LINK = UBUNTUONE_LINK + u'files/' |
207 | 72 | RESET_PASSWORD_LINK = EDIT_PROFILE_LINK + '+forgot_password' | 71 | RESET_PASSWORD_LINK = EDIT_PROFILE_LINK + u'+forgot_password' |
208 | 73 | TWITTER_LINK = 'http://twitter.com/ubuntuone/' | 72 | TWITTER_LINK = u'http://twitter.com/ubuntuone/' |
209 | 74 | 73 | ||
210 | 75 | ALWAYS_SUBSCRIBED = _('Always in sync') | 74 | ALWAYS_SUBSCRIBED = _('Always in sync') |
211 | 76 | CONNECT_BUTTON_LABEL = _('Connect to Ubuntu One') | 75 | CONNECT_BUTTON_LABEL = _('Connect to Ubuntu One') |
212 | @@ -86,7 +85,7 @@ | |||
213 | 86 | 'previous values were restored.') | 85 | 'previous values were restored.') |
214 | 87 | DEVICE_CONFIRM_REMOVE = _('Are you sure you want to remove this device ' | 86 | DEVICE_CONFIRM_REMOVE = _('Are you sure you want to remove this device ' |
215 | 88 | 'from Ubuntu One?') | 87 | 'from Ubuntu One?') |
217 | 89 | DEVICE_REMOVABLE_PREFIX = 'Ubuntu One @ ' | 88 | DEVICE_REMOVABLE_PREFIX = u'Ubuntu One @ ' |
218 | 90 | DEVICE_REMOVAL_ERROR = _('The device could not be removed.') | 89 | DEVICE_REMOVAL_ERROR = _('The device could not be removed.') |
219 | 91 | DEVICES_BUTTON_TOOLTIP = _('Manage devices registered with your personal ' | 90 | DEVICES_BUTTON_TOOLTIP = _('Manage devices registered with your personal ' |
220 | 92 | 'cloud') | 91 | 'cloud') |
221 | @@ -185,19 +184,3 @@ | |||
222 | 185 | str_bytes = str_bytes.rstrip('0') | 184 | str_bytes = str_bytes.rstrip('0') |
223 | 186 | str_bytes = str_bytes.rstrip('.') | 185 | str_bytes = str_bytes.rstrip('.') |
224 | 187 | return '%s %s' % (str_bytes, units[unit]) | 186 | return '%s %s' % (str_bytes, units[unit]) |
225 | 188 | |||
226 | 189 | |||
227 | 190 | def sign_url(url, credentials, timestamp=None): | ||
228 | 191 | """Sign the URL using the currently available credentials.""" | ||
229 | 192 | consumer = oauth.OAuthConsumer(credentials["consumer_key"], | ||
230 | 193 | credentials["consumer_secret"]) | ||
231 | 194 | token = oauth.OAuthToken(credentials["token"], | ||
232 | 195 | credentials["token_secret"]) | ||
233 | 196 | parameters = {'next': url, 'oauth_timestamp': timestamp} | ||
234 | 197 | request = oauth.OAuthRequest.from_consumer_and_token( | ||
235 | 198 | http_url=UBUNTUONE_FROM_OAUTH, http_method='GET', | ||
236 | 199 | oauth_consumer=consumer, token=token, | ||
237 | 200 | parameters=parameters) | ||
238 | 201 | sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1() | ||
239 | 202 | request.sign_request(sig_method, consumer, token) | ||
240 | 203 | return request.to_url() | ||
241 | 204 | 187 | ||
242 | === modified file 'ubuntuone/controlpanel/gui/qt/folders.py' | |||
243 | --- ubuntuone/controlpanel/gui/qt/folders.py 2012-01-18 17:40:33 +0000 | |||
244 | +++ ubuntuone/controlpanel/gui/qt/folders.py 2012-02-07 14:03:19 +0000 | |||
245 | @@ -1,8 +1,6 @@ | |||
246 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
247 | 2 | |||
248 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
249 | 4 | # | 2 | # |
251 | 5 | # Copyright 2011 Canonical Ltd. | 3 | # Copyright 2011-2012 Canonical Ltd. |
252 | 6 | # | 4 | # |
253 | 7 | # This program is free software: you can redistribute it and/or modify it | 5 | # This program is free software: you can redistribute it and/or modify it |
254 | 8 | # under the terms of the GNU General Public License version 3, as published | 6 | # under the terms of the GNU General Public License version 3, as published |
255 | @@ -85,10 +83,7 @@ | |||
256 | 85 | def on_folder_created(self, new_folder): | 83 | def on_folder_created(self, new_folder): |
257 | 86 | """Reload folder info after folder creation.""" | 84 | """Reload folder info after folder creation.""" |
258 | 87 | self.is_processing = True | 85 | self.is_processing = True |
263 | 88 | # hack to ensure that syncdaemon updates the folder list. | 86 | self.load() |
260 | 89 | # pylint: disable=W0404, E1101 | ||
261 | 90 | from twisted.internet import reactor | ||
262 | 91 | reactor.callLater(2, self.load) | ||
264 | 92 | 87 | ||
265 | 93 | # pylint: disable=E0202 | 88 | # pylint: disable=E0202 |
266 | 94 | @defer.inlineCallbacks | 89 | @defer.inlineCallbacks |
267 | 95 | 90 | ||
268 | === modified file 'ubuntuone/controlpanel/gui/qt/gotoweb.py' | |||
269 | --- ubuntuone/controlpanel/gui/qt/gotoweb.py 2011-10-05 23:12:23 +0000 | |||
270 | +++ ubuntuone/controlpanel/gui/qt/gotoweb.py 2012-02-07 14:03:19 +0000 | |||
271 | @@ -23,8 +23,7 @@ | |||
272 | 23 | from twisted.internet import defer | 23 | from twisted.internet import defer |
273 | 24 | 24 | ||
274 | 25 | from ubuntuone.controlpanel import cache | 25 | from ubuntuone.controlpanel import cache |
277 | 26 | from ubuntuone.controlpanel.gui import qt, sign_url, UBUNTUONE_LINK | 26 | from ubuntuone.controlpanel.gui import qt |
276 | 27 | from ubuntuone.controlpanel.web_client.txwebclient import timestamp_checker | ||
278 | 28 | 27 | ||
279 | 29 | 28 | ||
280 | 30 | class GoToWebButton(cache.Cache, QtGui.QPushButton): | 29 | class GoToWebButton(cache.Cache, QtGui.QPushButton): |
281 | @@ -44,14 +43,5 @@ | |||
282 | 44 | def on_clicked(self): | 43 | def on_clicked(self): |
283 | 45 | """Open self.uri if not None, do nothing otherwise.""" | 44 | """Open self.uri if not None, do nothing otherwise.""" |
284 | 46 | if self.uri is not None: | 45 | if self.uri is not None: |
295 | 47 | 46 | uri = yield self.backend.build_signed_iri(self.uri) | |
286 | 48 | credentials = None | ||
287 | 49 | if self.uri.startswith(UBUNTUONE_LINK): | ||
288 | 50 | credentials = yield self.backend.get_credentials() | ||
289 | 51 | |||
290 | 52 | uri = self.uri | ||
291 | 53 | if credentials: | ||
292 | 54 | timestamp = yield timestamp_checker.get_faithful_time() | ||
293 | 55 | uri = yield sign_url(uri, credentials, timestamp) | ||
294 | 56 | |||
296 | 57 | qt.uri_hook(uri) | 47 | qt.uri_hook(uri) |
297 | 58 | 48 | ||
298 | === modified file 'ubuntuone/controlpanel/gui/qt/gui.py' | |||
299 | --- ubuntuone/controlpanel/gui/qt/gui.py 2011-09-15 14:42:24 +0000 | |||
300 | +++ ubuntuone/controlpanel/gui/qt/gui.py 2012-02-07 14:03:19 +0000 | |||
301 | @@ -52,20 +52,20 @@ | |||
302 | 52 | # pylint: enable=C0103 | 52 | # pylint: enable=C0103 |
303 | 53 | 53 | ||
304 | 54 | 54 | ||
306 | 55 | def start(stop, minimized=False, with_icon=False): | 55 | def start(close_callback, minimized=False, with_icon=False): |
307 | 56 | """Show the UI elements.""" | 56 | """Show the UI elements.""" |
308 | 57 | # pylint: disable=W0404, F0401 | 57 | # pylint: disable=W0404, F0401 |
309 | 58 | if not minimized: | 58 | if not minimized: |
310 | 59 | if with_icon or minimized: | 59 | if with_icon or minimized: |
311 | 60 | window = MainWindow() | 60 | window = MainWindow() |
312 | 61 | else: | 61 | else: |
314 | 62 | window = MainWindow(close_callback=stop) | 62 | window = MainWindow(close_callback=close_callback) |
315 | 63 | window.show() | 63 | window.show() |
316 | 64 | else: | 64 | else: |
317 | 65 | window = None | 65 | window = None |
318 | 66 | if with_icon or minimized: | 66 | if with_icon or minimized: |
319 | 67 | QtGui.QApplication.instance().setQuitOnLastWindowClosed(False) | 67 | QtGui.QApplication.instance().setQuitOnLastWindowClosed(False) |
321 | 68 | icon = TrayIcon(window=window) | 68 | icon = TrayIcon(window=window, close_callback=close_callback) |
322 | 69 | else: | 69 | else: |
323 | 70 | icon = None | 70 | icon = None |
324 | 71 | return icon, window | 71 | return icon, window |
325 | 72 | 72 | ||
326 | === modified file 'ubuntuone/controlpanel/gui/qt/main/__init__.py' | |||
327 | --- ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-01-19 19:00:54 +0000 | |||
328 | +++ ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-02-07 14:03:19 +0000 | |||
329 | @@ -14,7 +14,7 @@ | |||
330 | 14 | # You should have received a copy of the GNU General Public License along | 14 | # You should have received a copy of the GNU General Public License along |
331 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
332 | 16 | 16 | ||
334 | 17 | """Provide the correct reactor and ui integration.""" | 17 | """Provide the correct ui main module.""" |
335 | 18 | 18 | ||
336 | 19 | import sys | 19 | import sys |
337 | 20 | 20 | ||
338 | @@ -37,32 +37,23 @@ | |||
339 | 37 | 37 | ||
340 | 38 | 38 | ||
341 | 39 | def main(switch_to='', alert=False, minimized=False, with_icon=False): | 39 | def main(switch_to='', alert=False, minimized=False, with_icon=False): |
343 | 40 | """Start the Qt reactor and open the main window.""" | 40 | """Start the Qt mainloop and open the main window.""" |
344 | 41 | # The following cannot be imported outside this function | 41 | # The following cannot be imported outside this function |
345 | 42 | # because u1trial already provides a reactor. | 42 | # because u1trial already provides a reactor. |
346 | 43 | 43 | ||
347 | 44 | # The main loop MUST be initialized before importing the reactor | ||
348 | 45 | app = UniqueApplication(sys.argv, "ubuntuone-control-panel") | 44 | app = UniqueApplication(sys.argv, "ubuntuone-control-panel") |
349 | 46 | source.main(app) | 45 | source.main(app) |
350 | 46 | |||
351 | 47 | qss = QtCore.QResource(":/ubuntuone.qss") | 47 | qss = QtCore.QResource(":/ubuntuone.qss") |
352 | 48 | app.setStyleSheet(qss.data()) | 48 | app.setStyleSheet(qss.data()) |
353 | 49 | 49 | ||
354 | 50 | # Reimport 'qt4reactor', 'reactor', 'start', pylint: disable=W0404, F0401 | ||
355 | 51 | import qt4reactor | ||
356 | 52 | qt4reactor.install() | ||
357 | 53 | |||
358 | 54 | from twisted.internet import reactor | ||
359 | 55 | from ubuntuone.controlpanel.gui.qt.gui import start | 50 | from ubuntuone.controlpanel.gui.qt.gui import start |
360 | 56 | # pylint: enable=W0404, F0401 | ||
361 | 57 | |||
362 | 58 | # Module 'reactor' has no 'run'/'stop' member, pylint: disable=E1101 | ||
363 | 59 | 51 | ||
364 | 60 | # Unused variable 'window', 'icon', pylint: disable=W0612 | 52 | # Unused variable 'window', 'icon', pylint: disable=W0612 |
366 | 61 | icon, window = start(reactor.stop, | 53 | icon, window = start(lambda: source.main_quit(app), |
367 | 62 | minimized=minimized, with_icon=with_icon) | 54 | minimized=minimized, with_icon=with_icon) |
368 | 63 | # pylint: enable=W0612 | 55 | # pylint: enable=W0612 |
369 | 64 | if icon: | 56 | if icon: |
370 | 65 | app.new_instance.connect(icon.restore_window) | 57 | app.new_instance.connect(icon.restore_window) |
371 | 66 | 58 | ||
374 | 67 | reactor.run() | 59 | source.main_start(app) |
373 | 68 | # pylint: enable=E1101 | ||
375 | 69 | 60 | ||
376 | === modified file 'ubuntuone/controlpanel/gui/qt/main/linux.py' | |||
377 | --- ubuntuone/controlpanel/gui/qt/main/linux.py 2012-01-26 12:41:08 +0000 | |||
378 | +++ ubuntuone/controlpanel/gui/qt/main/linux.py 2012-02-07 14:03:19 +0000 | |||
379 | @@ -16,10 +16,18 @@ | |||
380 | 16 | 16 | ||
381 | 17 | """Main method to be used on linux.""" | 17 | """Main method to be used on linux.""" |
382 | 18 | 18 | ||
383 | 19 | from dbus.mainloop.qt import DBusQtMainLoop | ||
384 | 20 | |||
385 | 21 | 19 | ||
386 | 22 | def main(app): | 20 | def main(app): |
387 | 23 | """Apply style sheet.""" | 21 | """Apply style sheet.""" |
389 | 24 | # The DBus main loop MUST be initialized before importing the reactor | 22 | from dbus.mainloop.qt import DBusQtMainLoop |
390 | 25 | DBusQtMainLoop(set_as_default=True) | 23 | DBusQtMainLoop(set_as_default=True) |
391 | 24 | |||
392 | 25 | |||
393 | 26 | def main_start(app): | ||
394 | 27 | """Start the mainloop.""" | ||
395 | 28 | app.exec_() | ||
396 | 29 | |||
397 | 30 | |||
398 | 31 | def main_quit(app): | ||
399 | 32 | """Stop the mainloop.""" | ||
400 | 33 | app.exit() | ||
401 | 26 | 34 | ||
402 | === modified file 'ubuntuone/controlpanel/gui/qt/main/windows.py' | |||
403 | --- ubuntuone/controlpanel/gui/qt/main/windows.py 2012-01-26 12:41:08 +0000 | |||
404 | +++ ubuntuone/controlpanel/gui/qt/main/windows.py 2012-02-07 14:03:19 +0000 | |||
405 | @@ -24,3 +24,24 @@ | |||
406 | 24 | # Apply font to the entire application | 24 | # Apply font to the entire application |
407 | 25 | QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf') | 25 | QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf') |
408 | 26 | QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf') | 26 | QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf') |
409 | 27 | |||
410 | 28 | import qt4reactor | ||
411 | 29 | qt4reactor.install() | ||
412 | 30 | |||
413 | 31 | |||
414 | 32 | # Module 'reactor' has no 'run'/'stop' member, pylint: disable=E1101 | ||
415 | 33 | |||
416 | 34 | |||
417 | 35 | def main_start(app): | ||
418 | 36 | """Start the mainloop.""" | ||
419 | 37 | from twisted.internet import reactor | ||
420 | 38 | reactor.run() | ||
421 | 39 | |||
422 | 40 | |||
423 | 41 | def main_quit(app): | ||
424 | 42 | """Stop the mainloop.""" | ||
425 | 43 | from twisted.internet import reactor | ||
426 | 44 | reactor.stop() | ||
427 | 45 | |||
428 | 46 | |||
429 | 47 | # pylint: enable=E1101 | ||
430 | 27 | 48 | ||
431 | === modified file 'ubuntuone/controlpanel/gui/qt/systray.py' | |||
432 | --- ubuntuone/controlpanel/gui/qt/systray.py 2011-12-27 13:54:49 +0000 | |||
433 | +++ ubuntuone/controlpanel/gui/qt/systray.py 2012-02-07 14:03:19 +0000 | |||
434 | @@ -25,7 +25,7 @@ | |||
435 | 25 | 25 | ||
436 | 26 | """System notification icon.""" | 26 | """System notification icon.""" |
437 | 27 | 27 | ||
439 | 28 | def __init__(self, window=None): | 28 | def __init__(self, window=None, close_callback=lambda: None): |
440 | 29 | super(TrayIcon, self).__init__(None) | 29 | super(TrayIcon, self).__init__(None) |
441 | 30 | self.setIcon(QtGui.QIcon(":/u1icon.png")) | 30 | self.setIcon(QtGui.QIcon(":/u1icon.png")) |
442 | 31 | self.setVisible(True) | 31 | self.setVisible(True) |
443 | @@ -41,6 +41,8 @@ | |||
444 | 41 | self.context_menu.addAction(self.quit) | 41 | self.context_menu.addAction(self.quit) |
445 | 42 | self.setContextMenu(self.context_menu) | 42 | self.setContextMenu(self.context_menu) |
446 | 43 | 43 | ||
447 | 44 | self.close_callback = close_callback | ||
448 | 45 | |||
449 | 44 | def on_activated(self, reason): | 46 | def on_activated(self, reason): |
450 | 45 | """The user activated the icon.""" | 47 | """The user activated the icon.""" |
451 | 46 | if reason == self.Trigger: # Left-click | 48 | if reason == self.Trigger: # Left-click |
452 | @@ -73,7 +75,4 @@ | |||
453 | 73 | except: | 75 | except: |
454 | 74 | # Maybe it was not running? | 76 | # Maybe it was not running? |
455 | 75 | pass | 77 | pass |
460 | 76 | # pylint: disable=W0404 | 78 | self.close_callback() |
457 | 77 | from twisted.internet import reactor | ||
458 | 78 | # pylint: enable=W0404 | ||
459 | 79 | reactor.stop() | ||
461 | 80 | 79 | ||
462 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/__init__.py' | |||
463 | --- ubuntuone/controlpanel/gui/qt/tests/__init__.py 2012-01-18 19:52:25 +0000 | |||
464 | +++ ubuntuone/controlpanel/gui/qt/tests/__init__.py 2012-02-07 14:03:19 +0000 | |||
465 | @@ -20,7 +20,6 @@ | |||
466 | 20 | 20 | ||
467 | 21 | import logging | 21 | import logging |
468 | 22 | import os | 22 | import os |
469 | 23 | import urllib | ||
470 | 24 | 23 | ||
471 | 25 | from PyQt4 import QtGui, QtCore | 24 | from PyQt4 import QtGui, QtCore |
472 | 26 | from twisted.internet import defer | 25 | from twisted.internet import defer |
473 | @@ -28,8 +27,7 @@ | |||
474 | 28 | 27 | ||
475 | 29 | from ubuntuone.controlpanel import backend, cache | 28 | from ubuntuone.controlpanel import backend, cache |
476 | 30 | from ubuntuone.controlpanel.tests import TestCase, EXPECTED_ACCOUNT_INFO, TOKEN | 29 | from ubuntuone.controlpanel.tests import TestCase, EXPECTED_ACCOUNT_INFO, TOKEN |
479 | 31 | from ubuntuone.controlpanel.gui import qt, UBUNTUONE_FROM_OAUTH | 30 | from ubuntuone.controlpanel.gui import qt |
478 | 32 | from ubuntuone.controlpanel.gui.qt import gotoweb | ||
480 | 33 | from ubuntuone.controlpanel.gui.tests import FakedObject, USER_HOME | 31 | from ubuntuone.controlpanel.gui.tests import FakedObject, USER_HOME |
481 | 34 | 32 | ||
482 | 35 | # Attribute 'yyy' defined outside __init__, access to a protected member | 33 | # Attribute 'yyy' defined outside __init__, access to a protected member |
483 | @@ -107,19 +105,31 @@ | |||
484 | 107 | 105 | ||
485 | 108 | next_result = [] | 106 | next_result = [] |
486 | 109 | exposed_methods = [ | 107 | exposed_methods = [ |
496 | 110 | 'account_info', # account | 108 | 'account_info', |
497 | 111 | 'devices_info', 'device_names_info', # devices | 109 | 'build_signed_iri', |
498 | 112 | 'change_device_settings', 'remove_device', | 110 | 'change_device_settings', |
499 | 113 | 'volumes_info', 'change_volume_settings', # volumes | 111 | 'change_file_sync_settings', |
500 | 114 | 'create_folder', 'validate_path_for_folder', | 112 | 'change_replication_settings', |
501 | 115 | 'replications_info', 'change_replication_settings', # replications | 113 | 'change_volume_settings', |
502 | 116 | 'file_sync_status', 'enable_files', 'disable_files', # files | 114 | 'connect_files', |
503 | 117 | 'connect_files', 'disconnect_files', | 115 | 'create_folder', |
504 | 118 | 'restart_files', 'start_files', 'stop_files', | 116 | 'device_names_info', |
505 | 117 | 'devices_info', | ||
506 | 118 | 'disable_files', | ||
507 | 119 | 'disconnect_files', | ||
508 | 120 | 'enable_files', | ||
509 | 119 | 'file_sync_settings_info', | 121 | 'file_sync_settings_info', |
511 | 120 | 'change_file_sync_settings', | 122 | 'file_sync_status', |
512 | 123 | 'login', | ||
513 | 124 | 'remove_device', | ||
514 | 125 | 'replications_info', | ||
515 | 126 | 'restart_files', | ||
516 | 121 | 'restore_file_sync_settings', | 127 | 'restore_file_sync_settings', |
518 | 122 | 'shutdown', 'login', | 128 | 'shutdown', |
519 | 129 | 'start_files', | ||
520 | 130 | 'stop_files', | ||
521 | 131 | 'validate_path_for_folder', | ||
522 | 132 | 'volumes_info', | ||
523 | 123 | ] | 133 | ] |
524 | 124 | 134 | ||
525 | 125 | def get_credentials(self): | 135 | def get_credentials(self): |
526 | @@ -131,6 +141,9 @@ | |||
527 | 131 | """Fake home return.""" | 141 | """Fake home return.""" |
528 | 132 | return USER_HOME | 142 | return USER_HOME |
529 | 133 | 143 | ||
530 | 144 | def build_signed_iri(self, iri): | ||
531 | 145 | """Fake iri signing.""" | ||
532 | 146 | |||
533 | 134 | 147 | ||
534 | 135 | class CrashyBackendException(Exception): | 148 | class CrashyBackendException(Exception): |
535 | 136 | """A faked backend crash.""" | 149 | """A faked backend crash.""" |
536 | @@ -284,19 +297,14 @@ | |||
537 | 284 | 297 | ||
538 | 285 | def assert_uri_hook_called(self, button, url): | 298 | def assert_uri_hook_called(self, button, url): |
539 | 286 | """Check that uri_hook was called with 'url' when clicking 'button'.""" | 299 | """Check that uri_hook was called with 'url' when clicking 'button'.""" |
541 | 287 | fake_time = 12345678 | 300 | self.patch(self.ui.backend, 'next_result', url) |
542 | 288 | self.patch(qt, 'uri_hook', self._set_called) | 301 | self.patch(qt, 'uri_hook', self._set_called) |
543 | 289 | self.patch(gotoweb.timestamp_checker, "get_faithful_time", | ||
544 | 290 | lambda: defer.succeed(fake_time)) | ||
545 | 291 | button.click() | 302 | button.click() |
546 | 292 | 303 | ||
547 | 293 | self.assertEqual(len(self._called), 2, 'uri_hook must be called.') | 304 | self.assertEqual(len(self._called), 2, 'uri_hook must be called.') |
548 | 294 | self.assertEqual(len(self._called[0]), 1, 'uri_hook must be called.') | 305 | self.assertEqual(len(self._called[0]), 1, 'uri_hook must be called.') |
549 | 295 | actual_url = self._called[0][0] | 306 | actual_url = self._called[0][0] |
554 | 296 | if actual_url.startswith(UBUNTUONE_FROM_OAUTH): | 307 | self.assertEqual(actual_url, url) |
551 | 297 | self.assertIn(urllib.urlencode({'next': url}), actual_url) | ||
552 | 298 | else: | ||
553 | 299 | self.assertEqual(actual_url, url) | ||
555 | 300 | 308 | ||
556 | 301 | def test_init_loads_ui(self, expected_setup_ui=None): | 309 | def test_init_loads_ui(self, expected_setup_ui=None): |
557 | 302 | """The __init__ method loads the ui.""" | 310 | """The __init__ method loads the ui.""" |
558 | 303 | 311 | ||
559 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py' | |||
560 | --- ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py 2011-10-06 20:33:42 +0000 | |||
561 | +++ ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py 2012-02-07 14:03:19 +0000 | |||
562 | @@ -20,7 +20,7 @@ | |||
563 | 20 | 20 | ||
564 | 21 | from twisted.internet import defer | 21 | from twisted.internet import defer |
565 | 22 | 22 | ||
567 | 23 | from ubuntuone.controlpanel.gui import qt, UBUNTUONE_LINK | 23 | from ubuntuone.controlpanel.gui import qt |
568 | 24 | from ubuntuone.controlpanel.gui.qt import gotoweb as gui | 24 | from ubuntuone.controlpanel.gui.qt import gotoweb as gui |
569 | 25 | from ubuntuone.controlpanel.gui.qt.tests import ( | 25 | from ubuntuone.controlpanel.gui.qt.tests import ( |
570 | 26 | BaseTestCase, | 26 | BaseTestCase, |
571 | @@ -53,11 +53,14 @@ | |||
572 | 53 | 53 | ||
573 | 54 | def test_open_uri_when_clicked(self): | 54 | def test_open_uri_when_clicked(self): |
574 | 55 | """When clicking the button, the uri is opened.""" | 55 | """When clicking the button, the uri is opened.""" |
575 | 56 | expected_iri = 'foo-bar-baz' | ||
576 | 57 | self.patch(self.ui.backend, 'build_signed_iri', lambda i: expected_iri) | ||
577 | 56 | self.patch(qt, 'uri_hook', self._set_called) | 58 | self.patch(qt, 'uri_hook', self._set_called) |
578 | 57 | self.ui.uri = 'yadda-yadda-yoo' | 59 | self.ui.uri = 'yadda-yadda-yoo' |
579 | 60 | |||
580 | 58 | self.ui.click() | 61 | self.ui.click() |
581 | 59 | 62 | ||
583 | 60 | self.assertEqual(self._called, ((self.ui.uri,), {})) | 63 | self.assertEqual(self._called, ((expected_iri,), {})) |
584 | 61 | 64 | ||
585 | 62 | def test_do_nothing_on_clicked_if_uri_is_none(self): | 65 | def test_do_nothing_on_clicked_if_uri_is_none(self): |
586 | 63 | """When clicking the button, if the uri is None, do nothing.""" | 66 | """When clicking the button, if the uri is None, do nothing.""" |
587 | @@ -66,53 +69,3 @@ | |||
588 | 66 | self.ui.click() | 69 | self.ui.click() |
589 | 67 | 70 | ||
590 | 68 | self.assertEqual(self._called, False) | 71 | self.assertEqual(self._called, False) |
591 | 69 | |||
592 | 70 | |||
593 | 71 | class SignUrlTestCase(GoToWebButtonTestCase): | ||
594 | 72 | """The test suite for the sign url management.""" | ||
595 | 73 | |||
596 | 74 | @defer.inlineCallbacks | ||
597 | 75 | def setUp(self): | ||
598 | 76 | yield super(SignUrlTestCase, self).setUp() | ||
599 | 77 | self.patch(qt, 'uri_hook', lambda url: None) | ||
600 | 78 | self.patch(gui, 'sign_url', self._set_called) | ||
601 | 79 | self.creds = yield self.ui.backend.get_credentials() | ||
602 | 80 | assert len(self.creds) > 0 | ||
603 | 81 | |||
604 | 82 | def test_without_ubuntuone_prefix(self): | ||
605 | 83 | """If given url is not an ubuntuone url, don't sign it.""" | ||
606 | 84 | self.ui.uri = 'bad_prefix' + UBUNTUONE_LINK | ||
607 | 85 | self.ui.click() | ||
608 | 86 | |||
609 | 87 | self.assertFalse(self._called) | ||
610 | 88 | |||
611 | 89 | def test_with_ubuntuone_prefix(self): | ||
612 | 90 | """If given url is an ubuntuone url, sign it.""" | ||
613 | 91 | fake_time = 12345 | ||
614 | 92 | self.ui.uri = UBUNTUONE_LINK + 'foo' | ||
615 | 93 | self.patch(gui.timestamp_checker, "get_faithful_time", | ||
616 | 94 | lambda: defer.succeed(fake_time)) | ||
617 | 95 | self.ui.click() | ||
618 | 96 | |||
619 | 97 | expected_call = ((self.ui.uri, self.creds, fake_time), {}) | ||
620 | 98 | self.assertEqual(self._called, expected_call) | ||
621 | 99 | |||
622 | 100 | |||
623 | 101 | class SignUrlNoCredsTestCase(SignUrlTestCase): | ||
624 | 102 | """The test suite for the sign url management when there are no creds.""" | ||
625 | 103 | |||
626 | 104 | @defer.inlineCallbacks | ||
627 | 105 | def setUp(self): | ||
628 | 106 | yield super(SignUrlNoCredsTestCase, self).setUp() | ||
629 | 107 | self.patch(self.ui.backend, 'get_credentials', lambda: {}) | ||
630 | 108 | |||
631 | 109 | def test_with_ubuntuone_prefix(self): | ||
632 | 110 | """If given url is an ubuntuone url, don't sign it. | ||
633 | 111 | |||
634 | 112 | Since we have no credentials, the url should not be signed. | ||
635 | 113 | |||
636 | 114 | """ | ||
637 | 115 | self.ui.uri = UBUNTUONE_LINK + 'foo' | ||
638 | 116 | self.ui.click() | ||
639 | 117 | |||
640 | 118 | self.assertFalse(self._called) | ||
641 | 119 | 72 | ||
642 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_signin.py' | |||
643 | --- ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2011-09-07 17:56:17 +0000 | |||
644 | +++ ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2012-02-07 14:03:19 +0000 | |||
645 | @@ -20,7 +20,6 @@ | |||
646 | 20 | 20 | ||
647 | 21 | from twisted.internet import defer | 21 | from twisted.internet import defer |
648 | 22 | 22 | ||
649 | 23 | from ubuntuone.controlpanel.gui import qt | ||
650 | 24 | from ubuntuone.controlpanel.gui.qt import signin as gui | 23 | from ubuntuone.controlpanel.gui.qt import signin as gui |
651 | 25 | from ubuntuone.controlpanel.gui.qt.tests import ( | 24 | from ubuntuone.controlpanel.gui.qt.tests import ( |
652 | 26 | CrashyBackend, | 25 | CrashyBackend, |
653 | @@ -107,10 +106,8 @@ | |||
654 | 107 | 106 | ||
655 | 108 | def test_forgot_password_button(self): | 107 | def test_forgot_password_button(self): |
656 | 109 | """When clicking the forgot passsword btn, the proper url is opened.""" | 108 | """When clicking the forgot passsword btn, the proper url is opened.""" |
661 | 110 | self.patch(qt, 'uri_hook', self._set_called) | 109 | self.assert_uri_hook_called(self.ui.ui.forgot_password_button, |
662 | 111 | self.ui.ui.forgot_password_button.click() | 110 | gui.RESET_PASSWORD_LINK) |
659 | 112 | |||
660 | 113 | self.assertEqual(self._called, ((gui.RESET_PASSWORD_LINK,), {})) | ||
663 | 114 | 111 | ||
664 | 115 | 112 | ||
665 | 116 | class SignInButtonPanelTestCase(BaseSignInPanelTestCase): | 113 | class SignInButtonPanelTestCase(BaseSignInPanelTestCase): |
666 | 117 | 114 | ||
667 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_start.py' | |||
668 | --- ubuntuone/controlpanel/gui/qt/tests/test_start.py 2011-09-15 15:37:04 +0000 | |||
669 | +++ ubuntuone/controlpanel/gui/qt/tests/test_start.py 2012-02-07 14:03:19 +0000 | |||
670 | @@ -1,91 +1,89 @@ | |||
762 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
763 | 2 | 2 | ||
764 | 3 | # Author: Roberto Alsina <roberto.alsina@canonical.com> | 3 | # Author: Roberto Alsina <roberto.alsina@canonical.com> |
765 | 4 | # | 4 | # |
766 | 5 | # Copyright 2011 Canonical Ltd. | 5 | # Copyright 2011 Canonical Ltd. |
767 | 6 | # | 6 | # |
768 | 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 |
769 | 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 |
770 | 9 | # by the Free Software Foundation. | 9 | # by the Free Software Foundation. |
771 | 10 | # | 10 | # |
772 | 11 | # This program is distributed in the hope that it will be useful, but | 11 | # This program is distributed in the hope that it will be useful, but |
773 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of |
774 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
775 | 14 | # PURPOSE. See the GNU General Public License for more details. | 14 | # PURPOSE. See the GNU General Public License for more details. |
776 | 15 | # | 15 | # |
777 | 16 | # You should have received a copy of the GNU General Public License along | 16 | # You should have received a copy of the GNU General Public License along |
778 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
779 | 18 | 18 | ||
780 | 19 | """Tests for the start function.""" | 19 | """Tests for the start function.""" |
781 | 20 | 20 | ||
782 | 21 | from twisted.internet import defer | 21 | from twisted.internet import defer |
783 | 22 | 22 | ||
784 | 23 | from ubuntuone.controlpanel.gui.qt import gui | 23 | from ubuntuone.controlpanel.gui.qt import gui |
785 | 24 | from ubuntuone.controlpanel.gui.qt.tests import NO_OP | 24 | from ubuntuone.controlpanel.tests import TestCase |
786 | 25 | from ubuntuone.controlpanel.tests import TestCase | 25 | |
787 | 26 | 26 | ||
788 | 27 | 27 | class FakeThing(object): | |
789 | 28 | class FakeThing(object): | 28 | |
790 | 29 | 29 | """A fake thing.""" | |
791 | 30 | """A fake thing.""" | 30 | |
792 | 31 | 31 | def __init__(self): | |
793 | 32 | def __init__(self): | 32 | self.args = [] |
794 | 33 | self.args = [] | 33 | self.shown = False |
795 | 34 | self.shown = False | 34 | |
796 | 35 | 35 | def __call__(self, *args, **kwargs): | |
797 | 36 | def __call__(self, *args, **kwargs): | 36 | self.args.append((args, kwargs)) |
798 | 37 | self.args.append((args, kwargs)) | 37 | return self |
799 | 38 | return self | 38 | |
800 | 39 | 39 | def show(self): | |
801 | 40 | def show(self): | 40 | """Show.""" |
802 | 41 | """Show.""" | 41 | self.shown = True |
803 | 42 | self.shown = True | 42 | |
804 | 43 | 43 | ||
805 | 44 | 44 | class StartTestCase(TestCase): | |
806 | 45 | class FakeReactor(object): | 45 | """Test the qt control panel.""" |
807 | 46 | """A fake reactor.""" | 46 | |
808 | 47 | 47 | @defer.inlineCallbacks | |
809 | 48 | def run(self): | 48 | def setUp(self): |
810 | 49 | """Start.""" | 49 | yield super(StartTestCase, self).setUp() |
811 | 50 | 50 | self.main_window = FakeThing() | |
812 | 51 | def stop(self): | 51 | self.tray_icon = FakeThing() |
813 | 52 | """Stop.""" | 52 | self.patch(gui, "MainWindow", self.main_window) |
814 | 53 | 53 | self.patch(gui, "TrayIcon", self.tray_icon) | |
815 | 54 | 54 | ||
816 | 55 | class StartTestCase(TestCase): | 55 | def close_cb(self): |
817 | 56 | """Test the qt control panel.""" | 56 | """A dummy close callback.""" |
818 | 57 | 57 | ||
819 | 58 | @defer.inlineCallbacks | 58 | def test_minimized(self): |
820 | 59 | def setUp(self): | 59 | """Test behaviour with minimized=True.""" |
821 | 60 | yield super(StartTestCase, self).setUp() | 60 | gui.start(close_callback=self.close_cb, |
822 | 61 | self.main_window = FakeThing() | 61 | minimized=True, with_icon=True) |
823 | 62 | self.tray_icon = FakeThing() | 62 | kwargs = {'close_callback': self.close_cb, 'window': None} |
824 | 63 | self.patch(gui, "MainWindow", self.main_window) | 63 | self.assertEqual(self.tray_icon.args, [((), kwargs)]) |
825 | 64 | self.patch(gui, "TrayIcon", self.tray_icon) | 64 | self.assertEqual(self.main_window.args, []) |
826 | 65 | 65 | ||
827 | 66 | def test_minimized(self): | 66 | def test_with_icon(self): |
828 | 67 | """Test behaviour with minimized=True.""" | 67 | """Test behaviour with with_icon=True.""" |
829 | 68 | gui.start(NO_OP, minimized=True, with_icon=True) | 68 | gui.start(close_callback=self.close_cb, |
830 | 69 | self.assertEqual(self.tray_icon.args, [((), {'window': None})]) | 69 | with_icon=True, minimized=False) |
831 | 70 | self.assertEqual(self.main_window.args, []) | 70 | kwargs = {'close_callback': self.close_cb, 'window': self.main_window} |
832 | 71 | 71 | self.assertEqual(self.main_window.args, [((), {})]) | |
833 | 72 | def test_with_icon(self): | 72 | self.assertEqual(self.tray_icon.args, [((), kwargs)]) |
834 | 73 | """Test behaviour with with_icon=True.""" | 73 | |
835 | 74 | gui.start(NO_OP, with_icon=True, minimized=False) | 74 | def test_both_false(self): |
836 | 75 | self.assertEqual(self.main_window.args, [((), {})]) | 75 | """Test behaviour when with_icon and minimized are False.""" |
837 | 76 | self.assertEqual(self.tray_icon.args, [((), | 76 | gui.start(close_callback=self.close_cb, |
838 | 77 | {'window': self.main_window})]) | 77 | with_icon=False, minimized=False) |
839 | 78 | 78 | # Should be called | |
840 | 79 | def test_both_false(self): | 79 | self.assertNotEqual(self.main_window.args, []) |
841 | 80 | """Test behaviour when with_icon and minimized are False.""" | 80 | # Should not be called |
842 | 81 | gui.start(NO_OP, with_icon=False, minimized=False) | 81 | self.assertEqual(self.tray_icon.args, []) |
843 | 82 | # Should be called | 82 | |
844 | 83 | self.assertNotEqual(self.main_window.args, []) | 83 | def test_both_true(self): |
845 | 84 | # Should not be called | 84 | """Test behaviour when with_icon and minimized are True.""" |
846 | 85 | self.assertEqual(self.tray_icon.args, []) | 85 | gui.start(close_callback=self.close_cb, |
847 | 86 | 86 | with_icon=True, minimized=True) | |
848 | 87 | def test_both_true(self): | 87 | kwargs = {'close_callback': self.close_cb, 'window': None} |
849 | 88 | """Test behaviour when with_icon and minimized are True.""" | 88 | self.assertEqual(self.tray_icon.args, [((), kwargs)]) |
850 | 89 | gui.start(NO_OP, with_icon=True, minimized=True) | 89 | self.assertEqual(self.main_window.args, []) |
760 | 90 | self.assertEqual(self.tray_icon.args, [((), {'window': None})]) | ||
761 | 91 | self.assertEqual(self.main_window.args, []) | ||
851 | 92 | 90 | ||
852 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_systray.py' | |||
853 | --- ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-01-02 19:38:14 +0000 | |||
854 | +++ ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-02-07 14:03:19 +0000 | |||
855 | @@ -19,7 +19,6 @@ | |||
856 | 19 | """Tests for the notification area icon.""" | 19 | """Tests for the notification area icon.""" |
857 | 20 | 20 | ||
858 | 21 | from PyQt4 import QtGui | 21 | from PyQt4 import QtGui |
859 | 22 | from twisted.internet import reactor | ||
860 | 23 | from twisted.internet.defer import inlineCallbacks | 22 | from twisted.internet.defer import inlineCallbacks |
861 | 24 | 23 | ||
862 | 25 | from ubuntuone.controlpanel.gui.qt import systray | 24 | from ubuntuone.controlpanel.gui.qt import systray |
863 | @@ -65,7 +64,6 @@ | |||
864 | 65 | """Quit should call SyncDaemonTool.quit().""" | 64 | """Quit should call SyncDaemonTool.quit().""" |
865 | 66 | st = FakeSDTool() | 65 | st = FakeSDTool() |
866 | 67 | self.patch(systray, "SyncDaemonTool", lambda: st) | 66 | self.patch(systray, "SyncDaemonTool", lambda: st) |
867 | 68 | self.patch(reactor, "stop", lambda: None) | ||
868 | 69 | tray = systray.TrayIcon() | 67 | tray = systray.TrayIcon() |
869 | 70 | yield tray.stop() | 68 | yield tray.stop() |
870 | 71 | self.assertTrue(st.called) | 69 | self.assertTrue(st.called) |
871 | 72 | 70 | ||
872 | === removed file 'ubuntuone/controlpanel/gui/tests/test_url_sign.py' | |||
873 | --- ubuntuone/controlpanel/gui/tests/test_url_sign.py 2011-10-05 23:12:23 +0000 | |||
874 | +++ ubuntuone/controlpanel/gui/tests/test_url_sign.py 1970-01-01 00:00:00 +0000 | |||
875 | @@ -1,102 +0,0 @@ | |||
876 | 1 | # -*- coding: utf-8 -*- | ||
877 | 2 | |||
878 | 3 | # Authors: Roberto Alsina <roberto.alsina@canonical.com> | ||
879 | 4 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
880 | 5 | # | ||
881 | 6 | # Copyright 2011 Canonical Ltd. | ||
882 | 7 | # | ||
883 | 8 | # This program is free software: you can redistribute it and/or modify it | ||
884 | 9 | # under the terms of the GNU General Public License version 3, as published | ||
885 | 10 | # by the Free Software Foundation. | ||
886 | 11 | # | ||
887 | 12 | # This program is distributed in the hope that it will be useful, but | ||
888 | 13 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
889 | 14 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
890 | 15 | # PURPOSE. See the GNU General Public License for more details. | ||
891 | 16 | # | ||
892 | 17 | # You should have received a copy of the GNU General Public License along | ||
893 | 18 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
894 | 19 | |||
895 | 20 | """Tests for the url signing function.""" | ||
896 | 21 | |||
897 | 22 | from urlparse import urlparse, parse_qs | ||
898 | 23 | |||
899 | 24 | from ubuntuone.controlpanel.tests import TestCase | ||
900 | 25 | from ubuntuone.controlpanel.gui import ( | ||
901 | 26 | sign_url, | ||
902 | 27 | UBUNTUONE_FROM_OAUTH, | ||
903 | 28 | ) | ||
904 | 29 | |||
905 | 30 | TOKEN = {u'consumer_key': u'consumer_key', | ||
906 | 31 | u'consumer_secret': u'consumer_secret', | ||
907 | 32 | u'token_name': u'test_token', | ||
908 | 33 | u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo', | ||
909 | 34 | u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'} | ||
910 | 35 | |||
911 | 36 | SAMPLE_SIGNED = UBUNTUONE_FROM_OAUTH + '?oauth_nonce=' \ | ||
912 | 37 | '36886134&oauth_timestamp=1310671062&oauth_consumer_key=consumer_key&' \ | ||
913 | 38 | 'oauth_signature_method=HMAC-SHA1&next=%2Fblah&oauth_version=1.0&' \ | ||
914 | 39 | 'oauth_token=GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo&' \ | ||
915 | 40 | 'oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo%3D' | ||
916 | 41 | |||
917 | 42 | #pylint: disable=E1101 | ||
918 | 43 | |||
919 | 44 | |||
920 | 45 | class SignURLTestCase(TestCase): | ||
921 | 46 | |||
922 | 47 | """Test cases for the URL signing function.""" | ||
923 | 48 | |||
924 | 49 | def test_is_correct_domain(self): | ||
925 | 50 | """Test that we are using the right domain.""" | ||
926 | 51 | signed = sign_url("/blah", TOKEN) | ||
927 | 52 | parsed_signed = urlparse(signed) | ||
928 | 53 | parsed_sample = urlparse(SAMPLE_SIGNED) | ||
929 | 54 | self.assertEqual(parsed_signed.netloc, parsed_sample.netloc) | ||
930 | 55 | |||
931 | 56 | def test_is_correct_path(self): | ||
932 | 57 | """Test that we are using the right path in the URL.""" | ||
933 | 58 | signed = sign_url("/blah", TOKEN) | ||
934 | 59 | parsed_signed = urlparse(signed) | ||
935 | 60 | parsed_sample = urlparse(SAMPLE_SIGNED) | ||
936 | 61 | self.assertEqual(parsed_signed.path, parsed_sample.path) | ||
937 | 62 | |||
938 | 63 | def test_is_correct_scheme(self): | ||
939 | 64 | """Test that we are using the right scheme.""" | ||
940 | 65 | signed = sign_url("/blah", TOKEN) | ||
941 | 66 | parsed_signed = urlparse(signed) | ||
942 | 67 | parsed_sample = urlparse(SAMPLE_SIGNED) | ||
943 | 68 | |||
944 | 69 | self.assertEqual(parsed_signed.scheme, parsed_sample.scheme) | ||
945 | 70 | |||
946 | 71 | def test_correct_query(self): | ||
947 | 72 | """Test the invariant parts of the signed URL.""" | ||
948 | 73 | signed = sign_url("/blah", TOKEN) | ||
949 | 74 | parsed_signed = urlparse(signed) | ||
950 | 75 | parsed_sample = urlparse(SAMPLE_SIGNED) | ||
951 | 76 | signed_query = parse_qs(parsed_signed.query) | ||
952 | 77 | sample_query = parse_qs(parsed_sample.query) | ||
953 | 78 | |||
954 | 79 | for key in ('next', | ||
955 | 80 | 'oauth_consumer_key', | ||
956 | 81 | 'oauth_signature_method', | ||
957 | 82 | 'oauth_token', | ||
958 | 83 | 'oauth_version'): | ||
959 | 84 | self.assertEqual("%s=%s" % (key, signed_query[key]), | ||
960 | 85 | "%s=%s" % (key, sample_query[key])) | ||
961 | 86 | |||
962 | 87 | def test_url_with_query(self): | ||
963 | 88 | """Test that we are using the right scheme.""" | ||
964 | 89 | signed = sign_url("/blah?foo=bar", TOKEN) | ||
965 | 90 | parsed_signed = urlparse(signed) | ||
966 | 91 | signed_query = parse_qs(parsed_signed.query) | ||
967 | 92 | |||
968 | 93 | self.assertEqual(signed_query['next'], ['/blah?foo=bar']) | ||
969 | 94 | |||
970 | 95 | def test_uses_timestamper(self): | ||
971 | 96 | """Test that the signed url is using the server-relative timestamp.""" | ||
972 | 97 | timestamp = 999 | ||
973 | 98 | signed = sign_url("/blah?foo=bar", TOKEN, timestamp) | ||
974 | 99 | parsed_signed = urlparse(signed) | ||
975 | 100 | signed_query = parse_qs(parsed_signed.query) | ||
976 | 101 | |||
977 | 102 | self.assertEqual(signed_query['oauth_timestamp'], [str(timestamp)]) | ||
978 | 103 | 0 | ||
979 | === modified file 'ubuntuone/controlpanel/tests/test_backend.py' | |||
980 | --- ubuntuone/controlpanel/tests/test_backend.py 2012-01-26 12:44:10 +0000 | |||
981 | +++ ubuntuone/controlpanel/tests/test_backend.py 2012-02-07 14:03:19 +0000 | |||
982 | @@ -43,6 +43,8 @@ | |||
983 | 43 | FILE_SYNC_SYNCING, | 43 | FILE_SYNC_SYNCING, |
984 | 44 | FILE_SYNC_UNKNOWN, | 44 | FILE_SYNC_UNKNOWN, |
985 | 45 | MSG_KEY, STATUS_KEY, | 45 | MSG_KEY, STATUS_KEY, |
986 | 46 | UBUNTUONE_FROM_OAUTH, | ||
987 | 47 | UBUNTUONE_LINK, | ||
988 | 46 | ) | 48 | ) |
989 | 47 | from ubuntuone.controlpanel.tests import (TestCase, | 49 | from ubuntuone.controlpanel.tests import (TestCase, |
990 | 48 | EMPTY_DESCRIPTION_JSON, | 50 | EMPTY_DESCRIPTION_JSON, |
991 | @@ -65,6 +67,12 @@ | |||
992 | 65 | USER_HOME, | 67 | USER_HOME, |
993 | 66 | ) | 68 | ) |
994 | 67 | 69 | ||
995 | 70 | SAMPLE_SIGNED = UBUNTUONE_FROM_OAUTH + '?oauth_nonce=' \ | ||
996 | 71 | '36886134&oauth_timestamp=1310671062&oauth_consumer_key=consumer_key&' \ | ||
997 | 72 | 'oauth_signature_method=HMAC-SHA1&next=%2Fblah&oauth_version=1.0&' \ | ||
998 | 73 | 'oauth_token=GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo&' \ | ||
999 | 74 | 'oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo%3D' | ||
1000 | 75 | |||
1001 | 68 | 76 | ||
1002 | 69 | # pylint: disable=E1101, W0201, W0212 | 77 | # pylint: disable=E1101, W0201, W0212 |
1003 | 70 | 78 | ||
1004 | @@ -104,6 +112,13 @@ | |||
1005 | 104 | result = simplejson.loads(self.results[method]) | 112 | result = simplejson.loads(self.results[method]) |
1006 | 105 | return defer.succeed(result) | 113 | return defer.succeed(result) |
1007 | 106 | 114 | ||
1008 | 115 | @defer.inlineCallbacks | ||
1009 | 116 | def build_signed_iri(self, iri, params): | ||
1010 | 117 | """Fake the IRI signing.""" | ||
1011 | 118 | creds = yield self.get_credentials() | ||
1012 | 119 | result = u'%s-%s-%s' % (iri, unicode(creds), unicode(params)) | ||
1013 | 120 | defer.returnValue(result) | ||
1014 | 121 | |||
1015 | 107 | 122 | ||
1016 | 108 | class MockLoginClient(CallRecorder): | 123 | class MockLoginClient(CallRecorder): |
1017 | 109 | """A mock login_client module.""" | 124 | """A mock login_client module.""" |
1018 | @@ -347,7 +362,7 @@ | |||
1019 | 347 | @defer.inlineCallbacks | 362 | @defer.inlineCallbacks |
1020 | 348 | def setUp(self): | 363 | def setUp(self): |
1021 | 349 | yield super(BackendBasicTestCase, self).setUp() | 364 | yield super(BackendBasicTestCase, self).setUp() |
1023 | 350 | self.patch(backend, "web_client_factory", MockWebClient) | 365 | self.patch(backend, "WebClient", MockWebClient) |
1024 | 351 | self.patch(backend, "CredentialsManagementTool", MockLoginClient) | 366 | self.patch(backend, "CredentialsManagementTool", MockLoginClient) |
1025 | 352 | self.patch(backend.sd_client, "SyncDaemonClient", MockSDClient) | 367 | self.patch(backend.sd_client, "SyncDaemonClient", MockSDClient) |
1026 | 353 | self.patch(backend, "replication_client", MockReplicationClient()) | 368 | self.patch(backend, "replication_client", MockReplicationClient()) |
1027 | @@ -368,12 +383,14 @@ | |||
1028 | 368 | @inlineCallbacks | 383 | @inlineCallbacks |
1029 | 369 | def test_get_token(self): | 384 | def test_get_token(self): |
1030 | 370 | """The get_token method returns the right token.""" | 385 | """The get_token method returns the right token.""" |
1031 | 386 | self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN)) | ||
1032 | 371 | token = yield self.be.get_token() | 387 | token = yield self.be.get_token() |
1033 | 372 | self.assertEqual(token, TOKEN["token"]) | 388 | self.assertEqual(token, TOKEN["token"]) |
1034 | 373 | 389 | ||
1035 | 374 | @inlineCallbacks | 390 | @inlineCallbacks |
1036 | 375 | def test_device_is_local(self): | 391 | def test_device_is_local(self): |
1037 | 376 | """The device_is_local returns the right result.""" | 392 | """The device_is_local returns the right result.""" |
1038 | 393 | self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN)) | ||
1039 | 377 | result = yield self.be.device_is_local(self.local_token) | 394 | result = yield self.be.device_is_local(self.local_token) |
1040 | 378 | self.assertTrue(result) | 395 | self.assertTrue(result) |
1041 | 379 | 396 | ||
1042 | @@ -408,6 +425,54 @@ | |||
1043 | 408 | self.assertIs(self.be.sd_client, self.be.sd_client) | 425 | self.assertIs(self.be.sd_client, self.be.sd_client) |
1044 | 409 | 426 | ||
1045 | 410 | 427 | ||
1046 | 428 | class SignIriTestCase(BackendBasicTestCase): | ||
1047 | 429 | """Test cases for the IRI signing function.""" | ||
1048 | 430 | |||
1049 | 431 | @defer.inlineCallbacks | ||
1050 | 432 | def setUp(self): | ||
1051 | 433 | yield super(SignIriTestCase, self).setUp() | ||
1052 | 434 | self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN)) | ||
1053 | 435 | |||
1054 | 436 | @inlineCallbacks | ||
1055 | 437 | def test_without_ubuntuone_prefix(self): | ||
1056 | 438 | """If given url is not an ubuntuone url, don't sign it.""" | ||
1057 | 439 | iri = 'bad_prefix' + UBUNTUONE_LINK | ||
1058 | 440 | result = yield self.be.build_signed_iri(iri) | ||
1059 | 441 | |||
1060 | 442 | self.assertEqual(result, iri) | ||
1061 | 443 | |||
1062 | 444 | @inlineCallbacks | ||
1063 | 445 | def test_with_ubuntuone_prefix(self): | ||
1064 | 446 | """If given url is an ubuntuone url, sign it.""" | ||
1065 | 447 | iri = UBUNTUONE_LINK + 'foo' | ||
1066 | 448 | result = yield self.be.build_signed_iri(iri) | ||
1067 | 449 | |||
1068 | 450 | expected = yield self.be.wc.build_signed_iri(UBUNTUONE_FROM_OAUTH, | ||
1069 | 451 | {'next': iri}) | ||
1070 | 452 | self.assertEqual(expected, result) | ||
1071 | 453 | |||
1072 | 454 | |||
1073 | 455 | class SignIriNoCredsTestCase(SignIriTestCase): | ||
1074 | 456 | """The test suite for the sign url management when there are no creds.""" | ||
1075 | 457 | |||
1076 | 458 | @defer.inlineCallbacks | ||
1077 | 459 | def setUp(self): | ||
1078 | 460 | yield super(SignIriNoCredsTestCase, self).setUp() | ||
1079 | 461 | self.patch(self.be, 'get_credentials', lambda: defer.succeed({})) | ||
1080 | 462 | |||
1081 | 463 | @inlineCallbacks | ||
1082 | 464 | def test_with_ubuntuone_prefix(self): | ||
1083 | 465 | """If given url is an ubuntuone url, don't sign it. | ||
1084 | 466 | |||
1085 | 467 | Since we have no credentials, the url should not be signed. | ||
1086 | 468 | |||
1087 | 469 | """ | ||
1088 | 470 | iri = UBUNTUONE_LINK + 'foo' | ||
1089 | 471 | result = yield self.be.build_signed_iri(iri) | ||
1090 | 472 | |||
1091 | 473 | self.assertEqual(result, iri) | ||
1092 | 474 | |||
1093 | 475 | |||
1094 | 411 | class BackendCredentialsTestCase(BackendBasicTestCase): | 476 | class BackendCredentialsTestCase(BackendBasicTestCase): |
1095 | 412 | """Credentials tests for the backend.""" | 477 | """Credentials tests for the backend.""" |
1096 | 413 | 478 | ||
1097 | 414 | 479 | ||
1098 | === modified file 'ubuntuone/controlpanel/tests/test_web_client.py' | |||
1099 | --- ubuntuone/controlpanel/tests/test_web_client.py 2011-10-07 14:36:14 +0000 | |||
1100 | +++ ubuntuone/controlpanel/tests/test_web_client.py 2012-02-07 14:03:19 +0000 | |||
1101 | @@ -1,9 +1,6 @@ | |||
1102 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
1103 | 2 | |||
1104 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1105 | 4 | # Authors: Natalia B. Bidart <nataliabidart@canonical.com> | ||
1106 | 5 | # | 2 | # |
1108 | 6 | # Copyright 2010 Canonical Ltd. | 3 | # Copyright 2011-2012 Canonical Ltd. |
1109 | 7 | # | 4 | # |
1110 | 8 | # This program is free software: you can redistribute it and/or modify it | 5 | # This program is free software: you can redistribute it and/or modify it |
1111 | 9 | # under the terms of the GNU General Public License version 3, as published | 6 | # under the terms of the GNU General Public License version 3, as published |
1112 | @@ -17,26 +14,38 @@ | |||
1113 | 17 | # You should have received a copy of the GNU General Public License along | 14 | # You should have received a copy of the GNU General Public License along |
1114 | 18 | # with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
1115 | 19 | 16 | ||
1119 | 20 | """Integration tests for the control panel backend webservice client.""" | 17 | """Integration tests for the webclient.""" |
1120 | 21 | 18 | ||
1121 | 22 | from twisted.application import internet, service | 19 | from urlparse import urlparse, parse_qs |
1122 | 20 | |||
1123 | 23 | from twisted.internet import defer | 21 | from twisted.internet import defer |
1128 | 24 | from twisted.internet.defer import inlineCallbacks | 22 | from twisted.web import resource |
1129 | 25 | from twisted.web import server, resource | 23 | |
1130 | 26 | 24 | from ubuntu_sso.utils.webclient.tests import BaseMockWebServer | |
1131 | 27 | from ubuntuone.controlpanel import web_client | 25 | |
1132 | 28 | from ubuntuone.controlpanel.tests import TestCase | 26 | from ubuntuone.controlpanel.tests import TestCase |
1133 | 27 | from ubuntuone.controlpanel.web_client import ( | ||
1134 | 28 | UnauthorizedError, | ||
1135 | 29 | WebClient, | ||
1136 | 30 | WebClientError, | ||
1137 | 31 | ) | ||
1138 | 29 | 32 | ||
1139 | 30 | 33 | ||
1140 | 31 | SAMPLE_KEY = "result" | 34 | SAMPLE_KEY = "result" |
1141 | 32 | SAMPLE_VALUE = "sample result" | 35 | SAMPLE_VALUE = "sample result" |
1142 | 33 | SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE) | 36 | SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE) |
1143 | 34 | SAMPLE_CREDENTIALS = dict( | 37 | SAMPLE_CREDENTIALS = dict( |
1148 | 35 | consumer_key="consumer key", | 38 | consumer_key="consumer_key", |
1149 | 36 | consumer_secret="consumer secret", | 39 | consumer_secret="consumer_secret", |
1150 | 37 | token="the real token", | 40 | token="the_real_token", |
1151 | 38 | token_secret="the token secret", | 41 | token_secret="the_token_secret", |
1152 | 39 | ) | 42 | ) |
1153 | 43 | SAMPLE_TARGET = u'http://example.com/' | ||
1154 | 44 | SAMPLE_SIGNED = SAMPLE_TARGET + u'?oauth_nonce=36886134&' \ | ||
1155 | 45 | 'oauth_timestamp=1328544832&oauth_consumer_key=%s&' \ | ||
1156 | 46 | 'oauth_signature_method=HMAC-SHA1&next=%%2Fblah&oauth_version=1.0&' \ | ||
1157 | 47 | 'oauth_token=%s&oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo3D' % \ | ||
1158 | 48 | (SAMPLE_CREDENTIALS['consumer_key'], SAMPLE_CREDENTIALS['token']) | ||
1159 | 40 | 49 | ||
1160 | 41 | 50 | ||
1161 | 42 | def sample_get_credentials(): | 51 | def sample_get_credentials(): |
1162 | @@ -63,11 +72,11 @@ | |||
1163 | 63 | return self.contents | 72 | return self.contents |
1164 | 64 | 73 | ||
1165 | 65 | 74 | ||
1168 | 66 | class MockWebService(object): | 75 | class MockWebServer(BaseMockWebServer): |
1169 | 67 | """A mock webservice for testing""" | 76 | """A mock webserver for the webclient tests.""" |
1170 | 68 | 77 | ||
1173 | 69 | def __init__(self): | 78 | def get_root_resource(self): |
1174 | 70 | """Start up this instance.""" | 79 | """Get the root resource with all the children.""" |
1175 | 71 | root = resource.Resource() | 80 | root = resource.Resource() |
1176 | 72 | devices_resource = MockResource() | 81 | devices_resource = MockResource() |
1177 | 73 | devices_resource.contents = SAMPLE_RESOURCE | 82 | devices_resource.contents = SAMPLE_RESOURCE |
1178 | @@ -77,91 +86,105 @@ | |||
1179 | 77 | "Unauthrorized", "Unauthrorized") | 86 | "Unauthrorized", "Unauthrorized") |
1180 | 78 | root.putChild("unauthorized", unauthorized) | 87 | root.putChild("unauthorized", unauthorized) |
1181 | 79 | 88 | ||
1200 | 80 | site = server.Site(root) | 89 | return root |
1183 | 81 | application = service.Application('web') | ||
1184 | 82 | self.service_collection = service.IServiceCollection(application) | ||
1185 | 83 | #pylint: disable=E1101 | ||
1186 | 84 | self.tcpserver = internet.TCPServer(0, site) | ||
1187 | 85 | self.tcpserver.setServiceParent(self.service_collection) | ||
1188 | 86 | self.service_collection.startService() | ||
1189 | 87 | |||
1190 | 88 | def get_url(self): | ||
1191 | 89 | """Build the url for this mock server.""" | ||
1192 | 90 | #pylint: disable=W0212 | ||
1193 | 91 | port_num = self.tcpserver._port.getHost().port | ||
1194 | 92 | return "http://localhost:%d/" % port_num | ||
1195 | 93 | |||
1196 | 94 | def stop(self): | ||
1197 | 95 | """Shut it down.""" | ||
1198 | 96 | #pylint: disable=E1101 | ||
1199 | 97 | return self.service_collection.stopService() | ||
1201 | 98 | 90 | ||
1202 | 99 | 91 | ||
1203 | 100 | class WebClientTestCase(TestCase): | 92 | class WebClientTestCase(TestCase): |
1204 | 101 | """Test for the webservice client.""" | 93 | """Test for the webservice client.""" |
1205 | 102 | 94 | ||
1207 | 103 | timeout = 8 | 95 | timeout = 5 |
1208 | 104 | 96 | ||
1210 | 105 | @inlineCallbacks | 97 | @defer.inlineCallbacks |
1211 | 106 | def setUp(self): | 98 | def setUp(self): |
1212 | 107 | yield super(WebClientTestCase, self).setUp() | 99 | yield super(WebClientTestCase, self).setUp() |
1217 | 108 | self.ws = MockWebService() | 100 | self.ws = MockWebServer() |
1218 | 109 | test_base_url = self.ws.get_url() | 101 | self.addCleanup(self.ws.stop) |
1219 | 110 | self.wc = web_client.web_client_factory(sample_get_credentials, | 102 | self.base_iri = self.ws.get_iri() |
1220 | 111 | test_base_url) | 103 | |
1221 | 104 | self.wc = WebClient(sample_get_credentials, base_url=self.base_iri) | ||
1222 | 112 | self.addCleanup(self.wc.shutdown) | 105 | self.addCleanup(self.wc.shutdown) |
1223 | 113 | self.addCleanup(self.ws.stop) | ||
1224 | 114 | web_module = web_client.web_client_module() | ||
1225 | 115 | if getattr(web_module, "timestamp_checker", None): | ||
1226 | 116 | fake_timestamp = 12345678 | ||
1227 | 117 | self.patch(web_module.timestamp_checker, "get_faithful_time", | ||
1228 | 118 | lambda: defer.succeed(fake_timestamp)) | ||
1229 | 119 | 106 | ||
1231 | 120 | @inlineCallbacks | 107 | @defer.inlineCallbacks |
1232 | 121 | def test_get_url(self): | 108 | def test_get_url(self): |
1233 | 122 | """A method is successfully called in the mock webservice.""" | 109 | """A method is successfully called in the mock webservice.""" |
1234 | 123 | result = yield self.wc.call_api("devices") | 110 | result = yield self.wc.call_api("devices") |
1235 | 124 | self.assertIn(SAMPLE_KEY, result) | 111 | self.assertIn(SAMPLE_KEY, result) |
1236 | 125 | self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY]) | 112 | self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY]) |
1237 | 126 | 113 | ||
1239 | 127 | @inlineCallbacks | 114 | @defer.inlineCallbacks |
1240 | 128 | def test_get_url_error(self): | 115 | def test_get_url_error(self): |
1241 | 129 | """The errback is called when there's some error.""" | 116 | """The errback is called when there's some error.""" |
1242 | 130 | yield self.assertFailure(self.wc.call_api("throwerror"), | 117 | yield self.assertFailure(self.wc.call_api("throwerror"), |
1244 | 131 | web_client.WebClientError) | 118 | WebClientError) |
1245 | 132 | 119 | ||
1247 | 133 | @inlineCallbacks | 120 | @defer.inlineCallbacks |
1248 | 134 | def test_unauthorized(self): | 121 | def test_unauthorized(self): |
1249 | 135 | """Detect when a request failed with UNAUTHORIZED.""" | 122 | """Detect when a request failed with UNAUTHORIZED.""" |
1250 | 136 | yield self.assertFailure(self.wc.call_api("unauthorized"), | 123 | yield self.assertFailure(self.wc.call_api("unauthorized"), |
1282 | 137 | web_client.UnauthorizedError) | 124 | UnauthorizedError) |
1283 | 138 | 125 | ||
1284 | 139 | 126 | ||
1285 | 140 | class OAuthTestCase(TestCase): | 127 | class WebClientBuildSignedIriTestCase(WebClientTestCase): |
1286 | 141 | """Test for the oauth signing code.""" | 128 | """Test for the webservice client when signing iris.""" |
1287 | 142 | 129 | ||
1288 | 143 | def test_build_oauth_headers(self): | 130 | # Instance of 'ParseResult' has no 'foo' member |
1289 | 144 | """Build the oauth headers for a sample request.""" | 131 | # pylint: disable=E1101 |
1290 | 145 | 132 | ||
1291 | 146 | sample_method = "GET" | 133 | @defer.inlineCallbacks |
1292 | 147 | sample_url = "http://one.ubuntu.com/" | 134 | def test_is_correct_domain(self): |
1293 | 148 | timestamp = 1 | 135 | """Test that we are using the right domain.""" |
1294 | 149 | headers = web_client.build_oauth_headers(sample_method, sample_url, | 136 | signed = yield self.wc.build_signed_iri(SAMPLE_TARGET) |
1295 | 150 | SAMPLE_CREDENTIALS, timestamp) | 137 | parsed_signed = urlparse(signed) |
1296 | 151 | self.assertIn("Authorization", headers) | 138 | parsed_sample = urlparse(SAMPLE_SIGNED) |
1297 | 152 | 139 | self.assertEqual(parsed_signed.netloc, parsed_sample.netloc) | |
1298 | 153 | def test_add_oauth_headers(self): | 140 | |
1299 | 154 | """Add the OAuth headers to a request.""" | 141 | @defer.inlineCallbacks |
1300 | 155 | 142 | def test_is_correct_path(self): | |
1301 | 156 | def sample_build_headers(*a): | 143 | """Test that we are using the right path in the URL.""" |
1302 | 157 | """Build some sample headers.""" | 144 | signed = yield self.wc.build_signed_iri(SAMPLE_TARGET) |
1303 | 158 | return {"header1": "h1", "header2": "h2"} | 145 | parsed_signed = urlparse(signed) |
1304 | 159 | 146 | parsed_sample = urlparse(SAMPLE_SIGNED) | |
1305 | 160 | self.patch(web_client, "build_oauth_headers", sample_build_headers) | 147 | self.assertEqual(parsed_signed.path, parsed_sample.path) |
1306 | 161 | test_request_headers = {} | 148 | |
1307 | 162 | append_method = test_request_headers.__setitem__ | 149 | @defer.inlineCallbacks |
1308 | 163 | timestamp = 12345 | 150 | def test_is_correct_scheme(self): |
1309 | 164 | web_client.add_oauth_headers(append_method, "GET", "http://this", {}, | 151 | """Test that we are using the right scheme.""" |
1310 | 165 | timestamp) | 152 | signed = yield self.wc.build_signed_iri(SAMPLE_TARGET) |
1311 | 166 | self.assertIn("header1", test_request_headers) | 153 | parsed_signed = urlparse(signed) |
1312 | 167 | self.assertIn("header2", test_request_headers) | 154 | parsed_sample = urlparse(SAMPLE_SIGNED) |
1313 | 155 | |||
1314 | 156 | self.assertEqual(parsed_signed.scheme, parsed_sample.scheme) | ||
1315 | 157 | |||
1316 | 158 | @defer.inlineCallbacks | ||
1317 | 159 | def test_correct_query(self): | ||
1318 | 160 | """Test the invariant parts of the signed URL.""" | ||
1319 | 161 | signed = yield self.wc.build_signed_iri(SAMPLE_TARGET, | ||
1320 | 162 | params={'next': u'/blah'}) | ||
1321 | 163 | parsed_signed = urlparse(signed) | ||
1322 | 164 | parsed_sample = urlparse(SAMPLE_SIGNED) | ||
1323 | 165 | signed_query = parse_qs(parsed_signed.query) | ||
1324 | 166 | sample_query = parse_qs(parsed_sample.query) | ||
1325 | 167 | |||
1326 | 168 | for key in ('next', 'oauth_consumer_key', 'oauth_version', | ||
1327 | 169 | 'oauth_signature_method', 'oauth_token'): | ||
1328 | 170 | self.assertEqual(map(unicode, signed_query[key]), | ||
1329 | 171 | sample_query[key]) | ||
1330 | 172 | |||
1331 | 173 | @defer.inlineCallbacks | ||
1332 | 174 | def test_url_with_query(self): | ||
1333 | 175 | """Test that we are using the right scheme.""" | ||
1334 | 176 | signed = yield self.wc.build_signed_iri(SAMPLE_TARGET, | ||
1335 | 177 | params={'next': u'/blah?foo=bar'}) | ||
1336 | 178 | parsed_signed = urlparse(signed) | ||
1337 | 179 | signed_query = parse_qs(parsed_signed.query) | ||
1338 | 180 | |||
1339 | 181 | self.assertEqual(signed_query['next'], [u'/blah?foo=bar']) | ||
1340 | 182 | |||
1341 | 183 | @defer.inlineCallbacks | ||
1342 | 184 | def test_uses_timestamper(self): | ||
1343 | 185 | """Test that the signed url is using the serverrelative timestamp.""" | ||
1344 | 186 | signed = yield self.wc.build_signed_iri(u'/blah?foo=bar') | ||
1345 | 187 | parsed_signed = urlparse(signed) | ||
1346 | 188 | signed_query = parse_qs(parsed_signed.query) | ||
1347 | 189 | |||
1348 | 190 | self.assertTrue(signed_query['oauth_timestamp'] is not None) | ||
1349 | 168 | 191 | ||
1350 | === removed directory 'ubuntuone/controlpanel/web_client' | |||
1351 | === renamed file 'ubuntuone/controlpanel/web_client/__init__.py' => 'ubuntuone/controlpanel/web_client.py' | |||
1352 | --- ubuntuone/controlpanel/web_client/__init__.py 2011-10-07 14:36:14 +0000 | |||
1353 | +++ ubuntuone/controlpanel/web_client.py 2012-02-07 14:03:19 +0000 | |||
1354 | @@ -1,9 +1,6 @@ | |||
1355 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
1356 | 2 | |||
1357 | 3 | # Authors: Natalia B Bidart <natalia.bidart@canonical.com> | ||
1358 | 4 | # Alejandro J. Cura <alecu@canonical.com> | ||
1359 | 5 | # | 2 | # |
1361 | 6 | # Copyright 2011 Canonical Ltd. | 3 | # Copyright 2011-2012 Canonical Ltd. |
1362 | 7 | # | 4 | # |
1363 | 8 | # This program is free software: you can redistribute it and/or modify it | 5 | # This program is free software: you can redistribute it and/or modify it |
1364 | 9 | # under the terms of the GNU General Public License version 3, as published | 6 | # under the terms of the GNU General Public License version 3, as published |
1365 | @@ -19,60 +16,55 @@ | |||
1366 | 19 | 16 | ||
1367 | 20 | """The web client.""" | 17 | """The web client.""" |
1368 | 21 | 18 | ||
1426 | 22 | from oauth import oauth | 19 | import simplejson |
1427 | 23 | 20 | ||
1428 | 24 | 21 | from twisted.internet import defer | |
1429 | 25 | # pylint: disable=W0401, W0614 | 22 | # need to export the exceptions to avoid API breakage |
1430 | 26 | 23 | # pylint: disable=W0611 | |
1431 | 27 | 24 | from ubuntu_sso.utils.webclient import ( | |
1432 | 28 | class WebClientError(Exception): | 25 | UnauthorizedError, |
1433 | 29 | """An http error happened while calling the webservice.""" | 26 | WebClientError, |
1434 | 30 | 27 | webclient_factory, | |
1435 | 31 | 28 | ) | |
1436 | 32 | class UnauthorizedError(WebClientError): | 29 | # pylint: enable=W0611 |
1437 | 33 | """The request ended with bad_request, unauthorized or forbidden.""" | 30 | |
1438 | 34 | 31 | from ubuntuone.controlpanel import WEBSERVICE_BASE_URL | |
1439 | 35 | 32 | from ubuntuone.controlpanel.logger import setup_logging | |
1440 | 36 | def build_oauth_headers(method, url, credentials, timestamp): | 33 | |
1441 | 37 | """Build an oauth request given some credentials.""" | 34 | |
1442 | 38 | consumer = oauth.OAuthConsumer(credentials["consumer_key"], | 35 | logger = setup_logging('webclient') |
1443 | 39 | credentials["consumer_secret"]) | 36 | |
1444 | 40 | token = oauth.OAuthToken(credentials["token"], | 37 | |
1445 | 41 | credentials["token_secret"]) | 38 | class WebClient(object): |
1446 | 42 | parameters = {} | 39 | """A client for the u1 webservice.""" |
1447 | 43 | if timestamp: | 40 | |
1448 | 44 | parameters["oauth_timestamp"] = timestamp | 41 | def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL): |
1449 | 45 | request = oauth.OAuthRequest.from_consumer_and_token( | 42 | """Initialize the webclient.""" |
1450 | 46 | http_url=url, | 43 | self.base_url = base_url |
1451 | 47 | http_method=method, | 44 | self.get_credentials = get_credentials |
1452 | 48 | parameters=parameters, | 45 | self.wc = webclient_factory() |
1453 | 49 | oauth_consumer=consumer, | 46 | logger.debug("WebClient created: base_url is %r, inner client is %r.", |
1454 | 50 | token=token) | 47 | self.base_url, self.wc) |
1455 | 51 | sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1() | 48 | |
1456 | 52 | request.sign_request(sig_method, consumer, token) | 49 | @defer.inlineCallbacks |
1457 | 53 | return request.to_header() | 50 | def call_api(self, api_name, extra_headers=None): |
1458 | 54 | 51 | """Call the webservice.""" | |
1459 | 55 | 52 | # this may log device ID's, but only for removals, which is OK | |
1460 | 56 | def add_oauth_headers(append_method, method, url, credentials, timestamp=None): | 53 | logger.debug("calling api: %r", api_name) |
1461 | 57 | """Sign a libsoup message with oauth headers.""" | 54 | iri = self.base_url + api_name |
1462 | 58 | headers = build_oauth_headers(method, url, credentials, timestamp) | 55 | credentials = yield self.get_credentials() |
1463 | 59 | for key, value in headers.items(): | 56 | response = yield self.wc.request(iri, extra_headers=extra_headers, |
1464 | 60 | append_method(key, value) | 57 | oauth_credentials=credentials) |
1465 | 61 | 58 | result = simplejson.loads(response.content) | |
1466 | 62 | 59 | defer.returnValue(result) | |
1467 | 63 | def web_client_module(): | 60 | |
1468 | 64 | """Choose the module of the web client.""" | 61 | @defer.inlineCallbacks |
1469 | 65 | # the reactor can only be imported after Qt is initialized | 62 | def build_signed_iri(self, iri, params=None): |
1470 | 66 | # pylint: disable=W0404 | 63 | """Build an OAuth iri.""" |
1471 | 67 | from twisted.internet import reactor | 64 | credentials = yield self.get_credentials() |
1472 | 68 | if getattr(reactor, "qApp", None): | 65 | result = yield self.wc.build_signed_iri(iri, credentials, params) |
1473 | 69 | from ubuntuone.controlpanel.web_client import txwebclient as web_module | 66 | defer.returnValue(result) |
1474 | 70 | else: | 67 | |
1475 | 71 | from ubuntuone.controlpanel.web_client import libsoup as web_module | 68 | def shutdown(self): |
1476 | 72 | return web_module | 69 | """Shutdown and cleanup.""" |
1477 | 73 | 70 | return self.wc.shutdown() | |
1421 | 74 | |||
1422 | 75 | def web_client_factory(*args, **kwargs): | ||
1423 | 76 | """Choose the type of the web client dynamically.""" | ||
1424 | 77 | web_module = web_client_module() | ||
1425 | 78 | return web_module.WebClient(*args, **kwargs) | ||
1478 | 79 | 71 | ||
1479 | === removed file 'ubuntuone/controlpanel/web_client/libsoup.py' | |||
1480 | --- ubuntuone/controlpanel/web_client/libsoup.py 2011-10-06 19:56:38 +0000 | |||
1481 | +++ ubuntuone/controlpanel/web_client/libsoup.py 1970-01-01 00:00:00 +0000 | |||
1482 | @@ -1,133 +0,0 @@ | |||
1483 | 1 | # -*- coding: utf-8 -*- | ||
1484 | 2 | |||
1485 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1486 | 4 | # Authors: Natalia B. Bidart <nataliabidart@canonical.com> | ||
1487 | 5 | # | ||
1488 | 6 | # Copyright 2010 Canonical Ltd. | ||
1489 | 7 | # | ||
1490 | 8 | # This program is free software: you can redistribute it and/or modify it | ||
1491 | 9 | # under the terms of the GNU General Public License version 3, as published | ||
1492 | 10 | # by the Free Software Foundation. | ||
1493 | 11 | # | ||
1494 | 12 | # This program is distributed in the hope that it will be useful, but | ||
1495 | 13 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1496 | 14 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1497 | 15 | # PURPOSE. See the GNU General Public License for more details. | ||
1498 | 16 | # | ||
1499 | 17 | # You should have received a copy of the GNU General Public License along | ||
1500 | 18 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1501 | 19 | |||
1502 | 20 | """The control panel backend webservice client.""" | ||
1503 | 21 | |||
1504 | 22 | import simplejson | ||
1505 | 23 | |||
1506 | 24 | # pylint: disable=E0611 | ||
1507 | 25 | from gi.repository import Soup, SoupGNOME | ||
1508 | 26 | from twisted.internet import defer | ||
1509 | 27 | |||
1510 | 28 | from ubuntuone.controlpanel import WEBSERVICE_BASE_URL | ||
1511 | 29 | from ubuntuone.controlpanel.web_client import (add_oauth_headers, | ||
1512 | 30 | WebClientError, | ||
1513 | 31 | UnauthorizedError) | ||
1514 | 32 | from ubuntuone.controlpanel.logger import setup_logging | ||
1515 | 33 | from ubuntuone.storageprotocol.utils import BaseTimestampChecker | ||
1516 | 34 | |||
1517 | 35 | logger = setup_logging('webclient') | ||
1518 | 36 | |||
1519 | 37 | # full list of status codes | ||
1520 | 38 | # http://library.gnome.org/devel/libsoup/stable/libsoup-2.4-soup-status.html | ||
1521 | 39 | |||
1522 | 40 | |||
1523 | 41 | class LibsoupTimestampChecker(BaseTimestampChecker): | ||
1524 | 42 | """Specialized TimestampChecker using libsoup.""" | ||
1525 | 43 | |||
1526 | 44 | def __init__(self): | ||
1527 | 45 | """Initialize this instance.""" | ||
1528 | 46 | super(LibsoupTimestampChecker, self).__init__() | ||
1529 | 47 | self.session = Soup.SessionAsync() | ||
1530 | 48 | self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME) | ||
1531 | 49 | |||
1532 | 50 | def _handler(self, session, msg, d): | ||
1533 | 51 | """Handle the result of an http message.""" | ||
1534 | 52 | logger.debug("got http response %d for uri %r", | ||
1535 | 53 | msg.status_code, msg.get_uri().to_string(False)) | ||
1536 | 54 | if msg.status_code == 200: | ||
1537 | 55 | date = msg.response_headers.get("Date") | ||
1538 | 56 | d.callback(date) | ||
1539 | 57 | else: | ||
1540 | 58 | e = WebClientError(msg.status_code, "") | ||
1541 | 59 | d.errback(e) | ||
1542 | 60 | |||
1543 | 61 | def get_server_date_header(self, server_url): | ||
1544 | 62 | """Get the server date using twisted webclient.""" | ||
1545 | 63 | method = "HEAD" | ||
1546 | 64 | msg = Soup.Message.new(method, server_url) | ||
1547 | 65 | msg.request_headers.append("Cache-Control", "no-cache") | ||
1548 | 66 | d = defer.Deferred() | ||
1549 | 67 | self.session.queue_message(msg, self._handler, d) | ||
1550 | 68 | return d | ||
1551 | 69 | |||
1552 | 70 | def shutdown(self): | ||
1553 | 71 | """End the soup session for this webclient.""" | ||
1554 | 72 | self.session.abort() | ||
1555 | 73 | |||
1556 | 74 | |||
1557 | 75 | # pylint: disable=C0103 | ||
1558 | 76 | timestamp_checker = LibsoupTimestampChecker() | ||
1559 | 77 | |||
1560 | 78 | |||
1561 | 79 | class WebClient(object): | ||
1562 | 80 | """A client for the u1 webservice.""" | ||
1563 | 81 | |||
1564 | 82 | def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL): | ||
1565 | 83 | """Initialize the webclient.""" | ||
1566 | 84 | self.base_url = base_url | ||
1567 | 85 | self.session = Soup.SessionAsync() | ||
1568 | 86 | self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME) | ||
1569 | 87 | self.get_credentials = get_credentials | ||
1570 | 88 | |||
1571 | 89 | def _handler(self, session, msg, d): | ||
1572 | 90 | """Handle the result of an http message.""" | ||
1573 | 91 | logger.debug("got http response %d for uri %r", | ||
1574 | 92 | msg.status_code, msg.get_uri().to_string(False)) | ||
1575 | 93 | data = msg.response_body.data | ||
1576 | 94 | if msg.status_code == 200: | ||
1577 | 95 | result = simplejson.loads(data) | ||
1578 | 96 | d.callback(result) | ||
1579 | 97 | else: | ||
1580 | 98 | if msg.status_code in (401,): | ||
1581 | 99 | e = UnauthorizedError(msg.status_code, data) | ||
1582 | 100 | else: | ||
1583 | 101 | e = WebClientError(msg.status_code, data) | ||
1584 | 102 | d.errback(e) | ||
1585 | 103 | |||
1586 | 104 | def _call_api_inner(self, credentials, api_name, timestamp): | ||
1587 | 105 | """Call the webservice with credentials and timestamp.""" | ||
1588 | 106 | url = (self.base_url + api_name).encode('utf-8') | ||
1589 | 107 | method = "GET" | ||
1590 | 108 | logger.debug("getting url: %s, %s", method, url) | ||
1591 | 109 | msg = Soup.Message.new(method, url) | ||
1592 | 110 | add_oauth_headers(msg.request_headers.append, method, url, | ||
1593 | 111 | credentials, timestamp) | ||
1594 | 112 | d = defer.Deferred() | ||
1595 | 113 | self.session.queue_message(msg, self._handler, d) | ||
1596 | 114 | return d | ||
1597 | 115 | |||
1598 | 116 | @defer.inlineCallbacks | ||
1599 | 117 | def _call_api_add_timestamp(self, credentials, api_name): | ||
1600 | 118 | """Add the timestamp to the api call.""" | ||
1601 | 119 | timestamp = yield timestamp_checker.get_faithful_time() | ||
1602 | 120 | result = yield self._call_api_inner(credentials, api_name, timestamp) | ||
1603 | 121 | defer.returnValue(result) | ||
1604 | 122 | |||
1605 | 123 | def call_api(self, api_name): | ||
1606 | 124 | """Call the webservice.""" | ||
1607 | 125 | # this may log device ID's, but only for removals, which is OK | ||
1608 | 126 | logger.debug("calling api: %s", api_name) | ||
1609 | 127 | d = self.get_credentials() | ||
1610 | 128 | d.addCallback(self._call_api_add_timestamp, api_name) | ||
1611 | 129 | return d | ||
1612 | 130 | |||
1613 | 131 | def shutdown(self): | ||
1614 | 132 | """End the soup session for this webclient.""" | ||
1615 | 133 | self.session.abort() | ||
1616 | 134 | 0 | ||
1617 | === removed directory 'ubuntuone/controlpanel/web_client/tests' | |||
1618 | === removed file 'ubuntuone/controlpanel/web_client/tests/__init__.py' | |||
1619 | --- ubuntuone/controlpanel/web_client/tests/__init__.py 2011-09-08 23:52:27 +0000 | |||
1620 | +++ ubuntuone/controlpanel/web_client/tests/__init__.py 1970-01-01 00:00:00 +0000 | |||
1621 | @@ -1,19 +0,0 @@ | |||
1622 | 1 | # -*- coding: utf-8 -*- | ||
1623 | 2 | |||
1624 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1625 | 4 | # | ||
1626 | 5 | # Copyright 2011 Canonical Ltd. | ||
1627 | 6 | # | ||
1628 | 7 | # This program is free software: you can redistribute it and/or modify it | ||
1629 | 8 | # under the terms of the GNU General Public License version 3, as published | ||
1630 | 9 | # by the Free Software Foundation. | ||
1631 | 10 | # | ||
1632 | 11 | # This program is distributed in the hope that it will be useful, but | ||
1633 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1634 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1635 | 14 | # PURPOSE. See the GNU General Public License for more details. | ||
1636 | 15 | # | ||
1637 | 16 | # You should have received a copy of the GNU General Public License along | ||
1638 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1639 | 18 | |||
1640 | 19 | """Unit tests for the control panel backend webservice clients.""" | ||
1641 | 20 | 0 | ||
1642 | === removed file 'ubuntuone/controlpanel/web_client/tests/test_libsoup.py' | |||
1643 | --- ubuntuone/controlpanel/web_client/tests/test_libsoup.py 2011-10-06 22:27:57 +0000 | |||
1644 | +++ ubuntuone/controlpanel/web_client/tests/test_libsoup.py 1970-01-01 00:00:00 +0000 | |||
1645 | @@ -1,106 +0,0 @@ | |||
1646 | 1 | # -*- coding: utf-8 -*- | ||
1647 | 2 | |||
1648 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1649 | 4 | # | ||
1650 | 5 | # Copyright 2011 Canonical Ltd. | ||
1651 | 6 | # | ||
1652 | 7 | # This program is free software: you can redistribute it and/or modify it | ||
1653 | 8 | # under the terms of the GNU General Public License version 3, as published | ||
1654 | 9 | # by the Free Software Foundation. | ||
1655 | 10 | # | ||
1656 | 11 | # This program is distributed in the hope that it will be useful, but | ||
1657 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1658 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1659 | 14 | # PURPOSE. See the GNU General Public License for more details. | ||
1660 | 15 | # | ||
1661 | 16 | # You should have received a copy of the GNU General Public License along | ||
1662 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1663 | 18 | |||
1664 | 19 | """Integration tests for the libsoup based webservice client.""" | ||
1665 | 20 | |||
1666 | 21 | import time | ||
1667 | 22 | |||
1668 | 23 | from twisted.application import internet, service | ||
1669 | 24 | from twisted.internet import defer | ||
1670 | 25 | from twisted.web import http, resource, server | ||
1671 | 26 | |||
1672 | 27 | from ubuntuone.controlpanel.tests import TestCase | ||
1673 | 28 | from ubuntuone.controlpanel.web_client.libsoup import LibsoupTimestampChecker | ||
1674 | 29 | |||
1675 | 30 | |||
1676 | 31 | class MockResource(resource.Resource): | ||
1677 | 32 | """A mock resource.""" | ||
1678 | 33 | |||
1679 | 34 | isLeaf = True | ||
1680 | 35 | |||
1681 | 36 | def __init__(self): | ||
1682 | 37 | """Initialize this mock instance.""" | ||
1683 | 38 | resource.Resource.__init__(self) | ||
1684 | 39 | self.request_headers = [] | ||
1685 | 40 | |||
1686 | 41 | # pylint: disable=C0103 | ||
1687 | 42 | def render_GET(self, request): | ||
1688 | 43 | """Render some content.""" | ||
1689 | 44 | self.request_headers.append(request.requestHeaders) | ||
1690 | 45 | return "hello!" | ||
1691 | 46 | |||
1692 | 47 | |||
1693 | 48 | class MockWebService(object): | ||
1694 | 49 | """A mock webservice for testing""" | ||
1695 | 50 | |||
1696 | 51 | def __init__(self): | ||
1697 | 52 | """Start up this instance.""" | ||
1698 | 53 | self.root = MockResource() | ||
1699 | 54 | site = server.Site(self.root) | ||
1700 | 55 | application = service.Application('web') | ||
1701 | 56 | self.service_collection = service.IServiceCollection(application) | ||
1702 | 57 | #pylint: disable=E1101 | ||
1703 | 58 | self.tcpserver = internet.TCPServer(0, site) | ||
1704 | 59 | self.tcpserver.setServiceParent(self.service_collection) | ||
1705 | 60 | self.service_collection.startService() | ||
1706 | 61 | |||
1707 | 62 | def get_url(self): | ||
1708 | 63 | """Build the url for this mock server.""" | ||
1709 | 64 | #pylint: disable=W0212 | ||
1710 | 65 | port_num = self.tcpserver._port.getHost().port | ||
1711 | 66 | return "http://localhost:%d/" % port_num | ||
1712 | 67 | |||
1713 | 68 | def stop(self): | ||
1714 | 69 | """Shut down the service.""" | ||
1715 | 70 | #pylint: disable=E1101 | ||
1716 | 71 | self.service_collection.stopService() | ||
1717 | 72 | |||
1718 | 73 | |||
1719 | 74 | class LibsoupTimestampCheckerTestCase(TestCase): | ||
1720 | 75 | """Tests for LibsoupTimestampChecker.""" | ||
1721 | 76 | |||
1722 | 77 | timeout = 3 | ||
1723 | 78 | |||
1724 | 79 | @defer.inlineCallbacks | ||
1725 | 80 | def setUp(self): | ||
1726 | 81 | yield super(LibsoupTimestampCheckerTestCase, self).setUp() | ||
1727 | 82 | self.ws = MockWebService() | ||
1728 | 83 | self.addCleanup(self.ws.stop) | ||
1729 | 84 | |||
1730 | 85 | @defer.inlineCallbacks | ||
1731 | 86 | def test_gets_server_date(self): | ||
1732 | 87 | """The server date is gotten right.""" | ||
1733 | 88 | fake_time = 1 | ||
1734 | 89 | self.patch(time, "time", lambda: fake_time) | ||
1735 | 90 | checker = LibsoupTimestampChecker() | ||
1736 | 91 | self.addCleanup(checker.shutdown) | ||
1737 | 92 | d = checker.get_server_date_header(self.ws.get_url()) | ||
1738 | 93 | result = yield d | ||
1739 | 94 | result_time = http.stringToDatetime(result) | ||
1740 | 95 | self.assertEqual(result_time, fake_time) | ||
1741 | 96 | |||
1742 | 97 | @defer.inlineCallbacks | ||
1743 | 98 | def test_server_date_sends_nocache_headers(self): | ||
1744 | 99 | """Getting the server date sends the no-cache headers.""" | ||
1745 | 100 | checker = LibsoupTimestampChecker() | ||
1746 | 101 | self.addCleanup(checker.shutdown) | ||
1747 | 102 | yield checker.get_server_date_header(self.ws.get_url()) | ||
1748 | 103 | self.assertEqual(len(self.ws.root.request_headers), 1) | ||
1749 | 104 | headers = self.ws.root.request_headers[0] | ||
1750 | 105 | result = headers.getRawHeaders("Cache-Control") | ||
1751 | 106 | self.assertEqual(result, ["no-cache"]) | ||
1752 | 107 | 0 | ||
1753 | === removed file 'ubuntuone/controlpanel/web_client/tests/test_txwebclient.py' | |||
1754 | --- ubuntuone/controlpanel/web_client/tests/test_txwebclient.py 2011-11-21 13:32:44 +0000 | |||
1755 | +++ ubuntuone/controlpanel/web_client/tests/test_txwebclient.py 1970-01-01 00:00:00 +0000 | |||
1756 | @@ -1,177 +0,0 @@ | |||
1757 | 1 | # -*- coding: utf-8 -*- | ||
1758 | 2 | |||
1759 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1760 | 4 | # | ||
1761 | 5 | # Copyright 2011 Canonical Ltd. | ||
1762 | 6 | # | ||
1763 | 7 | # This program is free software: you can redistribute it and/or modify it | ||
1764 | 8 | # under the terms of the GNU General Public License version 3, as published | ||
1765 | 9 | # by the Free Software Foundation. | ||
1766 | 10 | # | ||
1767 | 11 | # This program is distributed in the hope that it will be useful, but | ||
1768 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1769 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1770 | 14 | # PURPOSE. See the GNU General Public License for more details. | ||
1771 | 15 | # | ||
1772 | 16 | # You should have received a copy of the GNU General Public License along | ||
1773 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1774 | 18 | |||
1775 | 19 | """Unit tests for the control panel backend twisted webservice client.""" | ||
1776 | 20 | |||
1777 | 21 | import time | ||
1778 | 22 | |||
1779 | 23 | from twisted.application import internet, service | ||
1780 | 24 | from twisted.internet import defer, reactor | ||
1781 | 25 | from twisted.internet.defer import inlineCallbacks | ||
1782 | 26 | from twisted.web import server, resource | ||
1783 | 27 | from ubuntuone.devtools.testcases import skipIfOS | ||
1784 | 28 | |||
1785 | 29 | from ubuntuone.controlpanel.web_client import txwebclient | ||
1786 | 30 | from ubuntuone.controlpanel.tests import TestCase | ||
1787 | 31 | |||
1788 | 32 | |||
1789 | 33 | SAMPLE_KEY = "result" | ||
1790 | 34 | SAMPLE_VALUE = "sample result" | ||
1791 | 35 | SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE) | ||
1792 | 36 | SAMPLE_CREDENTIALS = dict( | ||
1793 | 37 | consumer_key="consumer key", | ||
1794 | 38 | consumer_secret="consumer secret", | ||
1795 | 39 | token="the real token", | ||
1796 | 40 | token_secret="the token secret", | ||
1797 | 41 | ) | ||
1798 | 42 | |||
1799 | 43 | |||
1800 | 44 | def sample_get_credentials(): | ||
1801 | 45 | """Will return the sample credentials right now.""" | ||
1802 | 46 | return defer.succeed(SAMPLE_CREDENTIALS) | ||
1803 | 47 | |||
1804 | 48 | |||
1805 | 49 | class MockResource(resource.Resource): | ||
1806 | 50 | """A simple web resource.""" | ||
1807 | 51 | isLeaf = True | ||
1808 | 52 | contents = "" | ||
1809 | 53 | |||
1810 | 54 | # pylint: disable=C0103 | ||
1811 | 55 | # t.w.resource methods have freeform cased names | ||
1812 | 56 | |||
1813 | 57 | def getChild(self, name, request): | ||
1814 | 58 | """Get a given child resource.""" | ||
1815 | 59 | if name == '': | ||
1816 | 60 | return self | ||
1817 | 61 | return resource.Resource.getChild(self, name, request) | ||
1818 | 62 | |||
1819 | 63 | def render_GET(self, request): | ||
1820 | 64 | """Make a bit of html out of these resource's content.""" | ||
1821 | 65 | return self.contents | ||
1822 | 66 | |||
1823 | 67 | |||
1824 | 68 | class MockWebService(object): | ||
1825 | 69 | """A mock webservice for testing""" | ||
1826 | 70 | |||
1827 | 71 | def __init__(self): | ||
1828 | 72 | """Start up this instance.""" | ||
1829 | 73 | root = resource.Resource() | ||
1830 | 74 | devices_resource = MockResource() | ||
1831 | 75 | devices_resource.contents = SAMPLE_RESOURCE | ||
1832 | 76 | root.putChild("devices", devices_resource) | ||
1833 | 77 | root.putChild("throwerror", resource.NoResource()) | ||
1834 | 78 | unauthorized = resource.ErrorPage(resource.http.UNAUTHORIZED, | ||
1835 | 79 | "Unauthrorized", "Unauthrorized") | ||
1836 | 80 | root.putChild("unauthorized", unauthorized) | ||
1837 | 81 | |||
1838 | 82 | site = server.Site(root) | ||
1839 | 83 | application = service.Application('web') | ||
1840 | 84 | self.service_collection = service.IServiceCollection(application) | ||
1841 | 85 | #pylint: disable=E1101 | ||
1842 | 86 | self.tcpserver = internet.TCPServer(0, site) | ||
1843 | 87 | self.tcpserver.setServiceParent(self.service_collection) | ||
1844 | 88 | self.service_collection.startService() | ||
1845 | 89 | |||
1846 | 90 | def get_url(self): | ||
1847 | 91 | """Build the url for this mock server.""" | ||
1848 | 92 | #pylint: disable=W0212 | ||
1849 | 93 | port_num = self.tcpserver._port.getHost().port | ||
1850 | 94 | return "http://localhost:%d/" % port_num | ||
1851 | 95 | |||
1852 | 96 | def stop(self): | ||
1853 | 97 | """Shut it down.""" | ||
1854 | 98 | #pylint: disable=E1101 | ||
1855 | 99 | return self.service_collection.stopService() | ||
1856 | 100 | |||
1857 | 101 | |||
1858 | 102 | class FakeAsyncTimestamper(object): | ||
1859 | 103 | """A fake timestamp.""" | ||
1860 | 104 | |||
1861 | 105 | def __init__(self): | ||
1862 | 106 | """Initialize this instance.""" | ||
1863 | 107 | self.called = False | ||
1864 | 108 | |||
1865 | 109 | def get_faithful_time(self): | ||
1866 | 110 | """Return the server checked timestamp.""" | ||
1867 | 111 | self.called = True | ||
1868 | 112 | return defer.succeed(time.time()) | ||
1869 | 113 | |||
1870 | 114 | |||
1871 | 115 | class WebClientTestCase(TestCase): | ||
1872 | 116 | """Test for the webservice client.""" | ||
1873 | 117 | |||
1874 | 118 | timeout = 8 | ||
1875 | 119 | |||
1876 | 120 | @defer.inlineCallbacks | ||
1877 | 121 | def setUp(self): | ||
1878 | 122 | yield super(WebClientTestCase, self).setUp() | ||
1879 | 123 | self.ws = MockWebService() | ||
1880 | 124 | test_base_url = self.ws.get_url() | ||
1881 | 125 | self.wc = txwebclient.WebClient(sample_get_credentials, test_base_url) | ||
1882 | 126 | self.timestamper = FakeAsyncTimestamper() | ||
1883 | 127 | self.patch(txwebclient, "timestamp_checker", self.timestamper) | ||
1884 | 128 | self.addCleanup(self.wc.shutdown) | ||
1885 | 129 | self.addCleanup(self.ws.stop) | ||
1886 | 130 | |||
1887 | 131 | @inlineCallbacks | ||
1888 | 132 | def test_get_url(self): | ||
1889 | 133 | """A method is successfully called in the mock webservice.""" | ||
1890 | 134 | result = yield self.wc.call_api("devices") | ||
1891 | 135 | self.assertIn(SAMPLE_KEY, result) | ||
1892 | 136 | self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY]) | ||
1893 | 137 | |||
1894 | 138 | @inlineCallbacks | ||
1895 | 139 | def test_call_api_uses_timestamp(self): | ||
1896 | 140 | """Check that call_api uses the timestamp.""" | ||
1897 | 141 | yield self.wc.call_api("devices") | ||
1898 | 142 | self.assertTrue(self.timestamper.called, | ||
1899 | 143 | "The timestamper must be used.") | ||
1900 | 144 | |||
1901 | 145 | @inlineCallbacks | ||
1902 | 146 | def test_get_url_error(self): | ||
1903 | 147 | """The errback is called when there's some error.""" | ||
1904 | 148 | yield self.assertFailure(self.wc.call_api("throwerror"), | ||
1905 | 149 | txwebclient.WebClientError) | ||
1906 | 150 | |||
1907 | 151 | @inlineCallbacks | ||
1908 | 152 | def test_unauthorized(self): | ||
1909 | 153 | """Detect when a request failed with UNAUTHORIZED.""" | ||
1910 | 154 | yield self.assertFailure(self.wc.call_api("unauthorized"), | ||
1911 | 155 | txwebclient.UnauthorizedError) | ||
1912 | 156 | |||
1913 | 157 | |||
1914 | 158 | class WebClientShutdownTestCase(TestCase): | ||
1915 | 159 | """The webclient behaviour during shutdown.""" | ||
1916 | 160 | |||
1917 | 161 | @skipIfOS('win32', 'Failing on windows, see LP: #851158.') | ||
1918 | 162 | @inlineCallbacks | ||
1919 | 163 | def test_shutdown(self): | ||
1920 | 164 | """The webclient behaves well during shutdown.""" | ||
1921 | 165 | self.patch(txwebclient, "timestamp_checker", FakeAsyncTimestamper()) | ||
1922 | 166 | d3 = defer.Deferred() | ||
1923 | 167 | # pylint: disable=E1101 | ||
1924 | 168 | reactor.callLater(1, d3.callback, None) | ||
1925 | 169 | ws = MockWebService() | ||
1926 | 170 | test_base_url = ws.get_url() | ||
1927 | 171 | wc = txwebclient.WebClient(sample_get_credentials, test_base_url) | ||
1928 | 172 | d1 = wc.call_api("throwerror") | ||
1929 | 173 | d2 = ws.stop() | ||
1930 | 174 | wc.shutdown() | ||
1931 | 175 | yield d2 | ||
1932 | 176 | yield defer.DeferredList([d1, d3], fireOnOneCallback=True, | ||
1933 | 177 | fireOnOneErrback=True) | ||
1934 | 178 | 0 | ||
1935 | === removed file 'ubuntuone/controlpanel/web_client/txwebclient.py' | |||
1936 | --- ubuntuone/controlpanel/web_client/txwebclient.py 2011-10-05 23:12:23 +0000 | |||
1937 | +++ ubuntuone/controlpanel/web_client/txwebclient.py 1970-01-01 00:00:00 +0000 | |||
1938 | @@ -1,108 +0,0 @@ | |||
1939 | 1 | # -*- coding: utf-8 -*- | ||
1940 | 2 | |||
1941 | 3 | # Authors: Alejandro J. Cura <alecu@canonical.com> | ||
1942 | 4 | # | ||
1943 | 5 | # Copyright 2011 Canonical Ltd. | ||
1944 | 6 | # | ||
1945 | 7 | # This program is free software: you can redistribute it and/or modify it | ||
1946 | 8 | # under the terms of the GNU General Public License version 3, as published | ||
1947 | 9 | # by the Free Software Foundation. | ||
1948 | 10 | # | ||
1949 | 11 | # This program is distributed in the hope that it will be useful, but | ||
1950 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1951 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1952 | 14 | # PURPOSE. See the GNU General Public License for more details. | ||
1953 | 15 | # | ||
1954 | 16 | # You should have received a copy of the GNU General Public License along | ||
1955 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1956 | 18 | |||
1957 | 19 | """The control panel backend webservice client using twisted.web.""" | ||
1958 | 20 | |||
1959 | 21 | import simplejson | ||
1960 | 22 | |||
1961 | 23 | from twisted.internet import defer, reactor | ||
1962 | 24 | from twisted.web import client, error, http | ||
1963 | 25 | |||
1964 | 26 | from ubuntuone.controlpanel import WEBSERVICE_BASE_URL | ||
1965 | 27 | from ubuntuone.controlpanel.web_client import (add_oauth_headers, | ||
1966 | 28 | WebClientError, | ||
1967 | 29 | UnauthorizedError) | ||
1968 | 30 | |||
1969 | 31 | from ubuntuone.controlpanel.logger import setup_logging | ||
1970 | 32 | from ubuntuone.storageprotocol.client import TwistedTimestampChecker | ||
1971 | 33 | |||
1972 | 34 | logger = setup_logging('webclient') | ||
1973 | 35 | # pylint: disable=C0103 | ||
1974 | 36 | timestamp_checker = TwistedTimestampChecker() | ||
1975 | 37 | |||
1976 | 38 | |||
1977 | 39 | class WebClient(object): | ||
1978 | 40 | """A client for the u1 webservice.""" | ||
1979 | 41 | |||
1980 | 42 | def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL): | ||
1981 | 43 | """Initialize the webclient.""" | ||
1982 | 44 | self.base_url = base_url | ||
1983 | 45 | self.get_credentials = get_credentials | ||
1984 | 46 | self.running = True | ||
1985 | 47 | # pylint: disable=E1101 | ||
1986 | 48 | self.trigger_id = reactor.addSystemEventTrigger("before", "shutdown", | ||
1987 | 49 | self.shutdown) | ||
1988 | 50 | |||
1989 | 51 | def _handle_response(self, result): | ||
1990 | 52 | """Handle the response of the webservice call.""" | ||
1991 | 53 | return simplejson.loads(result) | ||
1992 | 54 | |||
1993 | 55 | def _handle_error(self, failure): | ||
1994 | 56 | """Handle an error while calling the webservice.""" | ||
1995 | 57 | if failure.type == error.Error: | ||
1996 | 58 | exception = failure.value | ||
1997 | 59 | if exception.status == str(http.UNAUTHORIZED): | ||
1998 | 60 | raise UnauthorizedError(exception.status, exception.response) | ||
1999 | 61 | else: | ||
2000 | 62 | raise WebClientError(exception.status, exception.response) | ||
2001 | 63 | else: | ||
2002 | 64 | raise WebClientError(-1, failure) | ||
2003 | 65 | |||
2004 | 66 | def _call_api_inner(self, credentials, api_name, timestamp): | ||
2005 | 67 | """Call the webservice with credentials and timestamp.""" | ||
2006 | 68 | url = (self.base_url + api_name).encode('utf-8') | ||
2007 | 69 | method = "GET" | ||
2008 | 70 | logger.debug("getting url: %s, %s", method, url) | ||
2009 | 71 | headers = {} | ||
2010 | 72 | add_oauth_headers(headers.__setitem__, method, url, credentials, | ||
2011 | 73 | timestamp) | ||
2012 | 74 | d = client.getPage(url, headers=headers) | ||
2013 | 75 | d.addCallback(self._handle_response) | ||
2014 | 76 | d.addErrback(self._handle_error) | ||
2015 | 77 | return d | ||
2016 | 78 | |||
2017 | 79 | @defer.inlineCallbacks | ||
2018 | 80 | def _call_api_add_timestamp(self, credentials, api_name): | ||
2019 | 81 | """Add the timestamp to the api call.""" | ||
2020 | 82 | timestamp = yield timestamp_checker.get_faithful_time() | ||
2021 | 83 | result = yield self._call_api_inner(credentials, api_name, timestamp) | ||
2022 | 84 | defer.returnValue(result) | ||
2023 | 85 | |||
2024 | 86 | def call_api(self, api_name): | ||
2025 | 87 | """Call the webservice.""" | ||
2026 | 88 | # this may log device ID's, but only for removals, which is OK | ||
2027 | 89 | logger.debug("calling api: %s", api_name) | ||
2028 | 90 | d = self.get_credentials() | ||
2029 | 91 | d.addErrback(self._handle_error) | ||
2030 | 92 | d.addCallback(self._call_api_add_timestamp, api_name) | ||
2031 | 93 | d2 = defer.Deferred() | ||
2032 | 94 | d.addCallback(d2.callback) | ||
2033 | 95 | |||
2034 | 96 | def mask_errors_on_shutdown(failure): | ||
2035 | 97 | """Do not fire the errbacks if we are shutting down.""" | ||
2036 | 98 | if self.running: | ||
2037 | 99 | d2.errback(failure) | ||
2038 | 100 | |||
2039 | 101 | d.addErrback(mask_errors_on_shutdown) | ||
2040 | 102 | return d2 | ||
2041 | 103 | |||
2042 | 104 | def shutdown(self): | ||
2043 | 105 | """End the pending webclient calls.""" | ||
2044 | 106 | self.running = False | ||
2045 | 107 | # pylint: disable=E1101 | ||
2046 | 108 | reactor.removeSystemEventTrigger(self.trigger_id) |
Looks good to me....