Merge lp:~nataliabidart/ubuntuone-control-panel/replication-to-the-backend into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 45
Merged at revision: 44
Proposed branch: lp:~nataliabidart/ubuntuone-control-panel/replication-to-the-backend
Merge into: lp:ubuntuone-control-panel
Diff against target: 1056 lines (+677/-172)
7 files modified
ubuntuone/controlpanel/backend.py (+41/-1)
ubuntuone/controlpanel/dbus_service.py (+41/-0)
ubuntuone/controlpanel/integrationtests/test_dbus_service.py (+55/-7)
ubuntuone/controlpanel/replication_client.py (+115/-0)
ubuntuone/controlpanel/tests/__init__.py (+149/-1)
ubuntuone/controlpanel/tests/test_backend.py (+116/-163)
ubuntuone/controlpanel/tests/test_replication_client.py (+160/-0)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-control-panel/replication-to-the-backend
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+45439@code.launchpad.net

Commit message

Desktopcouch replication service is managed in the backend and exposed in the DBus service (LP: #696782).

Description of the change

There is no easy way to test this except by running the backen with:

DEBUG=True PYTHONPATH=. ./bin/ubuntuone-control-panel-backend

and using d-feet to play with the replication methods. The merge proposal:

https://code.launchpad.net/~nataliabidart/ubuntuone-control-panel/start-dc-on-backend/+merge/45440

provides the GUI that accesses this service directly.

To post a comment you must log in.
45. By Natalia Bidart

Forgot some files!

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Minor point: I don't like exclude/replicate as complementary method names, I'd prefer exclude/include or maybe even include_in_replication/exclude_from_replication for clarity.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

I'm not sure I understand the 'dependency' field in the replication record. Bookmarks and contacts should be replicated, or at least replicatable, regardless of what is installed. Am I missing the point?

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Looks good otherwise.

review: Approve
Revision history for this message
Roberto Alsina (ralsina) wrote :

Approved. I agree with Eric and Natalia (on IRC) that the spec needs some improvements because what dbs you replicate shouldn't depend on installed packages, but this is according to the current spec.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntuone/controlpanel/backend.py'
2--- ubuntuone/controlpanel/backend.py 2010-12-23 18:20:56 +0000
3+++ ubuntuone/controlpanel/backend.py 2011-01-06 20:44:10 +0000
4@@ -22,6 +22,7 @@
5 from twisted.internet.defer import inlineCallbacks, returnValue
6
7 from ubuntuone.controlpanel import dbus_client
8+from ubuntuone.controlpanel import replication_client
9 from ubuntuone.controlpanel.logger import setup_logging, log_call
10 from ubuntuone.controlpanel.webclient import WebClient
11
12@@ -48,6 +49,9 @@
13 MSG_KEY = 'message'
14 STATUS_KEY = 'status'
15
16+BOOKMARKS_PKG = 'xul-ext-bindwood'
17+CONTACTS_PKG = 'evolution-couchdb'
18+
19
20 def bool_str(value):
21 """Return the string representation of a bool (dbus-compatible)."""
22@@ -300,7 +304,7 @@
23
24 """
25 if 'subscribed' in settings:
26- subscribed = settings['subscribed']
27+ subscribed = bool(settings['subscribed'])
28 if subscribed:
29 yield self.subscribe_volume(volume_id)
30 else:
31@@ -321,6 +325,42 @@
32 yield dbus_client.unsubscribe_folder(volume_id)
33
34 @log_call(logger.debug)
35+ @inlineCallbacks
36+ def replications_info(self):
37+ """Get the user replications info."""
38+ replications = yield replication_client.get_replications()
39+ exclusions = yield replication_client.get_exclusions()
40+
41+ result = []
42+ for rep in replications:
43+ dependency = ''
44+ if rep == replication_client.BOOKMARKS:
45+ dependency = BOOKMARKS_PKG
46+ elif rep == replication_client.CONTACTS:
47+ dependency = CONTACTS_PKG
48+
49+ repd = {
50+ "replication_id": rep,
51+ "name": rep, # this may change to be more user friendly
52+ "enabled": bool_str(rep not in exclusions),
53+ "dependency": dependency,
54+ }
55+ result.append(repd)
56+
57+ returnValue(result)
58+
59+ @log_call(logger.info)
60+ @inlineCallbacks
61+ def change_replication_settings(self, replication_id, settings):
62+ """Change the settings for the given replication."""
63+ if 'enabled' in settings:
64+ if bool(settings['enabled']):
65+ yield replication_client.replicate(replication_id)
66+ else:
67+ yield replication_client.exclude(replication_id)
68+ returnValue(replication_id)
69+
70+ @log_call(logger.debug)
71 def query_bookmark_extension(self):
72 """True if the bookmark extension has been installed."""
73 # still pending (LP: #673672)
74
75=== modified file 'ubuntuone/controlpanel/dbus_service.py'
76--- ubuntuone/controlpanel/dbus_service.py 2010-12-23 18:20:56 +0000
77+++ ubuntuone/controlpanel/dbus_service.py 2011-01-06 20:44:10 +0000
78@@ -339,6 +339,47 @@
79
80 @log_call(logger.debug)
81 @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
82+ def replications_info(self):
83+ """Return the replications info."""
84+ d = self.backend.replications_info()
85+ d.addCallback(self.ReplicationsInfoReady)
86+ d.addErrback(transform_failure(self.ReplicationsInfoError))
87+
88+ @log_call(logger.debug)
89+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}")
90+ def ReplicationsInfoReady(self, info):
91+ """The replications info is ready."""
92+
93+ @log_call(logger.error)
94+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}")
95+ def ReplicationsInfoError(self, error):
96+ """Problem getting the replications info."""
97+
98+ #---
99+
100+ @log_call(logger.info)
101+ @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="sa{ss}")
102+ def change_replication_settings(self, replication_id, settings):
103+ """Configure a given replication."""
104+ d = self.backend.change_replication_settings(replication_id, settings)
105+ d.addCallback(self.ReplicationSettingsChanged)
106+ d.addErrback(transform_failure(self.ReplicationSettingsChangeError),
107+ replication_id)
108+
109+ @log_call(logger.info)
110+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s")
111+ def ReplicationSettingsChanged(self, replication_id):
112+ """The settings for the replication were changed."""
113+
114+ @log_call(logger.error)
115+ @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="sa{ss}")
116+ def ReplicationSettingsChangeError(self, replication_id, error):
117+ """Problem changing settings for the replication."""
118+
119+ #---
120+
121+ @log_call(logger.debug)
122+ @method(dbus_interface=DBUS_PREFERENCES_IFACE, in_signature="")
123 def query_bookmark_extension(self):
124 """Check if the extension to sync bookmarks is installed."""
125 d = self.backend.query_bookmark_extension()
126
127=== modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_service.py'
128--- ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2010-12-23 18:20:56 +0000
129+++ ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2011-01-06 20:44:10 +0000
130@@ -84,6 +84,11 @@
131 },
132 ]
133
134+SAMPLE_REPLICATIONS_INFO = [
135+ {'replication_id': 'yadda', 'wait for it': 'awesome'},
136+ {'replication_id': 'yoda', 'something else': 'awesome'},
137+]
138+
139
140 class DBusServiceMainTestCase(mocker.MockerTestCase):
141 """Tests for the main function."""
142@@ -166,6 +171,19 @@
143 """Configure a given volume."""
144 return self._process(volume_id)
145
146+ def replications_info(self):
147+ """Start the replication exclusion service if needed.
148+
149+ Return the replication info, which is a dictionary of (replication
150+ name, enabled).
151+
152+ """
153+ return self._process(SAMPLE_REPLICATIONS_INFO)
154+
155+ def change_replication_settings(self, replication_id, settings):
156+ """Configure a given replication."""
157+ return self._process(replication_id)
158+
159 def query_bookmark_extension(self):
160 """True if the bookmark extension has been installed."""
161 return self._process(False)
162@@ -258,13 +276,13 @@
163 self.assertEqual(expected, result)
164
165
166-class OperationsTestCase(TestCase):
167- """Test for the DBus service operations."""
168+class BaseTestCase(TestCase):
169+ """Base test case for the DBus service."""
170
171 timeout = 3
172
173 def setUp(self):
174- super(OperationsTestCase, self).setUp()
175+ super(BaseTestCase, self).setUp()
176 dbus_service.init_mainloop()
177 be = dbus_service.publish_backend(MockBackend())
178 self.addCleanup(be.remove_from_connection)
179@@ -279,7 +297,7 @@
180 def tearDown(self):
181 self.backend = None
182 self.deferred = None
183- super(OperationsTestCase, self).tearDown()
184+ super(BaseTestCase, self).tearDown()
185
186 def got_error(self, *a):
187 """Some error happened in the DBus call."""
188@@ -322,6 +340,10 @@
189
190 return self.deferred
191
192+
193+class OperationsTestCase(BaseTestCase):
194+ """Test for the DBus service operations."""
195+
196 def test_account_info_returned(self):
197 """The account info is successfully returned."""
198
199@@ -416,9 +438,9 @@
200 def test_volumes_info(self):
201 """The volumes info is reported."""
202
203- def got_signal(volumes_dict):
204+ def got_signal(volumes):
205 """The correct info was received."""
206- self.assertEqual(volumes_dict, SAMPLE_VOLUMES_INFO)
207+ self.assertEqual(volumes, SAMPLE_VOLUMES_INFO)
208 self.deferred.callback("success")
209
210 args = ("VolumesInfoReady", "VolumesInfoError", got_signal,
211@@ -439,6 +461,32 @@
212 expected_volume_id, {'subscribed': ''})
213 return self.assert_correct_method_call(*args)
214
215+ def test_replications_info(self):
216+ """The replications info is reported."""
217+
218+ def got_signal(replications):
219+ """The correct info was received."""
220+ self.assertEqual(replications, SAMPLE_REPLICATIONS_INFO)
221+ self.deferred.callback("success")
222+
223+ args = ("ReplicationsInfoReady", "ReplicationsInfoError", got_signal,
224+ self.backend.replications_info)
225+ return self.assert_correct_method_call(*args)
226+
227+ def test_change_replication_settings(self):
228+ """The replication settings are successfully changed."""
229+ expected_replication_id = SAMPLE_REPLICATIONS_INFO[0]['replication_id']
230+
231+ def got_signal(replication_id):
232+ """The correct replication was changed."""
233+ self.assertEqual(replication_id, expected_replication_id)
234+ self.deferred.callback("success")
235+
236+ args = ("ReplicationSettingsChanged", "ReplicationSettingsChangeError",
237+ got_signal, self.backend.change_replication_settings,
238+ expected_replication_id, {'enabled': ''})
239+ return self.assert_correct_method_call(*args)
240+
241 def test_query_bookmarks_extension(self):
242 """The bookmarks extension is queried."""
243
244@@ -495,7 +543,7 @@
245 error_sig, success_sig, got_error_signal, method, *args)
246
247
248-class FileSyncTestCase(OperationsTestCase):
249+class FileSyncTestCase(BaseTestCase):
250 """Test for the DBus service when requesting file sync status."""
251
252 def assert_correct_status_signal(self, status, sync_signal,
253
254=== added file 'ubuntuone/controlpanel/replication_client.py'
255--- ubuntuone/controlpanel/replication_client.py 1970-01-01 00:00:00 +0000
256+++ ubuntuone/controlpanel/replication_client.py 2011-01-06 20:44:10 +0000
257@@ -0,0 +1,115 @@
258+# -*- coding: utf-8 -*-
259+
260+# Authors: Natalia B. Bidart <nataliabidart@canonical.com>
261+#
262+# Copyright 2010 Canonical Ltd.
263+#
264+# This program is free software: you can redistribute it and/or modify it
265+# under the terms of the GNU General Public License version 3, as published
266+# by the Free Software Foundation.
267+#
268+# This program is distributed in the hope that it will be useful, but
269+# WITHOUT ANY WARRANTY; without even the implied warranties of
270+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
271+# PURPOSE. See the GNU General Public License for more details.
272+#
273+# You should have received a copy of the GNU General Public License along
274+# with this program. If not, see <http://www.gnu.org/licenses/>.
275+
276+"""Client to use replication services."""
277+
278+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
279+
280+from ubuntuone.controlpanel.logger import setup_logging
281+
282+
283+logger = setup_logging('replication_client')
284+
285+BOOKMARKS = 'bookmarks'
286+CONTACTS = 'contacts'
287+# we should get this list from somewhere else
288+REPLICATIONS = set([BOOKMARKS, CONTACTS])
289+
290+
291+class ReplicationError(Exception):
292+ """A replication error."""
293+
294+
295+class NoPairingRecord(ReplicationError):
296+ """There is no pairing record."""
297+
298+
299+class InvalidIdError(ReplicationError):
300+ """The replication id is not valid."""
301+
302+
303+class NotExcludedError(ReplicationError):
304+ """The replication can not be replicated since is not excluded."""
305+
306+
307+class AlreadyExcludedError(ReplicationError):
308+ """The replication can not be excluded since is already excluded."""
309+
310+
311+def get_replication_proxy(replication_module=None):
312+ """Return a proxy to the replication client."""
313+ d = Deferred()
314+ if replication_module is None:
315+ # delay import in case DC is not installed at module import time
316+ # Unable to import 'desktopcouch.application.replication_services'
317+ # pylint: disable=F0401
318+ from desktopcouch.application.replication_services \
319+ import ubuntuone as replication_module
320+ try:
321+ result = replication_module.ReplicationExclusion()
322+ except ValueError:
323+ d.errback(NoPairingRecord())
324+ else:
325+ d.callback(result)
326+
327+ return d
328+
329+
330+@inlineCallbacks
331+def get_replications():
332+ """Retrieve the list of replications."""
333+ yield get_replication_proxy()
334+ returnValue(REPLICATIONS)
335+
336+
337+@inlineCallbacks
338+def get_exclusions():
339+ """Retrieve the list of exclusions."""
340+ proxy = yield get_replication_proxy()
341+ result = proxy.all_exclusions()
342+ returnValue(result)
343+
344+
345+@inlineCallbacks
346+def replicate(replication_id):
347+ """Remove replication_id from the exclusions list."""
348+ replications = yield get_replications()
349+ if replication_id not in replications:
350+ raise InvalidIdError(replication_id)
351+
352+ exclusions = yield get_exclusions()
353+ if replication_id not in exclusions:
354+ raise NotExcludedError(replication_id)
355+
356+ proxy = yield get_replication_proxy()
357+ yield proxy.replicate(replication_id)
358+
359+
360+@inlineCallbacks
361+def exclude(replication_id):
362+ """Add replication_id to the exclusions list."""
363+ replications = yield get_replications()
364+ if replication_id not in replications:
365+ raise InvalidIdError(replication_id)
366+
367+ exclusions = yield get_exclusions()
368+ if replication_id in exclusions:
369+ raise AlreadyExcludedError(replication_id)
370+
371+ proxy = yield get_replication_proxy()
372+ yield proxy.exclude(replication_id)
373
374=== modified file 'ubuntuone/controlpanel/tests/__init__.py'
375--- ubuntuone/controlpanel/tests/__init__.py 2010-12-20 16:11:13 +0000
376+++ ubuntuone/controlpanel/tests/__init__.py 2011-01-06 20:44:10 +0000
377@@ -24,9 +24,157 @@
378 TOKEN = {u'consumer_key': u'xQ7xDAz',
379 u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy',
380 u'token_name': u'test',
381- u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo',
382+ u'token': u'ABCDEF01234-localtoken',
383 u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'}
384
385+SAMPLE_ACCOUNT_JSON = """
386+{
387+ "username": "andrewpz",
388+ "openid": "https://login.launchpad.net/+id/abcdefg",
389+ "first_name": "Andrew P.",
390+ "last_name": "Zoilo",
391+ "couchdb": {
392+ "host": "https://couchdb.one.ubuntu.com",
393+ "root": "https://couchdb.one.ubuntu.com/u/abc/def/12345",
394+ "dbpath": "u/abc/def/12345"
395+ },
396+ "couchdb_root": "https://couchdb.one.ubuntu.com/u/abc/def/12345",
397+ "email": "andrewpz@protocultura.net",%s
398+ "nickname": "Andrew P. Zoilo",
399+ "id": 12345,
400+ "subscription": {
401+ "upgrade_available": false,
402+ "description": "Paid Plan, 50 GB of storage",
403+ "trial": false,
404+ "started": "2010-03-24T18:38:38Z",
405+ "is_paid": true,
406+ "expires": null,
407+ "qty": 1,
408+ "price": 0.0,
409+ "currency": null,
410+ "id": 654321,
411+ "name": "50 GB"
412+ }
413+}
414+"""
415+
416+CURRENT_PLAN = "Ubuntu One Basic (2 GB) + 1 x 20-Pack with 20 GB (monthly)"
417+SAMPLE_CURRENT_PLAN = '\n "current_plan": "%s",' % CURRENT_PLAN
418+
419+SAMPLE_ACCOUNT_NO_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % ''
420+SAMPLE_ACCOUNT_WITH_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % SAMPLE_CURRENT_PLAN
421+
422+
423+SAMPLE_QUOTA_JSON = """
424+{
425+ "total": 53687091200,
426+ "used": 2350345156
427+}
428+"""
429+
430+EXPECTED_ACCOUNT_INFO = {
431+ "quota_used": "2350345156",
432+ "quota_total": "53687091200",
433+ "type": "Paid Plan, 50 GB of storage",
434+ "name": "Andrew P. Zoilo",
435+ "email": "andrewpz@protocultura.net",
436+}
437+
438+EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN = {
439+ "quota_used": "2350345156",
440+ "quota_total": "53687091200",
441+ "type": CURRENT_PLAN,
442+ "name": "Andrew P. Zoilo",
443+ "email": "andrewpz@protocultura.net",
444+}
445+
446+SAMPLE_DEVICES_JSON = """
447+[
448+ {
449+ "token": "ABCDEF01234token",
450+ "description": "Ubuntu One @ darkstar",
451+ "kind": "Computer"
452+ },
453+ {
454+ "token": "ABCDEF01234-localtoken",
455+ "description": "Ubuntu One @ localhost",
456+ "kind": "Computer"
457+ },
458+ {
459+ "kind": "Phone",
460+ "description": "Nokia E65",
461+ "id": 1000
462+ }
463+]
464+"""
465+
466+EXPECTED_DEVICES_INFO = [
467+ {
468+ "device_id": "ComputerABCDEF01234token",
469+ "name": "Ubuntu One @ darkstar",
470+ "type": "Computer",
471+ "is_local": '',
472+ "configurable": '',
473+ },
474+ {
475+ 'is_local': 'True',
476+ 'configurable': 'True',
477+ 'device_id': 'ComputerABCDEF01234-localtoken',
478+ 'limit_bandwidth': '',
479+ 'max_download_speed': '-1',
480+ 'max_upload_speed': '-1',
481+ 'name': 'Ubuntu One @ localhost',
482+ 'type': 'Computer'
483+ },
484+ {
485+ "device_id": "Phone1000",
486+ "name": "Nokia E65",
487+ "type": "Phone",
488+ "configurable": '',
489+ "is_local": '',
490+ },
491+]
492+
493+SAMPLE_FOLDERS = [
494+ {u'generation': u'2', u'node_id': u'341da068-81d8-437a-8f75-5bb9d86455ba',
495+ u'path': u'/home/tester/Public', u'subscribed': u'True',
496+ u'suggested_path': u'~/Public',
497+ u'type': u'UDF', u'volume_id': u'9ea892f8-15fa-4201-bdbf-8de99fa5f588'},
498+ {u'generation': u'', u'node_id': u'11fbc86c-0d7a-49f5-ae83-8402caf66c6a',
499+ u'path': u'/home/tester/Documents', u'subscribed': u'',
500+ u'suggested_path': u'~/Documents',
501+ u'type': u'UDF', u'volume_id': u'2db262f5-a151-4c19-969c-bb5ced753c61'},
502+ {u'generation': u'24', u'node_id': u'9ee0e130-a7c7-4d76-a5e3-5df506221b48',
503+ u'path': u'/home/tester/Pictures/Photos', u'subscribed': u'True',
504+ u'suggested_path': u'~/Pictures/Photos',
505+ u'type': u'UDF', u'volume_id': u'1deb2874-3d28-46ae-9999-d5f48de9f460'},
506+]
507+
508+SAMPLE_SHARES = [
509+ {u'accepted': u'True', u'access_level': u'View',
510+ u'free_bytes': u'39892622746', u'generation': u'2704',
511+ u'name': u're', u'node_id': u'c483f419-ed28-490a-825d-a8c074e2d795',
512+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
513+ u'path': u'/home/tester/.local/share/ubuntuone/shares/re from Other User',
514+ u'type': u'Share', u'volume_id': u'4a1b263b-a2b3-4f66-9e66-4cd18050810d'},
515+ {u'accepted': u'True', u'access_level': u'Modify',
516+ u'free_bytes': u'39892622746', u'generation': u'2704',
517+ u'name': u'do', u'node_id': u'84544ea4-aefe-4f91-9bb9-ed7b0a805baf',
518+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
519+ u'path': u'/home/tester/.local/share/ubuntuone/shares/do from Other User',
520+ u'type': u'Share', u'volume_id': u'7d130dfe-98b2-4bd5-8708-9eeba9838ac0'},
521+]
522+
523+SAMPLE_SHARED = [
524+ {u'accepted': u'True', u'access_level': u'View',
525+ u'free_bytes': u'', u'generation': u'',
526+ u'name': u'bar', u'node_id': u'31e47530-9448-4f03-b4dc-4154fdf35225',
527+ u'other_username': u'otheruser', u'other_visible_name': u'Other User',
528+ u'path': u'/home/tester/Ubuntu One/bar',
529+ u'type': u'Shared',
530+ u'volume_id': u'79584900-517f-4dff-b2f3-20e8c1e79365'},
531+]
532+
533
534 class TestCase(BaseTestCase):
535 """Basics for testing."""
536
537=== modified file 'ubuntuone/controlpanel/tests/test_backend.py'
538--- ubuntuone/controlpanel/tests/test_backend.py 2010-12-23 18:20:56 +0000
539+++ ubuntuone/controlpanel/tests/test_backend.py 2011-01-06 20:44:10 +0000
540@@ -25,9 +25,9 @@
541 from twisted.internet.defer import inlineCallbacks
542 from ubuntuone.devtools.handlers import MementoHandler
543
544-from ubuntuone.controlpanel import backend
545-from ubuntuone.controlpanel.backend import (ACCOUNT_API,
546- DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,
547+from ubuntuone.controlpanel import backend, replication_client
548+from ubuntuone.controlpanel.backend import (bool_str,
549+ ACCOUNT_API, DEVICES_API, DEVICE_REMOVE_API, QUOTA_API,
550 FILE_SYNC_DISABLED,
551 FILE_SYNC_DISCONNECTED,
552 FILE_SYNC_ERROR,
553@@ -37,160 +37,21 @@
554 FILE_SYNC_UNKNOWN,
555 MSG_KEY, STATUS_KEY,
556 )
557-
558-from ubuntuone.controlpanel.tests import TestCase
559+from ubuntuone.controlpanel.tests import (TestCase,
560+ EXPECTED_ACCOUNT_INFO,
561+ EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN,
562+ EXPECTED_DEVICES_INFO,
563+ SAMPLE_ACCOUNT_NO_CURRENT_PLAN,
564+ SAMPLE_ACCOUNT_WITH_CURRENT_PLAN,
565+ SAMPLE_DEVICES_JSON,
566+ SAMPLE_FOLDERS,
567+ SAMPLE_QUOTA_JSON,
568+ SAMPLE_SHARED,
569+ SAMPLE_SHARES,
570+ TOKEN,
571+)
572 from ubuntuone.controlpanel.webclient import WebClientError
573
574-SAMPLE_CREDENTIALS = {"token": "ABC1234DEF"}
575-
576-SAMPLE_ACCOUNT_JSON = """
577-{
578- "username": "andrewpz",
579- "openid": "https://login.launchpad.net/+id/abcdefg",
580- "first_name": "Andrew P.",
581- "last_name": "Zoilo",
582- "couchdb": {
583- "host": "https://couchdb.one.ubuntu.com",
584- "root": "https://couchdb.one.ubuntu.com/u/abc/def/12345",
585- "dbpath": "u/abc/def/12345"
586- },
587- "couchdb_root": "https://couchdb.one.ubuntu.com/u/abc/def/12345",
588- "email": "andrewpz@protocultura.net",%s
589- "nickname": "Andrew P. Zoilo",
590- "id": 12345,
591- "subscription": {
592- "upgrade_available": false,
593- "description": "Paid Plan, 50 GB of storage",
594- "trial": false,
595- "started": "2010-03-24T18:38:38Z",
596- "is_paid": true,
597- "expires": null,
598- "qty": 1,
599- "price": 0.0,
600- "currency": null,
601- "id": 654321,
602- "name": "50 GB"
603- }
604-}
605-"""
606-
607-CURRENT_PLAN = "Ubuntu One Basic (2 GB) + 1 x 20-Pack with 20 GB (monthly)"
608-SAMPLE_CURRENT_PLAN = '\n "current_plan": "%s",' % CURRENT_PLAN
609-
610-SAMPLE_ACCOUNT_NO_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % ''
611-SAMPLE_ACCOUNT_WITH_CURRENT_PLAN = SAMPLE_ACCOUNT_JSON % SAMPLE_CURRENT_PLAN
612-
613-
614-SAMPLE_QUOTA_JSON = """
615-{
616- "total": 53687091200,
617- "used": 2350345156
618-}
619-"""
620-
621-EXPECTED_ACCOUNT_INFO = {
622- "quota_used": "2350345156",
623- "quota_total": "53687091200",
624- "type": "Paid Plan, 50 GB of storage",
625- "name": "Andrew P. Zoilo",
626- "email": "andrewpz@protocultura.net",
627-}
628-
629-EXPECTED_ACCOUNT_INFO_WITH_CURRENT_PLAN = {
630- "quota_used": "2350345156",
631- "quota_total": "53687091200",
632- "type": CURRENT_PLAN,
633- "name": "Andrew P. Zoilo",
634- "email": "andrewpz@protocultura.net",
635-}
636-
637-SAMPLE_DEVICES_JSON = """
638-[
639- {
640- "token": "ABCDEF01234token",
641- "description": "Ubuntu One @ darkstar",
642- "kind": "Computer"
643- },
644- {
645- "token": "ABC1234DEF",
646- "description": "Ubuntu One @ localhost",
647- "kind": "Computer"
648- },
649- {
650- "kind": "Phone",
651- "description": "Nokia E65",
652- "id": 1000
653- }
654-]
655-"""
656-
657-EXPECTED_DEVICES_INFO = [
658- {
659- "device_id": "ComputerABCDEF01234token",
660- "name": "Ubuntu One @ darkstar",
661- "type": "Computer",
662- "is_local": '',
663- "configurable": '',
664- },
665- {
666- 'is_local': 'True',
667- 'configurable': 'True',
668- 'device_id': 'ComputerABC1234DEF',
669- 'limit_bandwidth': '',
670- 'max_download_speed': '-1',
671- 'max_upload_speed': '-1',
672- 'name': 'Ubuntu One @ localhost',
673- 'type': 'Computer'
674- },
675- {
676- "device_id": "Phone1000",
677- "name": "Nokia E65",
678- "type": "Phone",
679- "configurable": '',
680- "is_local": '',
681- },
682-]
683-
684-SAMPLE_FOLDERS = [
685- {u'generation': u'2', u'node_id': u'341da068-81d8-437a-8f75-5bb9d86455ba',
686- u'path': u'/home/tester/Public', u'subscribed': u'True',
687- u'suggested_path': u'~/Public',
688- u'type': u'UDF', u'volume_id': u'9ea892f8-15fa-4201-bdbf-8de99fa5f588'},
689- {u'generation': u'', u'node_id': u'11fbc86c-0d7a-49f5-ae83-8402caf66c6a',
690- u'path': u'/home/tester/Documents', u'subscribed': u'',
691- u'suggested_path': u'~/Documents',
692- u'type': u'UDF', u'volume_id': u'2db262f5-a151-4c19-969c-bb5ced753c61'},
693- {u'generation': u'24', u'node_id': u'9ee0e130-a7c7-4d76-a5e3-5df506221b48',
694- u'path': u'/home/tester/Pictures/Photos', u'subscribed': u'True',
695- u'suggested_path': u'~/Pictures/Photos',
696- u'type': u'UDF', u'volume_id': u'1deb2874-3d28-46ae-9999-d5f48de9f460'},
697-]
698-
699-SAMPLE_SHARES = [
700- {u'accepted': u'True', u'access_level': u'View',
701- u'free_bytes': u'39892622746', u'generation': u'2704',
702- u'name': u're', u'node_id': u'c483f419-ed28-490a-825d-a8c074e2d795',
703- u'other_username': u'otheruser', u'other_visible_name': u'Other User',
704- u'path': u'/home/tester/.local/share/ubuntuone/shares/re from Other User',
705- u'type': u'Share', u'volume_id': u'4a1b263b-a2b3-4f66-9e66-4cd18050810d'},
706- {u'accepted': u'True', u'access_level': u'Modify',
707- u'free_bytes': u'39892622746', u'generation': u'2704',
708- u'name': u'do', u'node_id': u'84544ea4-aefe-4f91-9bb9-ed7b0a805baf',
709- u'other_username': u'otheruser', u'other_visible_name': u'Other User',
710- u'path': u'/home/tester/.local/share/ubuntuone/shares/do from Other User',
711- u'type': u'Share', u'volume_id': u'7d130dfe-98b2-4bd5-8708-9eeba9838ac0'},
712-]
713-
714-SAMPLE_SHARED = [
715- {u'accepted': u'True', u'access_level': u'View',
716- u'free_bytes': u'', u'generation': u'',
717- u'name': u'bar', u'node_id': u'31e47530-9448-4f03-b4dc-4154fdf35225',
718- u'other_username': u'otheruser', u'other_visible_name': u'Other User',
719- u'path': u'/home/tester/Ubuntu One/bar',
720- u'type': u'Shared',
721- u'volume_id': u'79584900-517f-4dff-b2f3-20e8c1e79365'},
722-]
723-
724
725 class MockWebClient(object):
726 """A mock webclient."""
727@@ -213,7 +74,7 @@
728 class MockDBusClient(object):
729 """A mock dbus_client module."""
730
731- creds = SAMPLE_CREDENTIALS
732+ creds = TOKEN
733 throttling = False
734 limits = {"download": -1, "upload": -1}
735 file_sync = True
736@@ -292,6 +153,36 @@
737 return SAMPLE_SHARED
738
739
740+class MockReplicationClient(object):
741+ """A mock replication_client module."""
742+
743+ BOOKMARKS = 'awesome'
744+ CONTACTS = 'legendary'
745+
746+ replications = set([BOOKMARKS, CONTACTS, 'other'])
747+ exclusions = set([CONTACTS])
748+
749+ def get_replications(self):
750+ """Grab the list of replications in this machine."""
751+ return MockReplicationClient.replications
752+
753+ def get_exclusions(self):
754+ """Grab the list of exclusions in this machine."""
755+ return MockReplicationClient.exclusions
756+
757+ def replicate(self, replication_id):
758+ """Remove replication_id from the exclusions list."""
759+ if replication_id not in MockReplicationClient.replications:
760+ raise replication_client.ReplicationError(replication_id)
761+ MockReplicationClient.exclusions.remove(replication_id)
762+
763+ def exclude(self, replication_id):
764+ """Add replication_id to the exclusions list."""
765+ if replication_id not in MockReplicationClient.replications:
766+ raise replication_client.ReplicationError(replication_id)
767+ MockReplicationClient.exclusions.add(replication_id)
768+
769+
770 class BackendBasicTestCase(TestCase):
771 """Simple tests for the backend."""
772
773@@ -301,13 +192,14 @@
774 super(BackendBasicTestCase, self).setUp()
775 self.patch(backend, "WebClient", MockWebClient)
776 self.patch(backend, "dbus_client", MockDBusClient())
777- self.local_token = "Computer" + SAMPLE_CREDENTIALS["token"]
778+ self.patch(backend, "replication_client", MockReplicationClient())
779+ self.local_token = "Computer" + TOKEN["token"]
780 self.be = backend.ControlBackend()
781
782 self.memento = MementoHandler()
783 backend.logger.addHandler(self.memento)
784
785- MockDBusClient.creds = SAMPLE_CREDENTIALS
786+ MockDBusClient.creds = TOKEN
787
788 def test_backend_creation(self):
789 """The backend instance is successfully created."""
790@@ -317,7 +209,7 @@
791 def test_get_token(self):
792 """The get_token method returns the right token."""
793 token = yield self.be.get_token()
794- self.assertEqual(token, SAMPLE_CREDENTIALS["token"])
795+ self.assertEqual(token, TOKEN["token"])
796
797 @inlineCallbacks
798 def test_device_is_local(self):
799@@ -388,12 +280,12 @@
800 result = yield self.be.remove_device(device_id)
801 self.assertEqual(result, device_id)
802 # credentials were not cleared
803- self.assertEqual(MockDBusClient.creds, SAMPLE_CREDENTIALS)
804+ self.assertEqual(MockDBusClient.creds, TOKEN)
805
806 @inlineCallbacks
807 def test_remove_device_clear_credentials_if_local_device(self):
808 """The remove_device method clears the credentials if is local."""
809- apiurl = DEVICE_REMOVE_API % ('computer', SAMPLE_CREDENTIALS['token'])
810+ apiurl = DEVICE_REMOVE_API % ('computer', TOKEN['token'])
811 # pylint: disable=E1101
812 self.be.wc.results[apiurl] = SAMPLE_DEVICES_JSON
813 yield self.be.remove_device(self.local_token)
814@@ -487,10 +379,10 @@
815 """The volume settings can be changed."""
816 fid = '0123-4567'
817
818- yield self.be.change_volume_settings(fid, {'subscribed': True})
819+ yield self.be.change_volume_settings(fid, {'subscribed': 'True'})
820 self.assertEqual(MockDBusClient.subscribed_folders, [fid])
821
822- yield self.be.change_volume_settings(fid, {'subscribed': False})
823+ yield self.be.change_volume_settings(fid, {'subscribed': ''})
824 self.assertEqual(MockDBusClient.subscribed_folders, [])
825
826 @inlineCallbacks
827@@ -671,3 +563,64 @@
828
829 self.be.disable_files()
830 self.assertFalse(MockDBusClient.file_sync)
831+
832+
833+class BackendReplicationsTestCase(BackendBasicTestCase):
834+ """Replications tests for the backend."""
835+
836+ @inlineCallbacks
837+ def test_replications_info(self):
838+ """The replications_info method exercises its callback."""
839+ result = yield self.be.replications_info()
840+
841+ # replications_info will use exclusions information
842+ expected = []
843+ for name in MockReplicationClient.replications:
844+ enabled = bool_str(name not in MockReplicationClient.exclusions)
845+ dependency = ''
846+ if name == MockReplicationClient.BOOKMARKS:
847+ dependency = backend.BOOKMARKS_PKG
848+ elif name == MockReplicationClient.CONTACTS:
849+ dependency = backend.CONTACTS_PKG
850+
851+ item = {'replication_id': name, 'name': name,
852+ 'enabled': enabled, 'dependency': dependency}
853+ expected.append(item)
854+ self.assertEqual(sorted(expected), sorted(result))
855+
856+ @inlineCallbacks
857+ def test_change_replication_settings(self):
858+ """The replication settings can be changed."""
859+ rid = '0123-4567'
860+ MockReplicationClient.replications.add(rid)
861+ self.addCleanup(lambda: MockReplicationClient.replications.remove(rid))
862+
863+ yield self.be.change_replication_settings(rid, {'enabled': ''})
864+ self.assertIn(rid, MockReplicationClient.exclusions)
865+
866+ yield self.be.change_replication_settings(rid, {'enabled': 'True'})
867+ self.assertNotIn(rid, MockReplicationClient.exclusions)
868+
869+ @inlineCallbacks
870+ def test_change_replication_settings_not_in_replications(self):
871+ """The settings can not be changed for an item not in replications."""
872+ rid = '0123-4567'
873+ assert rid not in MockReplicationClient.replications
874+
875+ d = self.be.change_replication_settings(rid, {'enabled': 'True'})
876+ yield self.assertFailure(d, replication_client.ReplicationError)
877+
878+ d = self.be.change_replication_settings(rid, {'enabled': ''})
879+ yield self.assertFailure(d, replication_client.ReplicationError)
880+
881+ @inlineCallbacks
882+ def test_change_replication_settings_no_setting(self):
883+ """The change replication settings does not fail on empty settings."""
884+ rid = '0123-4567'
885+ MockReplicationClient.replications.add(rid)
886+ self.addCleanup(lambda: MockReplicationClient.replications.remove(rid))
887+
888+ prior = MockReplicationClient.exclusions.copy()
889+ yield self.be.change_replication_settings(rid, {})
890+
891+ self.assertEqual(MockReplicationClient.exclusions, prior)
892
893=== added file 'ubuntuone/controlpanel/tests/test_replication_client.py'
894--- ubuntuone/controlpanel/tests/test_replication_client.py 1970-01-01 00:00:00 +0000
895+++ ubuntuone/controlpanel/tests/test_replication_client.py 2011-01-06 20:44:10 +0000
896@@ -0,0 +1,160 @@
897+# -*- coding: utf-8 -*-
898+
899+# Authors: Natalia B. Bidart <natalia.bidart@canonical.com>
900+#
901+# Copyright 2010 Canonical Ltd.
902+#
903+# This program is free software: you can redistribute it and/or modify it
904+# under the terms of the GNU General Public License version 3, as published
905+# by the Free Software Foundation.
906+#
907+# This program is distributed in the hope that it will be useful, but
908+# WITHOUT ANY WARRANTY; without even the implied warranties of
909+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
910+# PURPOSE. See the GNU General Public License for more details.
911+#
912+# You should have received a copy of the GNU General Public License along
913+# with this program. If not, see <http://www.gnu.org/licenses/>.
914+
915+"""Tests for the DBus service when accessing desktopcouch replications."""
916+
917+from twisted.internet.defer import inlineCallbacks
918+
919+from ubuntuone.controlpanel import replication_client
920+from ubuntuone.controlpanel.tests import TestCase
921+
922+EXCLUSIONS = set()
923+
924+
925+class FakedReplication(object):
926+ """Faked a DC replication exclusion."""
927+
928+ def __init__(self):
929+ self.all_exclusions = lambda: EXCLUSIONS
930+ self.replicate = EXCLUSIONS.remove
931+ self.exclude = EXCLUSIONS.add
932+
933+
934+class FakedReplicationModule(object):
935+ """Faked a DC replication module."""
936+
937+ ReplicationExclusion = FakedReplication
938+
939+
940+class ReplicationsTestCase(TestCase):
941+ """Test for the replications client methods."""
942+
943+ def setUp(self):
944+ super(ReplicationsTestCase, self).setUp()
945+
946+ orig_get_proxy = replication_client.get_replication_proxy
947+
948+ def get_proxy():
949+ """Fake the proxy getter."""
950+ return orig_get_proxy(replication_module=FakedReplicationModule())
951+
952+ self.patch(replication_client, 'get_replication_proxy', get_proxy)
953+
954+ def tearDown(self):
955+ EXCLUSIONS.clear()
956+ super(ReplicationsTestCase, self).tearDown()
957+
958+ @inlineCallbacks
959+ def test_no_pairing_record(self):
960+ """Handle ValueError from replication layer."""
961+
962+ def no_pairing_record(*args, **kwargs):
963+ """Fail with ValueError."""
964+ raise ValueError('No pairing record.')
965+
966+ self.patch(FakedReplicationModule, 'ReplicationExclusion',
967+ no_pairing_record)
968+
969+ yield self.assertFailure(replication_client.get_replications(),
970+ replication_client.NoPairingRecord)
971+
972+ @inlineCallbacks
973+ def test_get_replications(self):
974+ """Replications are correctly retrieved."""
975+ result = yield replication_client.get_replications()
976+ self.assertEqual(result, replication_client.REPLICATIONS)
977+
978+ @inlineCallbacks
979+ def test_get_exclusions(self):
980+ """Exclusions are correctly retrieved."""
981+ replications = yield replication_client.get_replications()
982+ for rep in replications:
983+ yield replication_client.exclude(rep)
984+
985+ result = yield replication_client.get_exclusions()
986+ self.assertEqual(result, replications)
987+
988+ @inlineCallbacks
989+ def test_replicate(self):
990+ """Replicate a service is correct."""
991+ replications = yield replication_client.get_replications()
992+ rid = list(replications)[0]
993+ yield replication_client.exclude(rid)
994+
995+ yield replication_client.replicate(rid)
996+ exclusions = yield replication_client.get_exclusions()
997+ self.assertNotIn(rid, exclusions)
998+
999+ @inlineCallbacks
1000+ def test_replicate_name_not_in_replications(self):
1001+ """Replicate a service fails if not in replications."""
1002+ replications = yield replication_client.get_replications()
1003+ rid = 'not in replications'
1004+ assert rid not in replications
1005+
1006+ yield self.assertFailure(replication_client.replicate(rid),
1007+ replication_client.InvalidIdError)
1008+
1009+ @inlineCallbacks
1010+ def test_replicate_name_not_in_exclusions(self):
1011+ """Replicate a service fails if not in exclusions."""
1012+ replications = yield replication_client.get_replications()
1013+ rid = list(replications)[0]
1014+ assert rid in replications
1015+
1016+ exclusions = yield replication_client.get_exclusions()
1017+ assert rid not in exclusions
1018+
1019+ yield self.assertFailure(replication_client.replicate(rid),
1020+ replication_client.NotExcludedError)
1021+
1022+ @inlineCallbacks
1023+ def test_exclude(self):
1024+ """Excluding a service is correct."""
1025+ replications = yield replication_client.get_replications()
1026+ rid = list(replications)[0]
1027+ yield replication_client.exclude(rid)
1028+ yield replication_client.replicate(rid)
1029+
1030+ yield replication_client.exclude(rid)
1031+ exclusions = yield replication_client.get_exclusions()
1032+ self.assertIn(rid, exclusions)
1033+
1034+ @inlineCallbacks
1035+ def test_exclude_name_not_in_replications(self):
1036+ """Excluding a service fails if not in replications."""
1037+ replications = yield replication_client.get_replications()
1038+ rid = 'not in replications'
1039+ assert rid not in replications
1040+
1041+ yield self.assertFailure(replication_client.exclude(rid),
1042+ replication_client.InvalidIdError)
1043+
1044+ @inlineCallbacks
1045+ def test_exclude_name_in_exclusions(self):
1046+ """Excluding a service fails if already on exclusions."""
1047+ replications = yield replication_client.get_replications()
1048+ rid = list(replications)[0]
1049+ assert rid in replications
1050+
1051+ yield replication_client.exclude(rid)
1052+ exclusions = yield replication_client.get_exclusions()
1053+ assert rid in exclusions
1054+
1055+ yield self.assertFailure(replication_client.exclude(rid),
1056+ replication_client.AlreadyExcludedError)

Subscribers

People subscribed via source and target branches