Merge lp:~nataliabidart/ubuntuone-control-panel/use-webclient into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
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
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.

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) wrote :

Looks good to me....

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Code looks nice, all tests pass, tested IRL on linux.

review: Approve
Revision history for this message
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/controlpanel ***

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.get_suite(config)
  File "/usr/bin/u1trial", line 184, in get_suite
    config['ignore-paths']))
  File "/usr/bin/u1trial", line 168, in _collect_tests
    module_suite = self._load_unittest(filepath)
  File "/usr/bin/u1trial", line 108, in _load_unittest
    module = __import__(modpath, None, None, [""])
  File "/home/tarmac/cache/ubuntuone-control-panel/trunk/ubuntuone/controlpanel/tests/test_web_client.py", line 24, in <module>
    from ubuntu_sso.utils.webclient.tests import BaseMockWebServer
ImportError: No module named tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'setup.py'
--- setup.py 2012-01-26 13:31:22 +0000
+++ setup.py 2012-02-07 14:03:19 +0000
@@ -247,7 +247,6 @@
247 'ubuntuone.controlpanel.gui.qt.ui',247 'ubuntuone.controlpanel.gui.qt.ui',
248 'ubuntuone.controlpanel.sd_client',248 'ubuntuone.controlpanel.sd_client',
249 'ubuntuone.controlpanel.utils',249 'ubuntuone.controlpanel.utils',
250 'ubuntuone.controlpanel.web_client',
251 ],250 ],
252 extra_path='ubuntuone-control-panel',251 extra_path='ubuntuone-control-panel',
253 data_files=[252 data_files=[
254253
=== modified file 'ubuntuone/controlpanel/backend.py'
--- ubuntuone/controlpanel/backend.py 2012-01-25 14:49:59 +0000
+++ ubuntuone/controlpanel/backend.py 2012-02-07 14:03:19 +0000
@@ -34,41 +34,47 @@
3434
35from ubuntuone.controlpanel import sd_client, replication_client35from ubuntuone.controlpanel import sd_client, replication_client
36from ubuntuone.controlpanel.logger import setup_logging, log_call36from ubuntuone.controlpanel.logger import setup_logging, log_call
37# pylint: disable=W061137from ubuntuone.controlpanel.web_client import (
38from ubuntuone.controlpanel.web_client import (UnauthorizedError,38 UnauthorizedError,
39 web_client_factory, WebClientError)39 WebClient,
40# pylint: enable=W061140 WebClientError,
41)
4142
42logger = setup_logging('backend')43logger = setup_logging('backend')
4344
44ACCOUNT_API = "account/"45ACCOUNT_API = u"account/"
45QUOTA_API = "quota/"46DEVICES_API = u"1.0/devices/"
46DEVICES_API = "1.0/devices/"47DEVICE_REMOVE_API = u"1.0/devices/remove/%s/%s"
47DEVICE_REMOVE_API = "1.0/devices/remove/%s/%s"48QUOTA_API = u"quota/"
48DEVICE_TYPE_PHONE = "Phone"49
49DEVICE_TYPE_COMPUTER = "Computer"50DEVICE_TYPE_PHONE = u"Phone"
50AUTOCONNECT_KEY = 'autoconnect'51DEVICE_TYPE_COMPUTER = u"Computer"
51SHOW_ALL_NOTIFICATIONS_KEY = 'show_all_notifications'52
52SHARE_AUTOSUBSCRIBE_KEY = 'share_autosubscribe'53AUTOCONNECT_KEY = u'autoconnect'
53UDF_AUTOSUBSCRIBE_KEY = 'udf_autosubscribe'54SHOW_ALL_NOTIFICATIONS_KEY = u'show_all_notifications'
54LIMIT_BW_KEY = 'limit_bandwidth'55SHARE_AUTOSUBSCRIBE_KEY = u'share_autosubscribe'
55UPLOAD_KEY = "max_upload_speed"56UDF_AUTOSUBSCRIBE_KEY = u'udf_autosubscribe'
56DOWNLOAD_KEY = "max_download_speed"57LIMIT_BW_KEY = u'limit_bandwidth'
5758UPLOAD_KEY = u'max_upload_speed'
58FILE_SYNC_DISABLED = 'file-sync-disabled'59DOWNLOAD_KEY = u'max_download_speed'
59FILE_SYNC_DISCONNECTED = 'file-sync-disconnected'60
60FILE_SYNC_ERROR = 'file-sync-error'61FILE_SYNC_DISABLED = u'file-sync-disabled'
61FILE_SYNC_IDLE = 'file-sync-idle'62FILE_SYNC_DISCONNECTED = u'file-sync-disconnected'
62FILE_SYNC_STARTING = 'file-sync-starting'63FILE_SYNC_ERROR = u'file-sync-error'
63FILE_SYNC_STOPPED = 'file-sync-stopped'64FILE_SYNC_IDLE = u'file-sync-idle'
64FILE_SYNC_SYNCING = 'file-sync-syncing'65FILE_SYNC_STARTING = u'file-sync-starting'
65FILE_SYNC_UNKNOWN = 'file-sync-unknown'66FILE_SYNC_STOPPED = u'file-sync-stopped'
67FILE_SYNC_SYNCING = u'file-sync-syncing'
68FILE_SYNC_UNKNOWN = u'file-sync-unknown'
6669
67MSG_KEY = 'message'70MSG_KEY = 'message'
68STATUS_KEY = 'status'71STATUS_KEY = 'status'
6972
70CONTACTS_PKG = 'thunderbird-couchdb'73CONTACTS_PKG = 'thunderbird-couchdb'
7174
75UBUNTUONE_FROM_OAUTH = u'https://one.ubuntu.com/api/1.0/from_oauth/'
76UBUNTUONE_LINK = u'https://one.ubuntu.com/'
77
7278
73def append_path_sep(path):79def append_path_sep(path):
74 """If 'path' does not end with the path separator, append it."""80 """If 'path' does not end with the path separator, append it."""
@@ -135,7 +141,7 @@
135141
136 self.login_client = CredentialsManagementTool()142 self.login_client = CredentialsManagementTool()
137 self.sd_client = sd_client.SyncDaemonClient()143 self.sd_client = sd_client.SyncDaemonClient()
138 self.wc = web_client_factory(self.get_credentials)144 self.wc = WebClient(self.get_credentials)
139145
140 logger.info('ControlBackend: instance started.')146 logger.info('ControlBackend: instance started.')
141147
@@ -304,6 +310,19 @@
304 returnValue(path)310 returnValue(path)
305311
306 @inlineCallbacks312 @inlineCallbacks
313 def build_signed_iri(self, iri):
314 """Returned an OAuth signed iri."""
315 credentials = None
316 if iri.startswith(UBUNTUONE_LINK):
317 credentials = yield self.get_credentials()
318
319 if credentials:
320 parameters = {'next': iri}
321 iri = yield self.wc.build_signed_iri(UBUNTUONE_FROM_OAUTH,
322 parameters)
323 returnValue(iri)
324
325 @inlineCallbacks
307 def get_credentials(self):326 def get_credentials(self):
308 """Find credentials."""327 """Find credentials."""
309 if not self._credentials:328 if not self._credentials:
310329
=== modified file 'ubuntuone/controlpanel/gui/__init__.py'
--- ubuntuone/controlpanel/gui/__init__.py 2011-10-06 20:38:39 +0000
+++ ubuntuone/controlpanel/gui/__init__.py 2012-02-07 14:03:19 +0000
@@ -21,56 +21,55 @@
2121
22import gettext22import gettext
2323
24from oauth import oauth24from ubuntuone.controlpanel.backend import UBUNTUONE_LINK
25
2526
26_ = gettext.gettext27_ = gettext.gettext
2728
2829
29ERROR_COLOR = 'red'30ERROR_COLOR = u'red'
30KILOBYTES = 102431KILOBYTES = 1024
31NO_OP = lambda *a, **kw: None32NO_OP = lambda *a, **kw: None
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/
33ORANGE = '#DD4814'34ORANGE = u'#DD4814'
34QUOTA_THRESHOLD = 0.9535QUOTA_THRESHOLD = 0.95
35SHARES_MIN_SIZE_FULL = 104857636SHARES_MIN_SIZE_FULL = 1048576
36SUCCESS_COLOR = 'green'37SUCCESS_COLOR = u'green'
3738
38ERROR_ICON = u'✘'39ERROR_ICON = u'✘'
39SYNCING_ICON = u'⇅'40SYNCING_ICON = u'⇅'
40IDLE_ICON = u'✔'41IDLE_ICON = u'✔'
4142
42CONTACT_ICON_NAME = 'avatar-default'43CONTACT_ICON_NAME = u'avatar-default'
43FOLDER_ICON_NAME = 'folder'44FOLDER_ICON_NAME = u'folder'
44SHARE_ICON_NAME = 'folder-remote'45SHARE_ICON_NAME = u'folder-remote'
45MUSIC_ICON_NAME = 'audio-x-generic'46MUSIC_ICON_NAME = u'audio-x-generic'
4647
47CONTACTS_ICON = 'contacts.png'48CONTACTS_ICON = u'contacts.png'
48FACEBOOK_LOGO = 'facebook.png'49FACEBOOK_LOGO = u'facebook.png'
49FILES_ICON = 'files.png'50FILES_ICON = u'files.png'
50OVERVIEW_BANNER = 'overview.png'51OVERVIEW_BANNER = u'overview.png'
51TWITTER_LOGO = 'twitter.png'52TWITTER_LOGO = u'twitter.png'
52MUSIC_STORE_ICON = 'music-store.png'53MUSIC_STORE_ICON = u'music-store.png'
53MUSIC_STREAM_ICON = 'music-stream.png'54MUSIC_STREAM_ICON = u'music-stream.png'
54NOTES_ICON = 'notes.png'55NOTES_ICON = u'notes.png'
55SERVICES_CONTACTS_ICON = 'services-contacts.png'56SERVICES_CONTACTS_ICON = u'services-contacts.png'
56SERVICES_FILES_EXAMPLE = 'services-files-example.png'57SERVICES_FILES_EXAMPLE = u'services-files-example.png'
57SERVICES_FILES_ICON = 'services-files.png'58SERVICES_FILES_ICON = u'services-files.png'
5859
59FILE_URI_PREFIX = 'file://'60FILE_URI_PREFIX = u'file://'
60UBUNTUONE_FROM_OAUTH = 'https://one.ubuntu.com/api/1.0/from_oauth/'
61UBUNTUONE_LINK = 'https://one.ubuntu.com/'
6261
63CONTACTS_LINK = UBUNTUONE_LINK62CONTACTS_LINK = UBUNTUONE_LINK
64EDIT_ACCOUNT_LINK = UBUNTUONE_LINK + 'account/'63EDIT_ACCOUNT_LINK = UBUNTUONE_LINK + u'account/'
65EDIT_DEVICES_LINK = EDIT_ACCOUNT_LINK + 'machines/'64EDIT_DEVICES_LINK = EDIT_ACCOUNT_LINK + u'machines/'
66EDIT_PROFILE_LINK = 'https://login.ubuntu.com/'65EDIT_PROFILE_LINK = u'https://login.ubuntu.com/'
67EDIT_SERVICES_LINK = UBUNTUONE_LINK + 'services'66EDIT_SERVICES_LINK = UBUNTUONE_LINK + u'services'
68FACEBOOK_LINK = 'http://www.facebook.com/ubuntuone/'67FACEBOOK_LINK = u'http://www.facebook.com/ubuntuone/'
69GET_SUPPORT_LINK = UBUNTUONE_LINK + 'support/'68GET_SUPPORT_LINK = UBUNTUONE_LINK + u'support/'
70LEARN_MORE_LINK = UBUNTUONE_LINK69LEARN_MORE_LINK = UBUNTUONE_LINK
71MANAGE_FILES_LINK = UBUNTUONE_LINK + 'files/'70MANAGE_FILES_LINK = UBUNTUONE_LINK + u'files/'
72RESET_PASSWORD_LINK = EDIT_PROFILE_LINK + '+forgot_password'71RESET_PASSWORD_LINK = EDIT_PROFILE_LINK + u'+forgot_password'
73TWITTER_LINK = 'http://twitter.com/ubuntuone/'72TWITTER_LINK = u'http://twitter.com/ubuntuone/'
7473
75ALWAYS_SUBSCRIBED = _('Always in sync')74ALWAYS_SUBSCRIBED = _('Always in sync')
76CONNECT_BUTTON_LABEL = _('Connect to Ubuntu One')75CONNECT_BUTTON_LABEL = _('Connect to Ubuntu One')
@@ -86,7 +85,7 @@
86 'previous values were restored.')85 'previous values were restored.')
87DEVICE_CONFIRM_REMOVE = _('Are you sure you want to remove this device '86DEVICE_CONFIRM_REMOVE = _('Are you sure you want to remove this device '
88 'from Ubuntu One?')87 'from Ubuntu One?')
89DEVICE_REMOVABLE_PREFIX = 'Ubuntu One @ '88DEVICE_REMOVABLE_PREFIX = u'Ubuntu One @ '
90DEVICE_REMOVAL_ERROR = _('The device could not be removed.')89DEVICE_REMOVAL_ERROR = _('The device could not be removed.')
91DEVICES_BUTTON_TOOLTIP = _('Manage devices registered with your personal '90DEVICES_BUTTON_TOOLTIP = _('Manage devices registered with your personal '
92 'cloud')91 'cloud')
@@ -185,19 +184,3 @@
185 str_bytes = str_bytes.rstrip('0')184 str_bytes = str_bytes.rstrip('0')
186 str_bytes = str_bytes.rstrip('.')185 str_bytes = str_bytes.rstrip('.')
187 return '%s %s' % (str_bytes, units[unit])186 return '%s %s' % (str_bytes, units[unit])
188
189
190def sign_url(url, credentials, timestamp=None):
191 """Sign the URL using the currently available credentials."""
192 consumer = oauth.OAuthConsumer(credentials["consumer_key"],
193 credentials["consumer_secret"])
194 token = oauth.OAuthToken(credentials["token"],
195 credentials["token_secret"])
196 parameters = {'next': url, 'oauth_timestamp': timestamp}
197 request = oauth.OAuthRequest.from_consumer_and_token(
198 http_url=UBUNTUONE_FROM_OAUTH, http_method='GET',
199 oauth_consumer=consumer, token=token,
200 parameters=parameters)
201 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
202 request.sign_request(sig_method, consumer, token)
203 return request.to_url()
204187
=== modified file 'ubuntuone/controlpanel/gui/qt/folders.py'
--- ubuntuone/controlpanel/gui/qt/folders.py 2012-01-18 17:40:33 +0000
+++ ubuntuone/controlpanel/gui/qt/folders.py 2012-02-07 14:03:19 +0000
@@ -1,8 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4#2#
5# Copyright 2011 Canonical Ltd.3# Copyright 2011-2012 Canonical Ltd.
6#4#
7# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -85,10 +83,7 @@
85 def on_folder_created(self, new_folder):83 def on_folder_created(self, new_folder):
86 """Reload folder info after folder creation."""84 """Reload folder info after folder creation."""
87 self.is_processing = True85 self.is_processing = True
88 # hack to ensure that syncdaemon updates the folder list.86 self.load()
89 # pylint: disable=W0404, E1101
90 from twisted.internet import reactor
91 reactor.callLater(2, self.load)
9287
93 # pylint: disable=E020288 # pylint: disable=E0202
94 @defer.inlineCallbacks89 @defer.inlineCallbacks
9590
=== modified file 'ubuntuone/controlpanel/gui/qt/gotoweb.py'
--- ubuntuone/controlpanel/gui/qt/gotoweb.py 2011-10-05 23:12:23 +0000
+++ ubuntuone/controlpanel/gui/qt/gotoweb.py 2012-02-07 14:03:19 +0000
@@ -23,8 +23,7 @@
23from twisted.internet import defer23from twisted.internet import defer
2424
25from ubuntuone.controlpanel import cache25from ubuntuone.controlpanel import cache
26from ubuntuone.controlpanel.gui import qt, sign_url, UBUNTUONE_LINK26from ubuntuone.controlpanel.gui import qt
27from ubuntuone.controlpanel.web_client.txwebclient import timestamp_checker
2827
2928
30class GoToWebButton(cache.Cache, QtGui.QPushButton):29class GoToWebButton(cache.Cache, QtGui.QPushButton):
@@ -44,14 +43,5 @@
44 def on_clicked(self):43 def on_clicked(self):
45 """Open self.uri if not None, do nothing otherwise."""44 """Open self.uri if not None, do nothing otherwise."""
46 if self.uri is not None:45 if self.uri is not None:
4746 uri = yield self.backend.build_signed_iri(self.uri)
48 credentials = None
49 if self.uri.startswith(UBUNTUONE_LINK):
50 credentials = yield self.backend.get_credentials()
51
52 uri = self.uri
53 if credentials:
54 timestamp = yield timestamp_checker.get_faithful_time()
55 uri = yield sign_url(uri, credentials, timestamp)
56
57 qt.uri_hook(uri)47 qt.uri_hook(uri)
5848
=== modified file 'ubuntuone/controlpanel/gui/qt/gui.py'
--- ubuntuone/controlpanel/gui/qt/gui.py 2011-09-15 14:42:24 +0000
+++ ubuntuone/controlpanel/gui/qt/gui.py 2012-02-07 14:03:19 +0000
@@ -52,20 +52,20 @@
52 # pylint: enable=C010352 # pylint: enable=C0103
5353
5454
55def start(stop, minimized=False, with_icon=False):55def start(close_callback, minimized=False, with_icon=False):
56 """Show the UI elements."""56 """Show the UI elements."""
57 # pylint: disable=W0404, F040157 # pylint: disable=W0404, F0401
58 if not minimized:58 if not minimized:
59 if with_icon or minimized:59 if with_icon or minimized:
60 window = MainWindow()60 window = MainWindow()
61 else:61 else:
62 window = MainWindow(close_callback=stop)62 window = MainWindow(close_callback=close_callback)
63 window.show()63 window.show()
64 else:64 else:
65 window = None65 window = None
66 if with_icon or minimized:66 if with_icon or minimized:
67 QtGui.QApplication.instance().setQuitOnLastWindowClosed(False)67 QtGui.QApplication.instance().setQuitOnLastWindowClosed(False)
68 icon = TrayIcon(window=window)68 icon = TrayIcon(window=window, close_callback=close_callback)
69 else:69 else:
70 icon = None70 icon = None
71 return icon, window71 return icon, window
7272
=== modified file 'ubuntuone/controlpanel/gui/qt/main/__init__.py'
--- ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-01-19 19:00:54 +0000
+++ ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-02-07 14:03:19 +0000
@@ -14,7 +14,7 @@
14# You should have received a copy of the GNU General Public License along14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.15# with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17"""Provide the correct reactor and ui integration."""17"""Provide the correct ui main module."""
1818
19import sys19import sys
2020
@@ -37,32 +37,23 @@
3737
3838
39def main(switch_to='', alert=False, minimized=False, with_icon=False):39def main(switch_to='', alert=False, minimized=False, with_icon=False):
40 """Start the Qt reactor and open the main window."""40 """Start the Qt mainloop and open the main window."""
41 # The following cannot be imported outside this function41 # The following cannot be imported outside this function
42 # because u1trial already provides a reactor.42 # because u1trial already provides a reactor.
4343
44 # The main loop MUST be initialized before importing the reactor
45 app = UniqueApplication(sys.argv, "ubuntuone-control-panel")44 app = UniqueApplication(sys.argv, "ubuntuone-control-panel")
46 source.main(app)45 source.main(app)
46
47 qss = QtCore.QResource(":/ubuntuone.qss")47 qss = QtCore.QResource(":/ubuntuone.qss")
48 app.setStyleSheet(qss.data())48 app.setStyleSheet(qss.data())
4949
50 # Reimport 'qt4reactor', 'reactor', 'start', pylint: disable=W0404, F0401
51 import qt4reactor
52 qt4reactor.install()
53
54 from twisted.internet import reactor
55 from ubuntuone.controlpanel.gui.qt.gui import start50 from ubuntuone.controlpanel.gui.qt.gui import start
56 # pylint: enable=W0404, F0401
57
58 # Module 'reactor' has no 'run'/'stop' member, pylint: disable=E1101
5951
60 # Unused variable 'window', 'icon', pylint: disable=W061252 # Unused variable 'window', 'icon', pylint: disable=W0612
61 icon, window = start(reactor.stop,53 icon, window = start(lambda: source.main_quit(app),
62 minimized=minimized, with_icon=with_icon)54 minimized=minimized, with_icon=with_icon)
63 # pylint: enable=W061255 # pylint: enable=W0612
64 if icon:56 if icon:
65 app.new_instance.connect(icon.restore_window)57 app.new_instance.connect(icon.restore_window)
6658
67 reactor.run()59 source.main_start(app)
68 # pylint: enable=E1101
6960
=== modified file 'ubuntuone/controlpanel/gui/qt/main/linux.py'
--- ubuntuone/controlpanel/gui/qt/main/linux.py 2012-01-26 12:41:08 +0000
+++ ubuntuone/controlpanel/gui/qt/main/linux.py 2012-02-07 14:03:19 +0000
@@ -16,10 +16,18 @@
1616
17"""Main method to be used on linux."""17"""Main method to be used on linux."""
1818
19from dbus.mainloop.qt import DBusQtMainLoop
20
2119
22def main(app):20def main(app):
23 """Apply style sheet."""21 """Apply style sheet."""
24 # The DBus main loop MUST be initialized before importing the reactor22 from dbus.mainloop.qt import DBusQtMainLoop
25 DBusQtMainLoop(set_as_default=True)23 DBusQtMainLoop(set_as_default=True)
24
25
26def main_start(app):
27 """Start the mainloop."""
28 app.exec_()
29
30
31def main_quit(app):
32 """Stop the mainloop."""
33 app.exit()
2634
=== modified file 'ubuntuone/controlpanel/gui/qt/main/windows.py'
--- ubuntuone/controlpanel/gui/qt/main/windows.py 2012-01-26 12:41:08 +0000
+++ ubuntuone/controlpanel/gui/qt/main/windows.py 2012-02-07 14:03:19 +0000
@@ -24,3 +24,24 @@
24 # Apply font to the entire application24 # Apply font to the entire application
25 QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf')25 QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-R.ttf')
26 QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf')26 QtGui.QFontDatabase.addApplicationFont(':/Ubuntu-B.ttf')
27
28 import qt4reactor
29 qt4reactor.install()
30
31
32# Module 'reactor' has no 'run'/'stop' member, pylint: disable=E1101
33
34
35def main_start(app):
36 """Start the mainloop."""
37 from twisted.internet import reactor
38 reactor.run()
39
40
41def main_quit(app):
42 """Stop the mainloop."""
43 from twisted.internet import reactor
44 reactor.stop()
45
46
47# pylint: enable=E1101
2748
=== modified file 'ubuntuone/controlpanel/gui/qt/systray.py'
--- ubuntuone/controlpanel/gui/qt/systray.py 2011-12-27 13:54:49 +0000
+++ ubuntuone/controlpanel/gui/qt/systray.py 2012-02-07 14:03:19 +0000
@@ -25,7 +25,7 @@
2525
26 """System notification icon."""26 """System notification icon."""
2727
28 def __init__(self, window=None):28 def __init__(self, window=None, close_callback=lambda: None):
29 super(TrayIcon, self).__init__(None)29 super(TrayIcon, self).__init__(None)
30 self.setIcon(QtGui.QIcon(":/u1icon.png"))30 self.setIcon(QtGui.QIcon(":/u1icon.png"))
31 self.setVisible(True)31 self.setVisible(True)
@@ -41,6 +41,8 @@
41 self.context_menu.addAction(self.quit)41 self.context_menu.addAction(self.quit)
42 self.setContextMenu(self.context_menu)42 self.setContextMenu(self.context_menu)
4343
44 self.close_callback = close_callback
45
44 def on_activated(self, reason):46 def on_activated(self, reason):
45 """The user activated the icon."""47 """The user activated the icon."""
46 if reason == self.Trigger: # Left-click48 if reason == self.Trigger: # Left-click
@@ -73,7 +75,4 @@
73 except:75 except:
74 # Maybe it was not running?76 # Maybe it was not running?
75 pass77 pass
76 # pylint: disable=W040478 self.close_callback()
77 from twisted.internet import reactor
78 # pylint: enable=W0404
79 reactor.stop()
8079
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/__init__.py'
--- ubuntuone/controlpanel/gui/qt/tests/__init__.py 2012-01-18 19:52:25 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/__init__.py 2012-02-07 14:03:19 +0000
@@ -20,7 +20,6 @@
2020
21import logging21import logging
22import os22import os
23import urllib
2423
25from PyQt4 import QtGui, QtCore24from PyQt4 import QtGui, QtCore
26from twisted.internet import defer25from twisted.internet import defer
@@ -28,8 +27,7 @@
2827
29from ubuntuone.controlpanel import backend, cache28from ubuntuone.controlpanel import backend, cache
30from ubuntuone.controlpanel.tests import TestCase, EXPECTED_ACCOUNT_INFO, TOKEN29from ubuntuone.controlpanel.tests import TestCase, EXPECTED_ACCOUNT_INFO, TOKEN
31from ubuntuone.controlpanel.gui import qt, UBUNTUONE_FROM_OAUTH30from ubuntuone.controlpanel.gui import qt
32from ubuntuone.controlpanel.gui.qt import gotoweb
33from ubuntuone.controlpanel.gui.tests import FakedObject, USER_HOME31from ubuntuone.controlpanel.gui.tests import FakedObject, USER_HOME
3432
35# Attribute 'yyy' defined outside __init__, access to a protected member33# Attribute 'yyy' defined outside __init__, access to a protected member
@@ -107,19 +105,31 @@
107105
108 next_result = []106 next_result = []
109 exposed_methods = [107 exposed_methods = [
110 'account_info', # account108 'account_info',
111 'devices_info', 'device_names_info', # devices109 'build_signed_iri',
112 'change_device_settings', 'remove_device',110 'change_device_settings',
113 'volumes_info', 'change_volume_settings', # volumes111 'change_file_sync_settings',
114 'create_folder', 'validate_path_for_folder',112 'change_replication_settings',
115 'replications_info', 'change_replication_settings', # replications113 'change_volume_settings',
116 'file_sync_status', 'enable_files', 'disable_files', # files114 'connect_files',
117 'connect_files', 'disconnect_files',115 'create_folder',
118 'restart_files', 'start_files', 'stop_files',116 'device_names_info',
117 'devices_info',
118 'disable_files',
119 'disconnect_files',
120 'enable_files',
119 'file_sync_settings_info',121 'file_sync_settings_info',
120 'change_file_sync_settings',122 'file_sync_status',
123 'login',
124 'remove_device',
125 'replications_info',
126 'restart_files',
121 'restore_file_sync_settings',127 'restore_file_sync_settings',
122 'shutdown', 'login',128 'shutdown',
129 'start_files',
130 'stop_files',
131 'validate_path_for_folder',
132 'volumes_info',
123 ]133 ]
124134
125 def get_credentials(self):135 def get_credentials(self):
@@ -131,6 +141,9 @@
131 """Fake home return."""141 """Fake home return."""
132 return USER_HOME142 return USER_HOME
133143
144 def build_signed_iri(self, iri):
145 """Fake iri signing."""
146
134147
135class CrashyBackendException(Exception):148class CrashyBackendException(Exception):
136 """A faked backend crash."""149 """A faked backend crash."""
@@ -284,19 +297,14 @@
284297
285 def assert_uri_hook_called(self, button, url):298 def assert_uri_hook_called(self, button, url):
286 """Check that uri_hook was called with 'url' when clicking 'button'."""299 """Check that uri_hook was called with 'url' when clicking 'button'."""
287 fake_time = 12345678300 self.patch(self.ui.backend, 'next_result', url)
288 self.patch(qt, 'uri_hook', self._set_called)301 self.patch(qt, 'uri_hook', self._set_called)
289 self.patch(gotoweb.timestamp_checker, "get_faithful_time",
290 lambda: defer.succeed(fake_time))
291 button.click()302 button.click()
292303
293 self.assertEqual(len(self._called), 2, 'uri_hook must be called.')304 self.assertEqual(len(self._called), 2, 'uri_hook must be called.')
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.')
295 actual_url = self._called[0][0]306 actual_url = self._called[0][0]
296 if actual_url.startswith(UBUNTUONE_FROM_OAUTH):307 self.assertEqual(actual_url, url)
297 self.assertIn(urllib.urlencode({'next': url}), actual_url)
298 else:
299 self.assertEqual(actual_url, url)
300308
301 def test_init_loads_ui(self, expected_setup_ui=None):309 def test_init_loads_ui(self, expected_setup_ui=None):
302 """The __init__ method loads the ui."""310 """The __init__ method loads the ui."""
303311
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py 2011-10-06 20:33:42 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_gotoweb.py 2012-02-07 14:03:19 +0000
@@ -20,7 +20,7 @@
2020
21from twisted.internet import defer21from twisted.internet import defer
2222
23from ubuntuone.controlpanel.gui import qt, UBUNTUONE_LINK23from ubuntuone.controlpanel.gui import qt
24from ubuntuone.controlpanel.gui.qt import gotoweb as gui24from ubuntuone.controlpanel.gui.qt import gotoweb as gui
25from ubuntuone.controlpanel.gui.qt.tests import (25from ubuntuone.controlpanel.gui.qt.tests import (
26 BaseTestCase,26 BaseTestCase,
@@ -53,11 +53,14 @@
5353
54 def test_open_uri_when_clicked(self):54 def test_open_uri_when_clicked(self):
55 """When clicking the button, the uri is opened."""55 """When clicking the button, the uri is opened."""
56 expected_iri = 'foo-bar-baz'
57 self.patch(self.ui.backend, 'build_signed_iri', lambda i: expected_iri)
56 self.patch(qt, 'uri_hook', self._set_called)58 self.patch(qt, 'uri_hook', self._set_called)
57 self.ui.uri = 'yadda-yadda-yoo'59 self.ui.uri = 'yadda-yadda-yoo'
60
58 self.ui.click()61 self.ui.click()
5962
60 self.assertEqual(self._called, ((self.ui.uri,), {}))63 self.assertEqual(self._called, ((expected_iri,), {}))
6164
62 def test_do_nothing_on_clicked_if_uri_is_none(self):65 def test_do_nothing_on_clicked_if_uri_is_none(self):
63 """When clicking the button, if the uri is None, do nothing."""66 """When clicking the button, if the uri is None, do nothing."""
@@ -66,53 +69,3 @@
66 self.ui.click()69 self.ui.click()
6770
68 self.assertEqual(self._called, False)71 self.assertEqual(self._called, False)
69
70
71class SignUrlTestCase(GoToWebButtonTestCase):
72 """The test suite for the sign url management."""
73
74 @defer.inlineCallbacks
75 def setUp(self):
76 yield super(SignUrlTestCase, self).setUp()
77 self.patch(qt, 'uri_hook', lambda url: None)
78 self.patch(gui, 'sign_url', self._set_called)
79 self.creds = yield self.ui.backend.get_credentials()
80 assert len(self.creds) > 0
81
82 def test_without_ubuntuone_prefix(self):
83 """If given url is not an ubuntuone url, don't sign it."""
84 self.ui.uri = 'bad_prefix' + UBUNTUONE_LINK
85 self.ui.click()
86
87 self.assertFalse(self._called)
88
89 def test_with_ubuntuone_prefix(self):
90 """If given url is an ubuntuone url, sign it."""
91 fake_time = 12345
92 self.ui.uri = UBUNTUONE_LINK + 'foo'
93 self.patch(gui.timestamp_checker, "get_faithful_time",
94 lambda: defer.succeed(fake_time))
95 self.ui.click()
96
97 expected_call = ((self.ui.uri, self.creds, fake_time), {})
98 self.assertEqual(self._called, expected_call)
99
100
101class SignUrlNoCredsTestCase(SignUrlTestCase):
102 """The test suite for the sign url management when there are no creds."""
103
104 @defer.inlineCallbacks
105 def setUp(self):
106 yield super(SignUrlNoCredsTestCase, self).setUp()
107 self.patch(self.ui.backend, 'get_credentials', lambda: {})
108
109 def test_with_ubuntuone_prefix(self):
110 """If given url is an ubuntuone url, don't sign it.
111
112 Since we have no credentials, the url should not be signed.
113
114 """
115 self.ui.uri = UBUNTUONE_LINK + 'foo'
116 self.ui.click()
117
118 self.assertFalse(self._called)
11972
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_signin.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2011-09-07 17:56:17 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2012-02-07 14:03:19 +0000
@@ -20,7 +20,6 @@
2020
21from twisted.internet import defer21from twisted.internet import defer
2222
23from ubuntuone.controlpanel.gui import qt
24from ubuntuone.controlpanel.gui.qt import signin as gui23from ubuntuone.controlpanel.gui.qt import signin as gui
25from ubuntuone.controlpanel.gui.qt.tests import (24from ubuntuone.controlpanel.gui.qt.tests import (
26 CrashyBackend,25 CrashyBackend,
@@ -107,10 +106,8 @@
107106
108 def test_forgot_password_button(self):107 def test_forgot_password_button(self):
109 """When clicking the forgot passsword btn, the proper url is opened."""108 """When clicking the forgot passsword btn, the proper url is opened."""
110 self.patch(qt, 'uri_hook', self._set_called)109 self.assert_uri_hook_called(self.ui.ui.forgot_password_button,
111 self.ui.ui.forgot_password_button.click()110 gui.RESET_PASSWORD_LINK)
112
113 self.assertEqual(self._called, ((gui.RESET_PASSWORD_LINK,), {}))
114111
115112
116class SignInButtonPanelTestCase(BaseSignInPanelTestCase):113class SignInButtonPanelTestCase(BaseSignInPanelTestCase):
117114
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_start.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_start.py 2011-09-15 15:37:04 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_start.py 2012-02-07 14:03:19 +0000
@@ -1,91 +1,89 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3# Author: Roberto Alsina <roberto.alsina@canonical.com>3# Author: Roberto Alsina <roberto.alsina@canonical.com>
4#4#
5# Copyright 2011 Canonical Ltd.5# Copyright 2011 Canonical Ltd.
6#6#
7# This program is free software: you can redistribute it and/or modify it7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.9# by the Free Software Foundation.
10#10#
11# This program is distributed in the hope that it will be useful, but11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.14# PURPOSE. See the GNU General Public License for more details.
15#15#
16# You should have received a copy of the GNU General Public License along16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.17# with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19"""Tests for the start function."""19"""Tests for the start function."""
2020
21from twisted.internet import defer21from twisted.internet import defer
2222
23from ubuntuone.controlpanel.gui.qt import gui23from ubuntuone.controlpanel.gui.qt import gui
24from ubuntuone.controlpanel.gui.qt.tests import NO_OP24from ubuntuone.controlpanel.tests import TestCase
25from ubuntuone.controlpanel.tests import TestCase25
2626
2727class FakeThing(object):
28class FakeThing(object):28
2929 """A fake thing."""
30 """A fake thing."""30
3131 def __init__(self):
32 def __init__(self):32 self.args = []
33 self.args = []33 self.shown = False
34 self.shown = False34
3535 def __call__(self, *args, **kwargs):
36 def __call__(self, *args, **kwargs):36 self.args.append((args, kwargs))
37 self.args.append((args, kwargs))37 return self
38 return self38
3939 def show(self):
40 def show(self):40 """Show."""
41 """Show."""41 self.shown = True
42 self.shown = True42
4343
4444class StartTestCase(TestCase):
45class FakeReactor(object):45 """Test the qt control panel."""
46 """A fake reactor."""46
4747 @defer.inlineCallbacks
48 def run(self):48 def setUp(self):
49 """Start."""49 yield super(StartTestCase, self).setUp()
5050 self.main_window = FakeThing()
51 def stop(self):51 self.tray_icon = FakeThing()
52 """Stop."""52 self.patch(gui, "MainWindow", self.main_window)
5353 self.patch(gui, "TrayIcon", self.tray_icon)
5454
55class StartTestCase(TestCase):55 def close_cb(self):
56 """Test the qt control panel."""56 """A dummy close callback."""
5757
58 @defer.inlineCallbacks58 def test_minimized(self):
59 def setUp(self):59 """Test behaviour with minimized=True."""
60 yield super(StartTestCase, self).setUp()60 gui.start(close_callback=self.close_cb,
61 self.main_window = FakeThing()61 minimized=True, with_icon=True)
62 self.tray_icon = FakeThing()62 kwargs = {'close_callback': self.close_cb, 'window': None}
63 self.patch(gui, "MainWindow", self.main_window)63 self.assertEqual(self.tray_icon.args, [((), kwargs)])
64 self.patch(gui, "TrayIcon", self.tray_icon)64 self.assertEqual(self.main_window.args, [])
6565
66 def test_minimized(self):66 def test_with_icon(self):
67 """Test behaviour with minimized=True."""67 """Test behaviour with with_icon=True."""
68 gui.start(NO_OP, minimized=True, with_icon=True)68 gui.start(close_callback=self.close_cb,
69 self.assertEqual(self.tray_icon.args, [((), {'window': None})])69 with_icon=True, minimized=False)
70 self.assertEqual(self.main_window.args, [])70 kwargs = {'close_callback': self.close_cb, 'window': self.main_window}
7171 self.assertEqual(self.main_window.args, [((), {})])
72 def test_with_icon(self):72 self.assertEqual(self.tray_icon.args, [((), kwargs)])
73 """Test behaviour with with_icon=True."""73
74 gui.start(NO_OP, with_icon=True, minimized=False)74 def test_both_false(self):
75 self.assertEqual(self.main_window.args, [((), {})])75 """Test behaviour when with_icon and minimized are False."""
76 self.assertEqual(self.tray_icon.args, [((),76 gui.start(close_callback=self.close_cb,
77 {'window': self.main_window})])77 with_icon=False, minimized=False)
7878 # Should be called
79 def test_both_false(self):79 self.assertNotEqual(self.main_window.args, [])
80 """Test behaviour when with_icon and minimized are False."""80 # Should not be called
81 gui.start(NO_OP, with_icon=False, minimized=False)81 self.assertEqual(self.tray_icon.args, [])
82 # Should be called82
83 self.assertNotEqual(self.main_window.args, [])83 def test_both_true(self):
84 # Should not be called84 """Test behaviour when with_icon and minimized are True."""
85 self.assertEqual(self.tray_icon.args, [])85 gui.start(close_callback=self.close_cb,
8686 with_icon=True, minimized=True)
87 def test_both_true(self):87 kwargs = {'close_callback': self.close_cb, 'window': None}
88 """Test behaviour when with_icon and minimized are True."""88 self.assertEqual(self.tray_icon.args, [((), kwargs)])
89 gui.start(NO_OP, with_icon=True, minimized=True)89 self.assertEqual(self.main_window.args, [])
90 self.assertEqual(self.tray_icon.args, [((), {'window': None})])
91 self.assertEqual(self.main_window.args, [])
9290
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_systray.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-01-02 19:38:14 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-02-07 14:03:19 +0000
@@ -19,7 +19,6 @@
19"""Tests for the notification area icon."""19"""Tests for the notification area icon."""
2020
21from PyQt4 import QtGui21from PyQt4 import QtGui
22from twisted.internet import reactor
23from twisted.internet.defer import inlineCallbacks22from twisted.internet.defer import inlineCallbacks
2423
25from ubuntuone.controlpanel.gui.qt import systray24from ubuntuone.controlpanel.gui.qt import systray
@@ -65,7 +64,6 @@
65 """Quit should call SyncDaemonTool.quit()."""64 """Quit should call SyncDaemonTool.quit()."""
66 st = FakeSDTool()65 st = FakeSDTool()
67 self.patch(systray, "SyncDaemonTool", lambda: st)66 self.patch(systray, "SyncDaemonTool", lambda: st)
68 self.patch(reactor, "stop", lambda: None)
69 tray = systray.TrayIcon()67 tray = systray.TrayIcon()
70 yield tray.stop()68 yield tray.stop()
71 self.assertTrue(st.called)69 self.assertTrue(st.called)
7270
=== removed file 'ubuntuone/controlpanel/gui/tests/test_url_sign.py'
--- ubuntuone/controlpanel/gui/tests/test_url_sign.py 2011-10-05 23:12:23 +0000
+++ ubuntuone/controlpanel/gui/tests/test_url_sign.py 1970-01-01 00:00:00 +0000
@@ -1,102 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Roberto Alsina <roberto.alsina@canonical.com>
4# Authors: Alejandro J. Cura <alecu@canonical.com>
5#
6# Copyright 2011 Canonical Ltd.
7#
8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published
10# by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranties of
14# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15# PURPOSE. See the GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program. If not, see <http://www.gnu.org/licenses/>.
19
20"""Tests for the url signing function."""
21
22from urlparse import urlparse, parse_qs
23
24from ubuntuone.controlpanel.tests import TestCase
25from ubuntuone.controlpanel.gui import (
26 sign_url,
27 UBUNTUONE_FROM_OAUTH,
28)
29
30TOKEN = {u'consumer_key': u'consumer_key',
31 u'consumer_secret': u'consumer_secret',
32 u'token_name': u'test_token',
33 u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
34 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
35
36SAMPLE_SIGNED = UBUNTUONE_FROM_OAUTH + '?oauth_nonce=' \
37 '36886134&oauth_timestamp=1310671062&oauth_consumer_key=consumer_key&' \
38 'oauth_signature_method=HMAC-SHA1&next=%2Fblah&oauth_version=1.0&' \
39 'oauth_token=GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo&' \
40 'oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo%3D'
41
42#pylint: disable=E1101
43
44
45class SignURLTestCase(TestCase):
46
47 """Test cases for the URL signing function."""
48
49 def test_is_correct_domain(self):
50 """Test that we are using the right domain."""
51 signed = sign_url("/blah", TOKEN)
52 parsed_signed = urlparse(signed)
53 parsed_sample = urlparse(SAMPLE_SIGNED)
54 self.assertEqual(parsed_signed.netloc, parsed_sample.netloc)
55
56 def test_is_correct_path(self):
57 """Test that we are using the right path in the URL."""
58 signed = sign_url("/blah", TOKEN)
59 parsed_signed = urlparse(signed)
60 parsed_sample = urlparse(SAMPLE_SIGNED)
61 self.assertEqual(parsed_signed.path, parsed_sample.path)
62
63 def test_is_correct_scheme(self):
64 """Test that we are using the right scheme."""
65 signed = sign_url("/blah", TOKEN)
66 parsed_signed = urlparse(signed)
67 parsed_sample = urlparse(SAMPLE_SIGNED)
68
69 self.assertEqual(parsed_signed.scheme, parsed_sample.scheme)
70
71 def test_correct_query(self):
72 """Test the invariant parts of the signed URL."""
73 signed = sign_url("/blah", TOKEN)
74 parsed_signed = urlparse(signed)
75 parsed_sample = urlparse(SAMPLE_SIGNED)
76 signed_query = parse_qs(parsed_signed.query)
77 sample_query = parse_qs(parsed_sample.query)
78
79 for key in ('next',
80 'oauth_consumer_key',
81 'oauth_signature_method',
82 'oauth_token',
83 'oauth_version'):
84 self.assertEqual("%s=%s" % (key, signed_query[key]),
85 "%s=%s" % (key, sample_query[key]))
86
87 def test_url_with_query(self):
88 """Test that we are using the right scheme."""
89 signed = sign_url("/blah?foo=bar", TOKEN)
90 parsed_signed = urlparse(signed)
91 signed_query = parse_qs(parsed_signed.query)
92
93 self.assertEqual(signed_query['next'], ['/blah?foo=bar'])
94
95 def test_uses_timestamper(self):
96 """Test that the signed url is using the server-relative timestamp."""
97 timestamp = 999
98 signed = sign_url("/blah?foo=bar", TOKEN, timestamp)
99 parsed_signed = urlparse(signed)
100 signed_query = parse_qs(parsed_signed.query)
101
102 self.assertEqual(signed_query['oauth_timestamp'], [str(timestamp)])
1030
=== modified file 'ubuntuone/controlpanel/tests/test_backend.py'
--- ubuntuone/controlpanel/tests/test_backend.py 2012-01-26 12:44:10 +0000
+++ ubuntuone/controlpanel/tests/test_backend.py 2012-02-07 14:03:19 +0000
@@ -43,6 +43,8 @@
43 FILE_SYNC_SYNCING,43 FILE_SYNC_SYNCING,
44 FILE_SYNC_UNKNOWN,44 FILE_SYNC_UNKNOWN,
45 MSG_KEY, STATUS_KEY,45 MSG_KEY, STATUS_KEY,
46 UBUNTUONE_FROM_OAUTH,
47 UBUNTUONE_LINK,
46)48)
47from ubuntuone.controlpanel.tests import (TestCase,49from ubuntuone.controlpanel.tests import (TestCase,
48 EMPTY_DESCRIPTION_JSON,50 EMPTY_DESCRIPTION_JSON,
@@ -65,6 +67,12 @@
65 USER_HOME,67 USER_HOME,
66)68)
6769
70SAMPLE_SIGNED = UBUNTUONE_FROM_OAUTH + '?oauth_nonce=' \
71 '36886134&oauth_timestamp=1310671062&oauth_consumer_key=consumer_key&' \
72 'oauth_signature_method=HMAC-SHA1&next=%2Fblah&oauth_version=1.0&' \
73 'oauth_token=GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo&' \
74 'oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo%3D'
75
6876
69# pylint: disable=E1101, W0201, W021277# pylint: disable=E1101, W0201, W0212
7078
@@ -104,6 +112,13 @@
104 result = simplejson.loads(self.results[method])112 result = simplejson.loads(self.results[method])
105 return defer.succeed(result)113 return defer.succeed(result)
106114
115 @defer.inlineCallbacks
116 def build_signed_iri(self, iri, params):
117 """Fake the IRI signing."""
118 creds = yield self.get_credentials()
119 result = u'%s-%s-%s' % (iri, unicode(creds), unicode(params))
120 defer.returnValue(result)
121
107122
108class MockLoginClient(CallRecorder):123class MockLoginClient(CallRecorder):
109 """A mock login_client module."""124 """A mock login_client module."""
@@ -347,7 +362,7 @@
347 @defer.inlineCallbacks362 @defer.inlineCallbacks
348 def setUp(self):363 def setUp(self):
349 yield super(BackendBasicTestCase, self).setUp()364 yield super(BackendBasicTestCase, self).setUp()
350 self.patch(backend, "web_client_factory", MockWebClient)365 self.patch(backend, "WebClient", MockWebClient)
351 self.patch(backend, "CredentialsManagementTool", MockLoginClient)366 self.patch(backend, "CredentialsManagementTool", MockLoginClient)
352 self.patch(backend.sd_client, "SyncDaemonClient", MockSDClient)367 self.patch(backend.sd_client, "SyncDaemonClient", MockSDClient)
353 self.patch(backend, "replication_client", MockReplicationClient())368 self.patch(backend, "replication_client", MockReplicationClient())
@@ -368,12 +383,14 @@
368 @inlineCallbacks383 @inlineCallbacks
369 def test_get_token(self):384 def test_get_token(self):
370 """The get_token method returns the right token."""385 """The get_token method returns the right token."""
386 self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN))
371 token = yield self.be.get_token()387 token = yield self.be.get_token()
372 self.assertEqual(token, TOKEN["token"])388 self.assertEqual(token, TOKEN["token"])
373389
374 @inlineCallbacks390 @inlineCallbacks
375 def test_device_is_local(self):391 def test_device_is_local(self):
376 """The device_is_local returns the right result."""392 """The device_is_local returns the right result."""
393 self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN))
377 result = yield self.be.device_is_local(self.local_token)394 result = yield self.be.device_is_local(self.local_token)
378 self.assertTrue(result)395 self.assertTrue(result)
379396
@@ -408,6 +425,54 @@
408 self.assertIs(self.be.sd_client, self.be.sd_client)425 self.assertIs(self.be.sd_client, self.be.sd_client)
409426
410427
428class SignIriTestCase(BackendBasicTestCase):
429 """Test cases for the IRI signing function."""
430
431 @defer.inlineCallbacks
432 def setUp(self):
433 yield super(SignIriTestCase, self).setUp()
434 self.patch(self.be, 'get_credentials', lambda: defer.succeed(TOKEN))
435
436 @inlineCallbacks
437 def test_without_ubuntuone_prefix(self):
438 """If given url is not an ubuntuone url, don't sign it."""
439 iri = 'bad_prefix' + UBUNTUONE_LINK
440 result = yield self.be.build_signed_iri(iri)
441
442 self.assertEqual(result, iri)
443
444 @inlineCallbacks
445 def test_with_ubuntuone_prefix(self):
446 """If given url is an ubuntuone url, sign it."""
447 iri = UBUNTUONE_LINK + 'foo'
448 result = yield self.be.build_signed_iri(iri)
449
450 expected = yield self.be.wc.build_signed_iri(UBUNTUONE_FROM_OAUTH,
451 {'next': iri})
452 self.assertEqual(expected, result)
453
454
455class SignIriNoCredsTestCase(SignIriTestCase):
456 """The test suite for the sign url management when there are no creds."""
457
458 @defer.inlineCallbacks
459 def setUp(self):
460 yield super(SignIriNoCredsTestCase, self).setUp()
461 self.patch(self.be, 'get_credentials', lambda: defer.succeed({}))
462
463 @inlineCallbacks
464 def test_with_ubuntuone_prefix(self):
465 """If given url is an ubuntuone url, don't sign it.
466
467 Since we have no credentials, the url should not be signed.
468
469 """
470 iri = UBUNTUONE_LINK + 'foo'
471 result = yield self.be.build_signed_iri(iri)
472
473 self.assertEqual(result, iri)
474
475
411class BackendCredentialsTestCase(BackendBasicTestCase):476class BackendCredentialsTestCase(BackendBasicTestCase):
412 """Credentials tests for the backend."""477 """Credentials tests for the backend."""
413478
414479
=== modified file 'ubuntuone/controlpanel/tests/test_web_client.py'
--- ubuntuone/controlpanel/tests/test_web_client.py 2011-10-07 14:36:14 +0000
+++ ubuntuone/controlpanel/tests/test_web_client.py 2012-02-07 14:03:19 +0000
@@ -1,9 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4# Authors: Natalia B. Bidart <nataliabidart@canonical.com>
5#2#
6# Copyright 2010 Canonical Ltd.3# Copyright 2011-2012 Canonical Ltd.
7#4#
8# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -17,26 +14,38 @@
17# You should have received a copy of the GNU General Public License along14# You should have received a copy of the GNU General Public License along
18# with this program. If not, see <http://www.gnu.org/licenses/>.15# with this program. If not, see <http://www.gnu.org/licenses/>.
1916
20"""Integration tests for the control panel backend webservice client."""17"""Integration tests for the webclient."""
2118
22from twisted.application import internet, service19from urlparse import urlparse, parse_qs
20
23from twisted.internet import defer21from twisted.internet import defer
24from twisted.internet.defer import inlineCallbacks22from twisted.web import resource
25from twisted.web import server, resource23
2624from ubuntu_sso.utils.webclient.tests import BaseMockWebServer
27from ubuntuone.controlpanel import web_client25
28from ubuntuone.controlpanel.tests import TestCase26from ubuntuone.controlpanel.tests import TestCase
27from ubuntuone.controlpanel.web_client import (
28 UnauthorizedError,
29 WebClient,
30 WebClientError,
31)
2932
3033
31SAMPLE_KEY = "result"34SAMPLE_KEY = "result"
32SAMPLE_VALUE = "sample result"35SAMPLE_VALUE = "sample result"
33SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)36SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)
34SAMPLE_CREDENTIALS = dict(37SAMPLE_CREDENTIALS = dict(
35 consumer_key="consumer key",38 consumer_key="consumer_key",
36 consumer_secret="consumer secret",39 consumer_secret="consumer_secret",
37 token="the real token",40 token="the_real_token",
38 token_secret="the token secret",41 token_secret="the_token_secret",
39)42)
43SAMPLE_TARGET = u'http://example.com/'
44SAMPLE_SIGNED = SAMPLE_TARGET + u'?oauth_nonce=36886134&' \
45 'oauth_timestamp=1328544832&oauth_consumer_key=%s&' \
46 'oauth_signature_method=HMAC-SHA1&next=%%2Fblah&oauth_version=1.0&' \
47 'oauth_token=%s&oauth_signature=s6h0LRBiWchTADrTJWaJUSuaGpo3D' % \
48 (SAMPLE_CREDENTIALS['consumer_key'], SAMPLE_CREDENTIALS['token'])
4049
4150
42def sample_get_credentials():51def sample_get_credentials():
@@ -63,11 +72,11 @@
63 return self.contents72 return self.contents
6473
6574
66class MockWebService(object):75class MockWebServer(BaseMockWebServer):
67 """A mock webservice for testing"""76 """A mock webserver for the webclient tests."""
6877
69 def __init__(self):78 def get_root_resource(self):
70 """Start up this instance."""79 """Get the root resource with all the children."""
71 root = resource.Resource()80 root = resource.Resource()
72 devices_resource = MockResource()81 devices_resource = MockResource()
73 devices_resource.contents = SAMPLE_RESOURCE82 devices_resource.contents = SAMPLE_RESOURCE
@@ -77,91 +86,105 @@
77 "Unauthrorized", "Unauthrorized")86 "Unauthrorized", "Unauthrorized")
78 root.putChild("unauthorized", unauthorized)87 root.putChild("unauthorized", unauthorized)
7988
80 site = server.Site(root)89 return root
81 application = service.Application('web')
82 self.service_collection = service.IServiceCollection(application)
83 #pylint: disable=E1101
84 self.tcpserver = internet.TCPServer(0, site)
85 self.tcpserver.setServiceParent(self.service_collection)
86 self.service_collection.startService()
87
88 def get_url(self):
89 """Build the url for this mock server."""
90 #pylint: disable=W0212
91 port_num = self.tcpserver._port.getHost().port
92 return "http://localhost:%d/" % port_num
93
94 def stop(self):
95 """Shut it down."""
96 #pylint: disable=E1101
97 return self.service_collection.stopService()
9890
9991
100class WebClientTestCase(TestCase):92class WebClientTestCase(TestCase):
101 """Test for the webservice client."""93 """Test for the webservice client."""
10294
103 timeout = 895 timeout = 5
10496
105 @inlineCallbacks97 @defer.inlineCallbacks
106 def setUp(self):98 def setUp(self):
107 yield super(WebClientTestCase, self).setUp()99 yield super(WebClientTestCase, self).setUp()
108 self.ws = MockWebService()100 self.ws = MockWebServer()
109 test_base_url = self.ws.get_url()101 self.addCleanup(self.ws.stop)
110 self.wc = web_client.web_client_factory(sample_get_credentials,102 self.base_iri = self.ws.get_iri()
111 test_base_url)103
104 self.wc = WebClient(sample_get_credentials, base_url=self.base_iri)
112 self.addCleanup(self.wc.shutdown)105 self.addCleanup(self.wc.shutdown)
113 self.addCleanup(self.ws.stop)
114 web_module = web_client.web_client_module()
115 if getattr(web_module, "timestamp_checker", None):
116 fake_timestamp = 12345678
117 self.patch(web_module.timestamp_checker, "get_faithful_time",
118 lambda: defer.succeed(fake_timestamp))
119106
120 @inlineCallbacks107 @defer.inlineCallbacks
121 def test_get_url(self):108 def test_get_url(self):
122 """A method is successfully called in the mock webservice."""109 """A method is successfully called in the mock webservice."""
123 result = yield self.wc.call_api("devices")110 result = yield self.wc.call_api("devices")
124 self.assertIn(SAMPLE_KEY, result)111 self.assertIn(SAMPLE_KEY, result)
125 self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY])112 self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY])
126113
127 @inlineCallbacks114 @defer.inlineCallbacks
128 def test_get_url_error(self):115 def test_get_url_error(self):
129 """The errback is called when there's some error."""116 """The errback is called when there's some error."""
130 yield self.assertFailure(self.wc.call_api("throwerror"),117 yield self.assertFailure(self.wc.call_api("throwerror"),
131 web_client.WebClientError)118 WebClientError)
132119
133 @inlineCallbacks120 @defer.inlineCallbacks
134 def test_unauthorized(self):121 def test_unauthorized(self):
135 """Detect when a request failed with UNAUTHORIZED."""122 """Detect when a request failed with UNAUTHORIZED."""
136 yield self.assertFailure(self.wc.call_api("unauthorized"),123 yield self.assertFailure(self.wc.call_api("unauthorized"),
137 web_client.UnauthorizedError)124 UnauthorizedError)
138125
139126
140class OAuthTestCase(TestCase):127class WebClientBuildSignedIriTestCase(WebClientTestCase):
141 """Test for the oauth signing code."""128 """Test for the webservice client when signing iris."""
142129
143 def test_build_oauth_headers(self):130 # Instance of 'ParseResult' has no 'foo' member
144 """Build the oauth headers for a sample request."""131 # pylint: disable=E1101
145132
146 sample_method = "GET"133 @defer.inlineCallbacks
147 sample_url = "http://one.ubuntu.com/"134 def test_is_correct_domain(self):
148 timestamp = 1135 """Test that we are using the right domain."""
149 headers = web_client.build_oauth_headers(sample_method, sample_url,136 signed = yield self.wc.build_signed_iri(SAMPLE_TARGET)
150 SAMPLE_CREDENTIALS, timestamp)137 parsed_signed = urlparse(signed)
151 self.assertIn("Authorization", headers)138 parsed_sample = urlparse(SAMPLE_SIGNED)
152139 self.assertEqual(parsed_signed.netloc, parsed_sample.netloc)
153 def test_add_oauth_headers(self):140
154 """Add the OAuth headers to a request."""141 @defer.inlineCallbacks
155142 def test_is_correct_path(self):
156 def sample_build_headers(*a):143 """Test that we are using the right path in the URL."""
157 """Build some sample headers."""144 signed = yield self.wc.build_signed_iri(SAMPLE_TARGET)
158 return {"header1": "h1", "header2": "h2"}145 parsed_signed = urlparse(signed)
159146 parsed_sample = urlparse(SAMPLE_SIGNED)
160 self.patch(web_client, "build_oauth_headers", sample_build_headers)147 self.assertEqual(parsed_signed.path, parsed_sample.path)
161 test_request_headers = {}148
162 append_method = test_request_headers.__setitem__149 @defer.inlineCallbacks
163 timestamp = 12345150 def test_is_correct_scheme(self):
164 web_client.add_oauth_headers(append_method, "GET", "http://this", {},151 """Test that we are using the right scheme."""
165 timestamp)152 signed = yield self.wc.build_signed_iri(SAMPLE_TARGET)
166 self.assertIn("header1", test_request_headers)153 parsed_signed = urlparse(signed)
167 self.assertIn("header2", test_request_headers)154 parsed_sample = urlparse(SAMPLE_SIGNED)
155
156 self.assertEqual(parsed_signed.scheme, parsed_sample.scheme)
157
158 @defer.inlineCallbacks
159 def test_correct_query(self):
160 """Test the invariant parts of the signed URL."""
161 signed = yield self.wc.build_signed_iri(SAMPLE_TARGET,
162 params={'next': u'/blah'})
163 parsed_signed = urlparse(signed)
164 parsed_sample = urlparse(SAMPLE_SIGNED)
165 signed_query = parse_qs(parsed_signed.query)
166 sample_query = parse_qs(parsed_sample.query)
167
168 for key in ('next', 'oauth_consumer_key', 'oauth_version',
169 'oauth_signature_method', 'oauth_token'):
170 self.assertEqual(map(unicode, signed_query[key]),
171 sample_query[key])
172
173 @defer.inlineCallbacks
174 def test_url_with_query(self):
175 """Test that we are using the right scheme."""
176 signed = yield self.wc.build_signed_iri(SAMPLE_TARGET,
177 params={'next': u'/blah?foo=bar'})
178 parsed_signed = urlparse(signed)
179 signed_query = parse_qs(parsed_signed.query)
180
181 self.assertEqual(signed_query['next'], [u'/blah?foo=bar'])
182
183 @defer.inlineCallbacks
184 def test_uses_timestamper(self):
185 """Test that the signed url is using the serverrelative timestamp."""
186 signed = yield self.wc.build_signed_iri(u'/blah?foo=bar')
187 parsed_signed = urlparse(signed)
188 signed_query = parse_qs(parsed_signed.query)
189
190 self.assertTrue(signed_query['oauth_timestamp'] is not None)
168191
=== removed directory 'ubuntuone/controlpanel/web_client'
=== renamed file 'ubuntuone/controlpanel/web_client/__init__.py' => 'ubuntuone/controlpanel/web_client.py'
--- ubuntuone/controlpanel/web_client/__init__.py 2011-10-07 14:36:14 +0000
+++ ubuntuone/controlpanel/web_client.py 2012-02-07 14:03:19 +0000
@@ -1,9 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2
3# Authors: Natalia B Bidart <natalia.bidart@canonical.com>
4# Alejandro J. Cura <alecu@canonical.com>
5#2#
6# Copyright 2011 Canonical Ltd.3# Copyright 2011-2012 Canonical Ltd.
7#4#
8# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -19,60 +16,55 @@
1916
20"""The web client."""17"""The web client."""
2118
22from oauth import oauth19import simplejson
2320
2421from twisted.internet import defer
25# pylint: disable=W0401, W061422# need to export the exceptions to avoid API breakage
2623# pylint: disable=W0611
2724from ubuntu_sso.utils.webclient import (
28class WebClientError(Exception):25 UnauthorizedError,
29 """An http error happened while calling the webservice."""26 WebClientError,
3027 webclient_factory,
3128)
32class UnauthorizedError(WebClientError):29# pylint: enable=W0611
33 """The request ended with bad_request, unauthorized or forbidden."""30
3431from ubuntuone.controlpanel import WEBSERVICE_BASE_URL
3532from ubuntuone.controlpanel.logger import setup_logging
36def build_oauth_headers(method, url, credentials, timestamp):33
37 """Build an oauth request given some credentials."""34
38 consumer = oauth.OAuthConsumer(credentials["consumer_key"],35logger = setup_logging('webclient')
39 credentials["consumer_secret"])36
40 token = oauth.OAuthToken(credentials["token"],37
41 credentials["token_secret"])38class WebClient(object):
42 parameters = {}39 """A client for the u1 webservice."""
43 if timestamp:40
44 parameters["oauth_timestamp"] = timestamp41 def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL):
45 request = oauth.OAuthRequest.from_consumer_and_token(42 """Initialize the webclient."""
46 http_url=url,43 self.base_url = base_url
47 http_method=method,44 self.get_credentials = get_credentials
48 parameters=parameters,45 self.wc = webclient_factory()
49 oauth_consumer=consumer,46 logger.debug("WebClient created: base_url is %r, inner client is %r.",
50 token=token)47 self.base_url, self.wc)
51 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()48
52 request.sign_request(sig_method, consumer, token)49 @defer.inlineCallbacks
53 return request.to_header()50 def call_api(self, api_name, extra_headers=None):
5451 """Call the webservice."""
5552 # this may log device ID's, but only for removals, which is OK
56def add_oauth_headers(append_method, method, url, credentials, timestamp=None):53 logger.debug("calling api: %r", api_name)
57 """Sign a libsoup message with oauth headers."""54 iri = self.base_url + api_name
58 headers = build_oauth_headers(method, url, credentials, timestamp)55 credentials = yield self.get_credentials()
59 for key, value in headers.items():56 response = yield self.wc.request(iri, extra_headers=extra_headers,
60 append_method(key, value)57 oauth_credentials=credentials)
6158 result = simplejson.loads(response.content)
6259 defer.returnValue(result)
63def web_client_module():60
64 """Choose the module of the web client."""61 @defer.inlineCallbacks
65 # the reactor can only be imported after Qt is initialized62 def build_signed_iri(self, iri, params=None):
66 # pylint: disable=W040463 """Build an OAuth iri."""
67 from twisted.internet import reactor64 credentials = yield self.get_credentials()
68 if getattr(reactor, "qApp", None):65 result = yield self.wc.build_signed_iri(iri, credentials, params)
69 from ubuntuone.controlpanel.web_client import txwebclient as web_module66 defer.returnValue(result)
70 else:67
71 from ubuntuone.controlpanel.web_client import libsoup as web_module68 def shutdown(self):
72 return web_module69 """Shutdown and cleanup."""
7370 return self.wc.shutdown()
74
75def web_client_factory(*args, **kwargs):
76 """Choose the type of the web client dynamically."""
77 web_module = web_client_module()
78 return web_module.WebClient(*args, **kwargs)
7971
=== removed file 'ubuntuone/controlpanel/web_client/libsoup.py'
--- ubuntuone/controlpanel/web_client/libsoup.py 2011-10-06 19:56:38 +0000
+++ ubuntuone/controlpanel/web_client/libsoup.py 1970-01-01 00:00:00 +0000
@@ -1,133 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4# Authors: Natalia B. Bidart <nataliabidart@canonical.com>
5#
6# Copyright 2010 Canonical Ltd.
7#
8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published
10# by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranties of
14# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15# PURPOSE. See the GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program. If not, see <http://www.gnu.org/licenses/>.
19
20"""The control panel backend webservice client."""
21
22import simplejson
23
24# pylint: disable=E0611
25from gi.repository import Soup, SoupGNOME
26from twisted.internet import defer
27
28from ubuntuone.controlpanel import WEBSERVICE_BASE_URL
29from ubuntuone.controlpanel.web_client import (add_oauth_headers,
30 WebClientError,
31 UnauthorizedError)
32from ubuntuone.controlpanel.logger import setup_logging
33from ubuntuone.storageprotocol.utils import BaseTimestampChecker
34
35logger = setup_logging('webclient')
36
37# full list of status codes
38# http://library.gnome.org/devel/libsoup/stable/libsoup-2.4-soup-status.html
39
40
41class LibsoupTimestampChecker(BaseTimestampChecker):
42 """Specialized TimestampChecker using libsoup."""
43
44 def __init__(self):
45 """Initialize this instance."""
46 super(LibsoupTimestampChecker, self).__init__()
47 self.session = Soup.SessionAsync()
48 self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
49
50 def _handler(self, session, msg, d):
51 """Handle the result of an http message."""
52 logger.debug("got http response %d for uri %r",
53 msg.status_code, msg.get_uri().to_string(False))
54 if msg.status_code == 200:
55 date = msg.response_headers.get("Date")
56 d.callback(date)
57 else:
58 e = WebClientError(msg.status_code, "")
59 d.errback(e)
60
61 def get_server_date_header(self, server_url):
62 """Get the server date using twisted webclient."""
63 method = "HEAD"
64 msg = Soup.Message.new(method, server_url)
65 msg.request_headers.append("Cache-Control", "no-cache")
66 d = defer.Deferred()
67 self.session.queue_message(msg, self._handler, d)
68 return d
69
70 def shutdown(self):
71 """End the soup session for this webclient."""
72 self.session.abort()
73
74
75# pylint: disable=C0103
76timestamp_checker = LibsoupTimestampChecker()
77
78
79class WebClient(object):
80 """A client for the u1 webservice."""
81
82 def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL):
83 """Initialize the webclient."""
84 self.base_url = base_url
85 self.session = Soup.SessionAsync()
86 self.session.add_feature_by_type(SoupGNOME.ProxyResolverGNOME)
87 self.get_credentials = get_credentials
88
89 def _handler(self, session, msg, d):
90 """Handle the result of an http message."""
91 logger.debug("got http response %d for uri %r",
92 msg.status_code, msg.get_uri().to_string(False))
93 data = msg.response_body.data
94 if msg.status_code == 200:
95 result = simplejson.loads(data)
96 d.callback(result)
97 else:
98 if msg.status_code in (401,):
99 e = UnauthorizedError(msg.status_code, data)
100 else:
101 e = WebClientError(msg.status_code, data)
102 d.errback(e)
103
104 def _call_api_inner(self, credentials, api_name, timestamp):
105 """Call the webservice with credentials and timestamp."""
106 url = (self.base_url + api_name).encode('utf-8')
107 method = "GET"
108 logger.debug("getting url: %s, %s", method, url)
109 msg = Soup.Message.new(method, url)
110 add_oauth_headers(msg.request_headers.append, method, url,
111 credentials, timestamp)
112 d = defer.Deferred()
113 self.session.queue_message(msg, self._handler, d)
114 return d
115
116 @defer.inlineCallbacks
117 def _call_api_add_timestamp(self, credentials, api_name):
118 """Add the timestamp to the api call."""
119 timestamp = yield timestamp_checker.get_faithful_time()
120 result = yield self._call_api_inner(credentials, api_name, timestamp)
121 defer.returnValue(result)
122
123 def call_api(self, api_name):
124 """Call the webservice."""
125 # this may log device ID's, but only for removals, which is OK
126 logger.debug("calling api: %s", api_name)
127 d = self.get_credentials()
128 d.addCallback(self._call_api_add_timestamp, api_name)
129 return d
130
131 def shutdown(self):
132 """End the soup session for this webclient."""
133 self.session.abort()
1340
=== removed directory 'ubuntuone/controlpanel/web_client/tests'
=== removed file 'ubuntuone/controlpanel/web_client/tests/__init__.py'
--- ubuntuone/controlpanel/web_client/tests/__init__.py 2011-09-08 23:52:27 +0000
+++ ubuntuone/controlpanel/web_client/tests/__init__.py 1970-01-01 00:00:00 +0000
@@ -1,19 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Unit tests for the control panel backend webservice clients."""
200
=== removed file 'ubuntuone/controlpanel/web_client/tests/test_libsoup.py'
--- ubuntuone/controlpanel/web_client/tests/test_libsoup.py 2011-10-06 22:27:57 +0000
+++ ubuntuone/controlpanel/web_client/tests/test_libsoup.py 1970-01-01 00:00:00 +0000
@@ -1,106 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Integration tests for the libsoup based webservice client."""
20
21import time
22
23from twisted.application import internet, service
24from twisted.internet import defer
25from twisted.web import http, resource, server
26
27from ubuntuone.controlpanel.tests import TestCase
28from ubuntuone.controlpanel.web_client.libsoup import LibsoupTimestampChecker
29
30
31class MockResource(resource.Resource):
32 """A mock resource."""
33
34 isLeaf = True
35
36 def __init__(self):
37 """Initialize this mock instance."""
38 resource.Resource.__init__(self)
39 self.request_headers = []
40
41 # pylint: disable=C0103
42 def render_GET(self, request):
43 """Render some content."""
44 self.request_headers.append(request.requestHeaders)
45 return "hello!"
46
47
48class MockWebService(object):
49 """A mock webservice for testing"""
50
51 def __init__(self):
52 """Start up this instance."""
53 self.root = MockResource()
54 site = server.Site(self.root)
55 application = service.Application('web')
56 self.service_collection = service.IServiceCollection(application)
57 #pylint: disable=E1101
58 self.tcpserver = internet.TCPServer(0, site)
59 self.tcpserver.setServiceParent(self.service_collection)
60 self.service_collection.startService()
61
62 def get_url(self):
63 """Build the url for this mock server."""
64 #pylint: disable=W0212
65 port_num = self.tcpserver._port.getHost().port
66 return "http://localhost:%d/" % port_num
67
68 def stop(self):
69 """Shut down the service."""
70 #pylint: disable=E1101
71 self.service_collection.stopService()
72
73
74class LibsoupTimestampCheckerTestCase(TestCase):
75 """Tests for LibsoupTimestampChecker."""
76
77 timeout = 3
78
79 @defer.inlineCallbacks
80 def setUp(self):
81 yield super(LibsoupTimestampCheckerTestCase, self).setUp()
82 self.ws = MockWebService()
83 self.addCleanup(self.ws.stop)
84
85 @defer.inlineCallbacks
86 def test_gets_server_date(self):
87 """The server date is gotten right."""
88 fake_time = 1
89 self.patch(time, "time", lambda: fake_time)
90 checker = LibsoupTimestampChecker()
91 self.addCleanup(checker.shutdown)
92 d = checker.get_server_date_header(self.ws.get_url())
93 result = yield d
94 result_time = http.stringToDatetime(result)
95 self.assertEqual(result_time, fake_time)
96
97 @defer.inlineCallbacks
98 def test_server_date_sends_nocache_headers(self):
99 """Getting the server date sends the no-cache headers."""
100 checker = LibsoupTimestampChecker()
101 self.addCleanup(checker.shutdown)
102 yield checker.get_server_date_header(self.ws.get_url())
103 self.assertEqual(len(self.ws.root.request_headers), 1)
104 headers = self.ws.root.request_headers[0]
105 result = headers.getRawHeaders("Cache-Control")
106 self.assertEqual(result, ["no-cache"])
1070
=== removed file 'ubuntuone/controlpanel/web_client/tests/test_txwebclient.py'
--- ubuntuone/controlpanel/web_client/tests/test_txwebclient.py 2011-11-21 13:32:44 +0000
+++ ubuntuone/controlpanel/web_client/tests/test_txwebclient.py 1970-01-01 00:00:00 +0000
@@ -1,177 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Unit tests for the control panel backend twisted webservice client."""
20
21import time
22
23from twisted.application import internet, service
24from twisted.internet import defer, reactor
25from twisted.internet.defer import inlineCallbacks
26from twisted.web import server, resource
27from ubuntuone.devtools.testcases import skipIfOS
28
29from ubuntuone.controlpanel.web_client import txwebclient
30from ubuntuone.controlpanel.tests import TestCase
31
32
33SAMPLE_KEY = "result"
34SAMPLE_VALUE = "sample result"
35SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)
36SAMPLE_CREDENTIALS = dict(
37 consumer_key="consumer key",
38 consumer_secret="consumer secret",
39 token="the real token",
40 token_secret="the token secret",
41)
42
43
44def sample_get_credentials():
45 """Will return the sample credentials right now."""
46 return defer.succeed(SAMPLE_CREDENTIALS)
47
48
49class MockResource(resource.Resource):
50 """A simple web resource."""
51 isLeaf = True
52 contents = ""
53
54 # pylint: disable=C0103
55 # t.w.resource methods have freeform cased names
56
57 def getChild(self, name, request):
58 """Get a given child resource."""
59 if name == '':
60 return self
61 return resource.Resource.getChild(self, name, request)
62
63 def render_GET(self, request):
64 """Make a bit of html out of these resource's content."""
65 return self.contents
66
67
68class MockWebService(object):
69 """A mock webservice for testing"""
70
71 def __init__(self):
72 """Start up this instance."""
73 root = resource.Resource()
74 devices_resource = MockResource()
75 devices_resource.contents = SAMPLE_RESOURCE
76 root.putChild("devices", devices_resource)
77 root.putChild("throwerror", resource.NoResource())
78 unauthorized = resource.ErrorPage(resource.http.UNAUTHORIZED,
79 "Unauthrorized", "Unauthrorized")
80 root.putChild("unauthorized", unauthorized)
81
82 site = server.Site(root)
83 application = service.Application('web')
84 self.service_collection = service.IServiceCollection(application)
85 #pylint: disable=E1101
86 self.tcpserver = internet.TCPServer(0, site)
87 self.tcpserver.setServiceParent(self.service_collection)
88 self.service_collection.startService()
89
90 def get_url(self):
91 """Build the url for this mock server."""
92 #pylint: disable=W0212
93 port_num = self.tcpserver._port.getHost().port
94 return "http://localhost:%d/" % port_num
95
96 def stop(self):
97 """Shut it down."""
98 #pylint: disable=E1101
99 return self.service_collection.stopService()
100
101
102class FakeAsyncTimestamper(object):
103 """A fake timestamp."""
104
105 def __init__(self):
106 """Initialize this instance."""
107 self.called = False
108
109 def get_faithful_time(self):
110 """Return the server checked timestamp."""
111 self.called = True
112 return defer.succeed(time.time())
113
114
115class WebClientTestCase(TestCase):
116 """Test for the webservice client."""
117
118 timeout = 8
119
120 @defer.inlineCallbacks
121 def setUp(self):
122 yield super(WebClientTestCase, self).setUp()
123 self.ws = MockWebService()
124 test_base_url = self.ws.get_url()
125 self.wc = txwebclient.WebClient(sample_get_credentials, test_base_url)
126 self.timestamper = FakeAsyncTimestamper()
127 self.patch(txwebclient, "timestamp_checker", self.timestamper)
128 self.addCleanup(self.wc.shutdown)
129 self.addCleanup(self.ws.stop)
130
131 @inlineCallbacks
132 def test_get_url(self):
133 """A method is successfully called in the mock webservice."""
134 result = yield self.wc.call_api("devices")
135 self.assertIn(SAMPLE_KEY, result)
136 self.assertEqual(SAMPLE_VALUE, result[SAMPLE_KEY])
137
138 @inlineCallbacks
139 def test_call_api_uses_timestamp(self):
140 """Check that call_api uses the timestamp."""
141 yield self.wc.call_api("devices")
142 self.assertTrue(self.timestamper.called,
143 "The timestamper must be used.")
144
145 @inlineCallbacks
146 def test_get_url_error(self):
147 """The errback is called when there's some error."""
148 yield self.assertFailure(self.wc.call_api("throwerror"),
149 txwebclient.WebClientError)
150
151 @inlineCallbacks
152 def test_unauthorized(self):
153 """Detect when a request failed with UNAUTHORIZED."""
154 yield self.assertFailure(self.wc.call_api("unauthorized"),
155 txwebclient.UnauthorizedError)
156
157
158class WebClientShutdownTestCase(TestCase):
159 """The webclient behaviour during shutdown."""
160
161 @skipIfOS('win32', 'Failing on windows, see LP: #851158.')
162 @inlineCallbacks
163 def test_shutdown(self):
164 """The webclient behaves well during shutdown."""
165 self.patch(txwebclient, "timestamp_checker", FakeAsyncTimestamper())
166 d3 = defer.Deferred()
167 # pylint: disable=E1101
168 reactor.callLater(1, d3.callback, None)
169 ws = MockWebService()
170 test_base_url = ws.get_url()
171 wc = txwebclient.WebClient(sample_get_credentials, test_base_url)
172 d1 = wc.call_api("throwerror")
173 d2 = ws.stop()
174 wc.shutdown()
175 yield d2
176 yield defer.DeferredList([d1, d3], fireOnOneCallback=True,
177 fireOnOneErrback=True)
1780
=== removed file 'ubuntuone/controlpanel/web_client/txwebclient.py'
--- ubuntuone/controlpanel/web_client/txwebclient.py 2011-10-05 23:12:23 +0000
+++ ubuntuone/controlpanel/web_client/txwebclient.py 1970-01-01 00:00:00 +0000
@@ -1,108 +0,0 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""The control panel backend webservice client using twisted.web."""
20
21import simplejson
22
23from twisted.internet import defer, reactor
24from twisted.web import client, error, http
25
26from ubuntuone.controlpanel import WEBSERVICE_BASE_URL
27from ubuntuone.controlpanel.web_client import (add_oauth_headers,
28 WebClientError,
29 UnauthorizedError)
30
31from ubuntuone.controlpanel.logger import setup_logging
32from ubuntuone.storageprotocol.client import TwistedTimestampChecker
33
34logger = setup_logging('webclient')
35# pylint: disable=C0103
36timestamp_checker = TwistedTimestampChecker()
37
38
39class WebClient(object):
40 """A client for the u1 webservice."""
41
42 def __init__(self, get_credentials, base_url=WEBSERVICE_BASE_URL):
43 """Initialize the webclient."""
44 self.base_url = base_url
45 self.get_credentials = get_credentials
46 self.running = True
47 # pylint: disable=E1101
48 self.trigger_id = reactor.addSystemEventTrigger("before", "shutdown",
49 self.shutdown)
50
51 def _handle_response(self, result):
52 """Handle the response of the webservice call."""
53 return simplejson.loads(result)
54
55 def _handle_error(self, failure):
56 """Handle an error while calling the webservice."""
57 if failure.type == error.Error:
58 exception = failure.value
59 if exception.status == str(http.UNAUTHORIZED):
60 raise UnauthorizedError(exception.status, exception.response)
61 else:
62 raise WebClientError(exception.status, exception.response)
63 else:
64 raise WebClientError(-1, failure)
65
66 def _call_api_inner(self, credentials, api_name, timestamp):
67 """Call the webservice with credentials and timestamp."""
68 url = (self.base_url + api_name).encode('utf-8')
69 method = "GET"
70 logger.debug("getting url: %s, %s", method, url)
71 headers = {}
72 add_oauth_headers(headers.__setitem__, method, url, credentials,
73 timestamp)
74 d = client.getPage(url, headers=headers)
75 d.addCallback(self._handle_response)
76 d.addErrback(self._handle_error)
77 return d
78
79 @defer.inlineCallbacks
80 def _call_api_add_timestamp(self, credentials, api_name):
81 """Add the timestamp to the api call."""
82 timestamp = yield timestamp_checker.get_faithful_time()
83 result = yield self._call_api_inner(credentials, api_name, timestamp)
84 defer.returnValue(result)
85
86 def call_api(self, api_name):
87 """Call the webservice."""
88 # this may log device ID's, but only for removals, which is OK
89 logger.debug("calling api: %s", api_name)
90 d = self.get_credentials()
91 d.addErrback(self._handle_error)
92 d.addCallback(self._call_api_add_timestamp, api_name)
93 d2 = defer.Deferred()
94 d.addCallback(d2.callback)
95
96 def mask_errors_on_shutdown(failure):
97 """Do not fire the errbacks if we are shutting down."""
98 if self.running:
99 d2.errback(failure)
100
101 d.addErrback(mask_errors_on_shutdown)
102 return d2
103
104 def shutdown(self):
105 """End the pending webclient calls."""
106 self.running = False
107 # pylint: disable=E1101
108 reactor.removeSystemEventTrigger(self.trigger_id)

Subscribers

People subscribed via source and target branches