Merge lp:~jpds/friends/linkedin-protocol into lp:friends
- linkedin-protocol
- Merge into trunk
Proposed by
Jonathan Davies
Status: | Superseded |
---|---|
Proposed branch: | lp:~jpds/friends/linkedin-protocol |
Merge into: | lp:friends |
Diff against target: |
1291 lines (+357/-306) (has conflicts) 20 files modified
Makefile (+1/-1) debian/changelog (+15/-2) debian/control (+6/-0) debian/friends-linkedin.install (+1/-0) friends/protocols/flickr.py (+3/-3) friends/protocols/linkedin.py (+133/-0) friends/service/dispatcher.py (+9/-8) friends/tests/mocks.py (+9/-4) friends/tests/test_account.py (+29/-107) friends/tests/test_authentication.py (+33/-39) friends/tests/test_dispatcher.py (+23/-24) friends/tests/test_facebook.py (+8/-5) friends/tests/test_flickr.py (+6/-2) friends/tests/test_foursquare.py (+4/-0) friends/tests/test_identica.py (+4/-0) friends/tests/test_twitter.py (+11/-21) friends/utils/account.py (+32/-71) friends/utils/authentication.py (+23/-10) friends/utils/base.py (+3/-3) tools/debug_live.py (+4/-6) Text conflict in debian/changelog |
To merge this branch: | bzr merge lp:~jpds/friends/linkedin-protocol |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Robert Bruce Park | Needs Resubmitting | ||
Review via email: mp+155194@code.launchpad.net |
This proposal has been superseded by a proposal from 2013-03-25.
Commit message
Implemented a base LinkedIn protocol for friends.
Description of the change
Implemented a base LinkedIn protocol for friends.
To post a comment you must log in.
- 197. By Jonathan Davies
-
Gave url .get() a default value for robustness.
- 198. By Jonathan Davies
-
Save LinkedIn connections to EDS with _push_to_eds().
Unmerged revisions
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile' |
2 | --- Makefile 2013-02-05 01:11:35 +0000 |
3 | +++ Makefile 2013-03-25 10:33:12 +0000 |
4 | @@ -14,7 +14,7 @@ |
5 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
6 | |
7 | check: |
8 | - python3 -m unittest discover -vv |
9 | + python3 -m unittest discover |
10 | |
11 | install: |
12 | python3 setup.py install |
13 | |
14 | === modified file 'debian/changelog' |
15 | --- debian/changelog 2013-03-22 05:02:25 +0000 |
16 | +++ debian/changelog 2013-03-25 10:33:12 +0000 |
17 | @@ -1,3 +1,4 @@ |
18 | +<<<<<<< TREE |
19 | friends (0.1.3daily13.03.22-0ubuntu1) raring; urgency=low |
20 | |
21 | * Automatic snapshot from revision 168 |
22 | @@ -14,18 +15,30 @@ |
23 | |
24 | -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 21 Mar 2013 05:02:17 +0000 |
25 | |
26 | +======= |
27 | +friends (0.2.0-0ubuntu2) UNRELEASED; urgency=low |
28 | + |
29 | + [ Robert Bruce Park ] |
30 | + * Version bump for the next development series. |
31 | + |
32 | + [ Jonathan Davies ] |
33 | + * Added friends-linkedin. |
34 | + |
35 | + -- Jonathan Davies <jonathan.davies@canonical.com> Thu, 21 Mar 2013 22:52:07 +0000 |
36 | + |
37 | +>>>>>>> MERGE-SOURCE |
38 | friends (0.1.3daily13.03.20-0ubuntu1) raring; urgency=low |
39 | |
40 | [ Robert Bruce Park ] |
41 | * Keep the Dispatcher alive for 30s beyond the return of the final |
42 | method invocation. |
43 | - * Stop deduplicating messages across protocols, simplifying model |
44 | + * Stop deduplicating messages across protocols, simplifying model |
45 | schema (LP: #1156941) |
46 | * Add schema columns for latitude, longitude, and location name. |
47 | * Fix 'likes' column from gdouble to guint64. |
48 | * Add geotagging support from foursquare, facebook, flickr. |
49 | * Implement since= for Facebook, reducing bandwidth usage. |
50 | - * Automatically prepend the required @mention to Twitter |
51 | + * Automatically prepend the required @mention to Twitter |
52 | replies (LP: #1156829) |
53 | * Automatically linkify URLs that get published to the model. |
54 | * Fix the publishing of Facebook Stories (LP: #1155785) |
55 | |
56 | === modified file 'debian/control' |
57 | --- debian/control 2013-02-22 00:06:42 +0000 |
58 | +++ debian/control 2013-03-25 10:33:12 +0000 |
59 | @@ -103,3 +103,9 @@ |
60 | Depends: friends, ${misc:Depends}, ${python3:Depends} |
61 | Description: Social integration with the desktop - Flickr |
62 | Provides social networking integration with the desktop |
63 | + |
64 | +Package: friends-linkedin |
65 | +Architecture: all |
66 | +Depends: friends, ${misc:Depends}, ${python3:Depends} |
67 | +Description: Social integration with the desktop - LinkedIn |
68 | + Provides social networking integration with the desktop |
69 | |
70 | === added file 'debian/friends-linkedin.install' |
71 | --- debian/friends-linkedin.install 1970-01-01 00:00:00 +0000 |
72 | +++ debian/friends-linkedin.install 2013-03-25 10:33:12 +0000 |
73 | @@ -0,0 +1,1 @@ |
74 | +usr/lib/python3/dist-packages/friends/protocols/linkedin* |
75 | |
76 | === modified file 'friends/protocols/flickr.py' |
77 | --- friends/protocols/flickr.py 2013-03-12 21:08:21 +0000 |
78 | +++ friends/protocols/flickr.py 2013-03-25 10:33:12 +0000 |
79 | @@ -82,7 +82,7 @@ |
80 | # http://www.flickr.com/services/api/flickr.people.getInfo.html |
81 | def _get_avatar(self, nsid): |
82 | args = dict( |
83 | - api_key=self._account.auth.parameters.get('ConsumerKey'), |
84 | + api_key=self._account.consumer_key, |
85 | method='flickr.people.getInfo', |
86 | format='json', |
87 | nojsoncallback='1', |
88 | @@ -109,7 +109,7 @@ |
89 | self._get_access_token() |
90 | |
91 | args = dict( |
92 | - api_key=self._account.auth.parameters.get('ConsumerKey'), |
93 | + api_key=self._account.consumer_key, |
94 | method='flickr.photos.getContactsPhotos', |
95 | format='json', |
96 | nojsoncallback='1', |
97 | @@ -179,7 +179,7 @@ |
98 | self._get_access_token() |
99 | |
100 | args = dict( |
101 | - api_key=self._account.auth.parameters.get('ConsumerKey'), |
102 | + api_key=self._account.consumer_key, |
103 | title=title, |
104 | ) |
105 | |
106 | |
107 | === added file 'friends/protocols/linkedin.py' |
108 | --- friends/protocols/linkedin.py 1970-01-01 00:00:00 +0000 |
109 | +++ friends/protocols/linkedin.py 2013-03-25 10:33:12 +0000 |
110 | @@ -0,0 +1,133 @@ |
111 | +# friends-dispatcher -- send & receive messages from any social network |
112 | +# Copyright (C) 2013 Canonical Ltd |
113 | +# |
114 | +# This program is free software: you can redistribute it and/or modify |
115 | +# it under the terms of the GNU General Public License as published by |
116 | +# the Free Software Foundation, version 3 of the License. |
117 | +# |
118 | +# This program is distributed in the hope that it will be useful, |
119 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
120 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
121 | +# GNU General Public License for more details. |
122 | +# |
123 | +# You should have received a copy of the GNU General Public License |
124 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
125 | + |
126 | +"""The LinkedIn protocol plugin.""" |
127 | + |
128 | + |
129 | +__all__ = [ |
130 | + 'LinkedIn', |
131 | + ] |
132 | + |
133 | +import time |
134 | +import logging |
135 | + |
136 | +from friends.utils.avatar import Avatar |
137 | +from friends.utils.base import Base, feature |
138 | +from friends.utils.cache import JsonCache |
139 | +from friends.utils.http import Downloader, Uploader |
140 | +from friends.utils.time import parsetime, iso8601utc |
141 | +from friends.errors import FriendsError |
142 | + |
143 | +log = logging.getLogger(__name__) |
144 | + |
145 | +class LinkedIn(Base): |
146 | + _api_base = 'https://api.linkedin.com/v1/{endpoint}?format=json&secure-urls=true&oauth2_access_token={token}' |
147 | + |
148 | + def _whoami(self, authdata): |
149 | + """Identify the authenticating user.""" |
150 | + # http://developer.linkedin.com/documents/profile-fields |
151 | + url = self._api_base.format( |
152 | + endpoint='people/~:(id,first-name,last-name)', |
153 | + token=self._get_access_token()) |
154 | + result = Downloader(url).get_json() |
155 | + self._account.user_id = result.get('id') |
156 | + self._account.user_full_name = '{firstName} {lastName}'.format(**result) |
157 | + |
158 | + def _publish_entry(self, entry, stream='messages'): |
159 | + """Publish a single update into the Dee.SharedModel.""" |
160 | + message_id = entry.get('updateKey') |
161 | + |
162 | + if message_id is None: |
163 | + # We can't do much with this entry. |
164 | + return |
165 | + |
166 | + content = entry.get('updateContent') |
167 | + person = content.get('person') |
168 | + name = '{firstName} {lastName}'.format(**person) |
169 | + person_id = person.get('id') |
170 | + status = person.get('currentStatus') |
171 | + picture = person.get('pictureUrl', '') |
172 | + url = person.get('siteStandardProfileRequest').get('url') |
173 | + timestamp = entry.get('timestamp') |
174 | + # We need to divide by 1000 here, as LinkedIn's timestamps have |
175 | + # milliseconds. |
176 | + iso_time = iso8601utc(int(timestamp/1000)) |
177 | + |
178 | + # Posts gives us a likes dict, while replies give us an int. |
179 | + likes = entry.get('numLikes', 0) |
180 | + |
181 | + args = dict( |
182 | + message_id=message_id, |
183 | + stream=stream, |
184 | + message=status, |
185 | + likes=likes, |
186 | + sender_id=person_id, |
187 | + sender=name, |
188 | + icon_uri=picture, |
189 | + link_url=url, |
190 | + timestamp=iso_time |
191 | + ) |
192 | + |
193 | + self._publish(**args) |
194 | + |
195 | + @feature |
196 | + def home(self): |
197 | + """Gather and publish public timeline messages.""" |
198 | + url = self._api_base.format( |
199 | + endpoint='people/~/network/updates', |
200 | + token=self._get_access_token()) + '&type=STAT' |
201 | + result = Downloader(url).get_json() |
202 | + values = result.get('values') |
203 | + for update in values: |
204 | + self._publish_entry(update) |
205 | + |
206 | + @feature |
207 | + def receive(self): |
208 | + """Gather and publish all incoming messages.""" |
209 | + self.home() |
210 | + return self._get_n_rows() |
211 | + |
212 | + def _create_contact(self, connection_json): |
213 | + """Build a VCard based on a dict representation of a contact.""" |
214 | + user_id = connection_json.get('id') |
215 | + |
216 | + user_fullname = '{firstName} {lastName}'.format(**connection_json) |
217 | + user_link = connection_json.get('siteStandardProfileRequest').get('url') |
218 | + |
219 | + attrs = {} |
220 | + attrs['linkined-id'] = user_id |
221 | + attrs['linkedin-name'] = user_fullname |
222 | + attrs['X-URIS'] = user_link |
223 | + |
224 | + contact = Base._create_contact( |
225 | + self, user_fullname, None, attrs) |
226 | + |
227 | + return contact |
228 | + |
229 | + @feature |
230 | + def contacts(self): |
231 | + """Retrieve a list of up to 500 LinkedIn connections.""" |
232 | + # http://developer.linkedin.com/documents/connections-api |
233 | + url = self._api_base.format( |
234 | + endpoint='people/~/connections', |
235 | + token=self._get_access_token()) |
236 | + result = Downloader(url).get_json() |
237 | + connections = result.get('values') |
238 | + |
239 | + for connection in connections: |
240 | + if connection.get('id') != 'private': |
241 | + # We cannot access information on profiles that are set to |
242 | + # private. |
243 | + self._create_contact(connection) |
244 | |
245 | === modified file 'friends/service/dispatcher.py' |
246 | --- friends/service/dispatcher.py 2013-03-20 01:19:39 +0000 |
247 | +++ friends/service/dispatcher.py 2013-03-25 10:33:12 +0000 |
248 | @@ -31,7 +31,7 @@ |
249 | from contextlib import ContextDecorator |
250 | |
251 | from friends.utils.avatar import Avatar |
252 | -from friends.utils.account import AccountManager |
253 | +from friends.utils.account import find_accounts |
254 | from friends.utils.manager import protocol_manager |
255 | from friends.utils.menus import MenuManager |
256 | from friends.utils.model import Model, persist_model |
257 | @@ -96,7 +96,8 @@ |
258 | bus_name = dbus.service.BusName(DBUS_INTERFACE, bus=self.bus) |
259 | super().__init__(bus_name, self.__dbus_object_path__) |
260 | self.mainloop = mainloop |
261 | - self.account_manager = AccountManager() |
262 | + |
263 | + self.accounts = find_accounts() |
264 | |
265 | self._unread_count = 0 |
266 | self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit) |
267 | @@ -119,7 +120,7 @@ |
268 | # account.protocol() starts a new thread and then returns |
269 | # immediately, so there is no delay or blocking during the |
270 | # execution of this method. |
271 | - for account in self.account_manager.get_all(): |
272 | + for account in self.accounts.values(): |
273 | try: |
274 | account.protocol('receive') |
275 | except NotImplementedError: |
276 | @@ -163,14 +164,14 @@ |
277 | service.Do('list', '6', 'list_id') # Fetch a single list. |
278 | """ |
279 | if account_id: |
280 | - accounts = [self.account_manager.get(account_id)] |
281 | + accounts = [self.accounts.get(int(account_id))] |
282 | if None in accounts: |
283 | message = 'Could not find account: {}'.format(account_id) |
284 | failure(message) |
285 | log.error(message) |
286 | return |
287 | else: |
288 | - accounts = list(self.account_manager.get_all()) |
289 | + accounts = list(self.accounts.values()) |
290 | |
291 | called = False |
292 | for account in accounts: |
293 | @@ -205,7 +206,7 @@ |
294 | service.SendMessage('Your message') |
295 | """ |
296 | sent = False |
297 | - for account in self.account_manager.get_all(): |
298 | + for account in self.accounts.values(): |
299 | if account.send_enabled: |
300 | sent = True |
301 | log.debug( |
302 | @@ -240,7 +241,7 @@ |
303 | service.SendReply('6', '34245645347345626', 'Your reply') |
304 | """ |
305 | log.debug('Replying to {}, {}'.format(account_id, message_id)) |
306 | - account = self.account_manager.get(account_id) |
307 | + account = self.accounts.get(int(account_id)) |
308 | if account is not None: |
309 | account.protocol( |
310 | 'send_thread', |
311 | @@ -298,7 +299,7 @@ |
312 | free to ignore error conditions at your peril. |
313 | """ |
314 | log.debug('Uploading {} to {}'.format(uri, account_id)) |
315 | - account = self.account_manager.get(account_id) |
316 | + account = self.accounts.get(int(account_id)) |
317 | if account is not None: |
318 | account.protocol( |
319 | 'upload', |
320 | |
321 | === modified file 'friends/tests/mocks.py' |
322 | --- friends/tests/mocks.py 2013-03-14 19:44:13 +0000 |
323 | +++ friends/tests/mocks.py 2013-03-25 10:33:12 +0000 |
324 | @@ -100,16 +100,21 @@ |
325 | |
326 | |
327 | class FakeAuth: |
328 | - id = 'fakeauth id' |
329 | - method = 'fakeauth method' |
330 | - parameters = {'ConsumerKey': 'fake', 'ConsumerSecret': 'alsofake'} |
331 | - mechanism = 'fakeauth mechanism' |
332 | + get_credentials_id = lambda *ignore: 'fakeauth id' |
333 | + get_method = lambda *ignore: 'fakeauth method' |
334 | + get_mechanism = lambda *ignore: 'fakeauth mechanism' |
335 | + get_parameters = lambda *ignore: { |
336 | + 'ConsumerKey': 'fake', |
337 | + 'ConsumerSecret': 'alsofake', |
338 | + } |
339 | |
340 | |
341 | class FakeAccount: |
342 | """A fake account object for testing purposes.""" |
343 | |
344 | def __init__(self, service=None, account_id=88): |
345 | + self.consumer_secret = 'secret' |
346 | + self.consumer_key = 'consume' |
347 | self.access_token = None |
348 | self.secret_token = None |
349 | self.user_full_name = None |
350 | |
351 | === modified file 'friends/tests/test_account.py' |
352 | --- friends/tests/test_account.py 2013-03-14 19:14:03 +0000 |
353 | +++ friends/tests/test_account.py 2013-03-25 10:33:12 +0000 |
354 | @@ -17,7 +17,6 @@ |
355 | |
356 | __all__ = [ |
357 | 'TestAccount', |
358 | - 'TestAccountManager', |
359 | ] |
360 | |
361 | |
362 | @@ -26,14 +25,15 @@ |
363 | from friends.errors import UnsupportedProtocolError |
364 | from friends.protocols.flickr import Flickr |
365 | from friends.tests.mocks import FakeAccount, LogMock, SettingsIterMock |
366 | -from friends.tests.mocks import TestModel, mock |
367 | -from friends.utils.account import Account, AccountManager |
368 | +from friends.tests.mocks import TestModel, LogMock, mock |
369 | +from friends.utils.account import Account, _find_accounts_uoa |
370 | |
371 | |
372 | class TestAccount(unittest.TestCase): |
373 | """Test Account class.""" |
374 | |
375 | def setUp(self): |
376 | + self.log_mock = LogMock('friends.utils.account') |
377 | def connect_side_effect(signal, callback, account): |
378 | # The account service provides a .connect method that connects a |
379 | # signal to a callback. We have to mock a side effect into the |
380 | @@ -49,7 +49,9 @@ |
381 | 'get_credentials_id.return_value': 'fake credentials', |
382 | 'get_method.return_value': 'fake method', |
383 | 'get_mechanism.return_value': 'fake mechanism', |
384 | - 'get_parameters.return_value': 'fake parameters', |
385 | + 'get_parameters.return_value': { |
386 | + 'ConsumerKey': 'fake_key', |
387 | + 'ConsumerSecret': 'fake_secret'}, |
388 | }), |
389 | 'get_account.return_value': mock.Mock(**{ |
390 | 'get_settings_iter.return_value': SettingsIterMock(), |
391 | @@ -63,17 +65,21 @@ |
392 | }) |
393 | self.account = Account(self.account_service) |
394 | |
395 | + def tearDown(self): |
396 | + self.log_mock.stop() |
397 | + |
398 | def test_account_auth(self): |
399 | # Test that the constructor initializes the 'auth' attribute. |
400 | auth = self.account.auth |
401 | - self.assertEqual(auth.id, 'fake credentials') |
402 | - self.assertEqual(auth.method, 'fake method') |
403 | - self.assertEqual(auth.mechanism, 'fake mechanism') |
404 | - self.assertEqual(auth.parameters, 'fake parameters') |
405 | + self.assertEqual(auth.get_credentials_id(), 'fake credentials') |
406 | + self.assertEqual(auth.get_method(), 'fake method') |
407 | + self.assertEqual(auth.get_mechanism(), 'fake mechanism') |
408 | + self.assertEqual(auth.get_parameters(), |
409 | + dict(ConsumerKey='fake_key', |
410 | + ConsumerSecret='fake_secret')) |
411 | |
412 | def test_account_id(self): |
413 | self.assertEqual(self.account.id, 'fake_id') |
414 | - self.assertEqual(self.account.protocol_name, 'flickr') |
415 | |
416 | def test_account_service(self): |
417 | # The protocol attribute refers directly to the protocol used. |
418 | @@ -122,102 +128,18 @@ |
419 | self.assertFalse(hasattr(self.account, 'bee')) |
420 | self.assertFalse(hasattr(self.account, 'cat')) |
421 | |
422 | - def test_enabled(self): |
423 | - # .enabled() just passes through from the account service. |
424 | - self.account_service.get_enabled.return_value = True |
425 | - self.assertTrue(self.account.enabled) |
426 | - self.account_service.get_enabled.return_value = False |
427 | - self.assertFalse(self.account.enabled) |
428 | - |
429 | - def test_equal(self): |
430 | - # Two accounts are equal if their account services are equal. |
431 | - other = Account(self.account_service) |
432 | - self.assertEqual(self.account, other) |
433 | - assert not self.account == None |
434 | - |
435 | - def test_unequal(self): |
436 | - # Two accounts are unequal if their account services are unequal. The |
437 | - # other mock service has to at least support the basic required API. |
438 | - other = Account(mock.Mock(**{ |
439 | - 'get_account.return_value': mock.Mock(**{ |
440 | - 'get_settings_iter.return_value': SettingsIterMock(), |
441 | - # It's okay if the provider names are the same; the test |
442 | - # is for whether the account services are the same or not, |
443 | - # and in this test, they'll be different mock instances. |
444 | - 'get_provider_name.return_value': 'flickr', |
445 | - }), |
446 | - })) |
447 | - self.assertNotEqual(self.account, other) |
448 | - assert self.account != None |
449 | - |
450 | - |
451 | -accounts_manager = mock.Mock() |
452 | -accounts_manager.new_for_service_type( |
453 | - 'microblogging').get_enabled_account_services.return_value = [] |
454 | - |
455 | - |
456 | -@mock.patch('gi.repository.Accounts.Manager', accounts_manager) |
457 | -@mock.patch('friends.utils.account.Account', FakeAccount) |
458 | -class TestAccountManager(unittest.TestCase): |
459 | - """Test the AccountManager API.""" |
460 | - |
461 | - def setUp(self): |
462 | - TestModel.clear() |
463 | - self.account_service = mock.Mock() |
464 | - |
465 | + @mock.patch('friends.utils.account.manager') |
466 | + @mock.patch('friends.utils.account.Account') |
467 | @mock.patch('friends.utils.account.Accounts') |
468 | - def test_get_service(self, accounts_mock): |
469 | - manager = AccountManager() |
470 | - manager_mock = mock.Mock() |
471 | - account_mock = mock.Mock() |
472 | - service_mock = mock.Mock() |
473 | - manager_mock.get_account.return_value = account_mock |
474 | - account_mock.list_services.return_value = [service_mock] |
475 | - account_service_mock = accounts_mock.AccountService.new(account_mock, |
476 | - service_mock) |
477 | - account_service_mock.get_service( |
478 | - ).get_display_name().lower.return_value = 'protocol' |
479 | - |
480 | - service = manager._get_service(manager_mock, 10) |
481 | - |
482 | - manager_mock.get_account.assert_called_once_with(10) |
483 | - account_mock.list_services.assert_called_once_with() |
484 | - accounts_mock.AccountService.new.assert_called_with(account_mock, |
485 | - service_mock) |
486 | - |
487 | - def test_account_manager_add_new_account(self): |
488 | - # Explicitly adding a new account puts the account's global_id into |
489 | - # the account manager's mapping. |
490 | - manager = AccountManager() |
491 | - manager._add_new_account(self.account_service) |
492 | - self.assertIn(88, manager._accounts) |
493 | - |
494 | - def test_account_manager_enabled_event(self): |
495 | - manager = AccountManager() |
496 | - manager._get_service = mock.Mock() |
497 | - manager._get_service.return_value = mock.Mock() |
498 | - manager._add_new_account = mock.Mock() |
499 | - manager._add_new_account.return_value = account = mock.Mock() |
500 | - manager._on_enabled_event(accounts_manager, 2) |
501 | - account.protocol.assert_called_once_with('receive') |
502 | - |
503 | - |
504 | -@mock.patch('gi.repository.Accounts.Manager', accounts_manager) |
505 | -class TestAccountManagerRealAccount(unittest.TestCase): |
506 | - """Test of the AccountManager API requiring the real Account class. |
507 | - |
508 | - You'll need to guarantee other mocks are in place such that the real |
509 | - accounts are not touched. |
510 | - """ |
511 | - def setUp(self): |
512 | - self.account_service = mock.Mock() |
513 | - |
514 | - def test_account_manager_add_new_account_unsupported(self): |
515 | - fake_account = self.account_service.get_account() |
516 | - fake_account.get_provider_name.return_value = 'no service' |
517 | - manager = AccountManager() |
518 | - with LogMock('friends.utils.account') as log_mock: |
519 | - manager._add_new_account(self.account_service) |
520 | - log_contents = log_mock.empty(trim=False) |
521 | - self.assertNotIn('no service', manager._accounts) |
522 | - self.assertEqual(log_contents, 'Unsupported protocol: no service\n') |
523 | + def test_find_accounts(self, accts, acct, manager): |
524 | + service = mock.Mock() |
525 | + get_enabled = manager.get_enabled_account_services |
526 | + get_enabled.return_value = [service] |
527 | + manager.reset_mock() |
528 | + accounts = _find_accounts_uoa() |
529 | + get_enabled.assert_called_once_with() |
530 | + acct.assert_called_once_with(service) |
531 | + self.assertEqual(accounts, {acct().id: acct()}) |
532 | + self.assertEqual(self.log_mock.empty(), |
533 | + 'Flickr (fake_id) got send_enabled: True\n' |
534 | + 'Accounts found: 1\n') |
535 | |
536 | === modified file 'friends/tests/test_authentication.py' |
537 | --- friends/tests/test_authentication.py 2013-02-05 01:11:35 +0000 |
538 | +++ friends/tests/test_authentication.py 2013-03-25 10:33:12 +0000 |
539 | @@ -27,7 +27,7 @@ |
540 | import unittest |
541 | |
542 | from friends.utils.authentication import Authentication |
543 | -from friends.tests.mocks import FakeAccount, mock |
544 | +from friends.tests.mocks import FakeAccount, LogMock, mock |
545 | from friends.errors import AuthorizationError |
546 | |
547 | |
548 | @@ -43,7 +43,11 @@ |
549 | # error, and user_data arguments. We'll use the parameters |
550 | # argument as a way to specify whether an error occurred during |
551 | # authentication or not. |
552 | - callback(None, self.results, parameters, None) |
553 | + callback( |
554 | + None, |
555 | + self.results, |
556 | + parameters if hasattr(parameters, 'message') else None, |
557 | + None) |
558 | |
559 | |
560 | class FakeSignon: |
561 | @@ -56,59 +60,49 @@ |
562 | results = dict(NoAccessToken='fail') |
563 | |
564 | |
565 | -class Logger: |
566 | - def __init__(self): |
567 | - self.debug_messages = [] |
568 | - self.error_messages = [] |
569 | - |
570 | - def debug(self, message, *args): |
571 | - self.debug_messages.append(message.format(*args)) |
572 | - |
573 | - def error(self, message, *args): |
574 | - self.error_messages.append(message.format(*args)) |
575 | - |
576 | - reset = __init__ |
577 | - |
578 | - |
579 | -logger = Logger() |
580 | - |
581 | - |
582 | class TestAuthentication(unittest.TestCase): |
583 | """Test authentication.""" |
584 | |
585 | def setUp(self): |
586 | + self.log_mock = LogMock('friends.utils.authentication') |
587 | self.account = FakeAccount() |
588 | - self.account.auth.id = 'my id' |
589 | - self.account.auth.method = 'some method' |
590 | - self.account.auth.parameters = 'change me' |
591 | - self.account.auth.mechanism = ['whatever'] |
592 | - logger.reset() |
593 | - |
594 | - @mock.patch('friends.utils.authentication.log', logger) |
595 | + self.account.auth.get_credentials_id = lambda *ignore: 'my id' |
596 | + self.account.auth.get_method = lambda *ignore: 'some method' |
597 | + self.account.auth.get_parameters = lambda *ignore: 'change me' |
598 | + self.account.auth.get_mechanism = lambda *ignore: 'whatever' |
599 | + |
600 | + def tearDown(self): |
601 | + self.log_mock.stop() |
602 | + |
603 | + @mock.patch('friends.utils.authentication.manager') |
604 | @mock.patch('friends.utils.authentication.Signon', FakeSignon) |
605 | - def test_successful_login(self): |
606 | + @mock.patch('friends.utils.authentication.Accounts') |
607 | + def test_successful_login(self, accounts, *mocks): |
608 | # Prevent an error in the callback. |
609 | - self.account.auth.parameters = False |
610 | - authenticator = Authentication(self.account) |
611 | + accounts.AccountService.new().get_auth_data( |
612 | + ).get_parameters.return_value = False |
613 | + authenticator = Authentication(self.account.id) |
614 | reply = authenticator.login() |
615 | self.assertEqual(reply, dict(AccessToken='auth reply')) |
616 | - self.assertEqual(logger.debug_messages, ['Login completed']) |
617 | - self.assertEqual(logger.error_messages, []) |
618 | + self.assertEqual(self.log_mock.empty(), 'Login completed\n') |
619 | |
620 | - @mock.patch('friends.utils.authentication.log', logger) |
621 | + @mock.patch('friends.utils.authentication.manager') |
622 | + @mock.patch('friends.utils.authentication.Accounts') |
623 | @mock.patch('friends.utils.authentication.Signon', FailingSignon) |
624 | - def test_missing_access_token(self): |
625 | + def test_missing_access_token(self, *mocks): |
626 | # Prevent an error in the callback. |
627 | - self.account.auth.parameters = False |
628 | - authenticator = Authentication(self.account) |
629 | + self.account.auth.get_parameters = lambda *ignore: False |
630 | + authenticator = Authentication(self.account.id) |
631 | self.assertRaises(AuthorizationError, authenticator.login) |
632 | |
633 | - @mock.patch('friends.utils.authentication.log', logger) |
634 | + @mock.patch('friends.utils.authentication.manager') |
635 | @mock.patch('friends.utils.authentication.Signon', FakeSignon) |
636 | - def test_failed_login(self): |
637 | + @mock.patch('friends.utils.authentication.Accounts') |
638 | + def test_failed_login(self, accounts, *mocks): |
639 | # Trigger an error in the callback. |
640 | class Error: |
641 | message = 'who are you?' |
642 | - self.account.auth.parameters = Error |
643 | - authenticator = Authentication(self.account) |
644 | + accounts.AccountService.new( |
645 | + ).get_auth_data().get_parameters.return_value = Error |
646 | + authenticator = Authentication(self.account.id) |
647 | self.assertRaises(AuthorizationError, authenticator.login) |
648 | |
649 | === modified file 'friends/tests/test_dispatcher.py' |
650 | --- friends/tests/test_dispatcher.py 2013-03-20 01:19:39 +0000 |
651 | +++ friends/tests/test_dispatcher.py 2013-03-25 10:33:12 +0000 |
652 | @@ -38,13 +38,13 @@ |
653 | """Test the dispatcher's ability to dispatch.""" |
654 | |
655 | @mock.patch('dbus.service.BusName') |
656 | - @mock.patch('friends.service.dispatcher.AccountManager') |
657 | - @mock.patch('friends.service.dispatcher.Dispatcher.Refresh') |
658 | + @mock.patch('friends.service.dispatcher.find_accounts') |
659 | @mock.patch('dbus.service.Object.__init__') |
660 | def setUp(self, *mocks): |
661 | self.log_mock = LogMock('friends.service.dispatcher', |
662 | 'friends.utils.account') |
663 | self.dispatcher = Dispatcher(mock.Mock(), mock.Mock()) |
664 | + self.dispatcher.accounts = {} |
665 | |
666 | def tearDown(self): |
667 | self.log_mock.stop() |
668 | @@ -53,12 +53,12 @@ |
669 | def test_refresh(self, threading_mock): |
670 | account = mock.Mock() |
671 | threading_mock.activeCount.return_value = 1 |
672 | - self.dispatcher.account_manager = mock.Mock() |
673 | - self.dispatcher.account_manager.get_all.return_value = [account] |
674 | + self.dispatcher.accounts = mock.Mock() |
675 | + self.dispatcher.accounts.values.return_value = [account] |
676 | |
677 | self.assertIsNone(self.dispatcher.Refresh()) |
678 | |
679 | - self.dispatcher.account_manager.get_all.assert_called_once_with() |
680 | + self.dispatcher.accounts.values.assert_called_once_with() |
681 | account.protocol.assert_called_once_with('receive') |
682 | |
683 | self.assertEqual(self.log_mock.empty(), |
684 | @@ -75,12 +75,11 @@ |
685 | def test_do(self): |
686 | account = mock.Mock() |
687 | account.id = '345' |
688 | - self.dispatcher.account_manager = mock.Mock() |
689 | - self.dispatcher.account_manager.get.return_value = account |
690 | + self.dispatcher.accounts = mock.Mock() |
691 | + self.dispatcher.accounts.get.return_value = account |
692 | |
693 | self.dispatcher.Do('like', '345', '23346356767354626') |
694 | - self.dispatcher.account_manager.get.assert_called_once_with( |
695 | - '345') |
696 | + self.dispatcher.accounts.get.assert_called_once_with(345) |
697 | account.protocol.assert_called_once_with( |
698 | 'like', '23346356767354626', success=STUB, failure=STUB) |
699 | |
700 | @@ -92,11 +91,11 @@ |
701 | |
702 | def test_failing_do(self): |
703 | account = mock.Mock() |
704 | - self.dispatcher.account_manager = mock.Mock() |
705 | - self.dispatcher.account_manager.get.return_value = None |
706 | + self.dispatcher.accounts = mock.Mock() |
707 | + self.dispatcher.accounts.get.return_value = None |
708 | |
709 | self.dispatcher.Do('unlike', '6', '23346356767354626') |
710 | - self.dispatcher.account_manager.get.assert_called_once_with('6') |
711 | + self.dispatcher.accounts.get.assert_called_once_with(6) |
712 | self.assertEqual(account.protocol.call_count, 0) |
713 | |
714 | self.assertEqual(self.log_mock.empty(), |
715 | @@ -111,15 +110,15 @@ |
716 | account3 = mock.Mock() |
717 | account2.send_enabled = False |
718 | |
719 | - self.dispatcher.account_manager = mock.Mock() |
720 | - self.dispatcher.account_manager.get_all.return_value = [ |
721 | + self.dispatcher.accounts = mock.Mock() |
722 | + self.dispatcher.accounts.values.return_value = [ |
723 | account1, |
724 | account2, |
725 | account3, |
726 | ] |
727 | |
728 | self.dispatcher.SendMessage('Howdy friends!') |
729 | - self.dispatcher.account_manager.get_all.assert_called_once_with() |
730 | + self.dispatcher.accounts.values.assert_called_once_with() |
731 | account1.protocol.assert_called_once_with( |
732 | 'send', 'Howdy friends!', success=STUB, failure=STUB) |
733 | account3.protocol.assert_called_once_with( |
734 | @@ -128,11 +127,11 @@ |
735 | |
736 | def test_send_reply(self): |
737 | account = mock.Mock() |
738 | - self.dispatcher.account_manager = mock.Mock() |
739 | - self.dispatcher.account_manager.get.return_value = account |
740 | + self.dispatcher.accounts = mock.Mock() |
741 | + self.dispatcher.accounts.get.return_value = account |
742 | |
743 | self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]') |
744 | - self.dispatcher.account_manager.get.assert_called_once_with('2') |
745 | + self.dispatcher.accounts.get.assert_called_once_with(2) |
746 | account.protocol.assert_called_once_with( |
747 | 'send_thread', 'objid', '[Hilarious Response]', |
748 | success=STUB, failure=STUB) |
749 | @@ -145,11 +144,11 @@ |
750 | |
751 | def test_send_reply_failed(self): |
752 | account = mock.Mock() |
753 | - self.dispatcher.account_manager = mock.Mock() |
754 | - self.dispatcher.account_manager.get.return_value = None |
755 | + self.dispatcher.accounts = mock.Mock() |
756 | + self.dispatcher.accounts.get.return_value = None |
757 | |
758 | self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]') |
759 | - self.dispatcher.account_manager.get.assert_called_once_with('2') |
760 | + self.dispatcher.accounts.get.assert_called_once_with(2) |
761 | self.assertEqual(account.protocol.call_count, 0) |
762 | |
763 | self.assertEqual(self.log_mock.empty(), |
764 | @@ -161,8 +160,8 @@ |
765 | |
766 | def test_upload_async(self): |
767 | account = mock.Mock() |
768 | - self.dispatcher.account_manager = mock.Mock() |
769 | - self.dispatcher.account_manager.get.return_value = account |
770 | + self.dispatcher.accounts = mock.Mock() |
771 | + self.dispatcher.accounts.get.return_value = account |
772 | |
773 | success = mock.Mock() |
774 | failure = mock.Mock() |
775 | @@ -172,7 +171,7 @@ |
776 | 'A thousand words', |
777 | success=success, |
778 | failure=failure) |
779 | - self.dispatcher.account_manager.get.assert_called_once_with('2') |
780 | + self.dispatcher.accounts.get.assert_called_once_with(2) |
781 | account.protocol.assert_called_once_with( |
782 | 'upload', |
783 | 'file://path/to/image.png', |
784 | |
785 | === modified file 'friends/tests/test_facebook.py' |
786 | --- friends/tests/test_facebook.py 2013-03-16 00:54:45 +0000 |
787 | +++ friends/tests/test_facebook.py 2013-03-25 10:33:12 +0000 |
788 | @@ -60,11 +60,13 @@ |
789 | ['contacts', 'delete', 'home', 'like', 'receive', 'search', 'send', |
790 | 'send_thread', 'unlike', 'upload', 'wall']) |
791 | |
792 | + @mock.patch('friends.utils.authentication.manager') |
793 | + @mock.patch('friends.utils.authentication.Accounts') |
794 | @mock.patch('friends.utils.authentication.Authentication.login', |
795 | return_value=dict(AccessToken='abc')) |
796 | @mock.patch('friends.utils.http.Soup.Message', |
797 | FakeSoupMessage('friends.tests.data', 'facebook-login.dat')) |
798 | - def test_successful_login(self, mock): |
799 | + def test_successful_login(self, *mocks): |
800 | # Test that a successful response from graph.facebook.com returning |
801 | # the user's data, sets up the account dict correctly. |
802 | self.protocol._login() |
803 | @@ -72,14 +74,18 @@ |
804 | self.assertEqual(self.account.user_name, 'Bart Person') |
805 | self.assertEqual(self.account.user_id, '801') |
806 | |
807 | + @mock.patch('friends.utils.authentication.manager') |
808 | + @mock.patch('friends.utils.authentication.Accounts') |
809 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
810 | @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
811 | - def test_login_unsuccessful_authentication(self, mock): |
812 | + def test_login_unsuccessful_authentication(self, *mocks): |
813 | # The user is not already logged in, but the act of logging in fails. |
814 | self.assertRaises(AuthorizationError, self.protocol._login) |
815 | self.assertIsNone(self.account.access_token) |
816 | self.assertIsNone(self.account.user_name) |
817 | |
818 | + @mock.patch('friends.utils.authentication.manager') |
819 | + @mock.patch('friends.utils.authentication.Accounts') |
820 | @mock.patch('friends.utils.authentication.Authentication.login', |
821 | return_value=dict(AccessToken='abc')) |
822 | @mock.patch('friends.protocols.facebook.Downloader.get_json', |
823 | @@ -237,9 +243,6 @@ |
824 | def test_home_since_id(self, *mocks): |
825 | self.account.access_token = 'access' |
826 | self.account.secret_token = 'secret' |
827 | - self.account.auth.parameters = dict( |
828 | - ConsumerKey='key', |
829 | - ConsumerSecret='secret') |
830 | self.assertEqual(self.protocol.home(), 12) |
831 | |
832 | with open(self._root.format('facebook_ids'), 'r') as fd: |
833 | |
834 | === modified file 'friends/tests/test_flickr.py' |
835 | --- friends/tests/test_flickr.py 2013-03-20 23:57:40 +0000 |
836 | +++ friends/tests/test_flickr.py 2013-03-25 10:33:12 +0000 |
837 | @@ -86,6 +86,8 @@ |
838 | # But also no photos. |
839 | self.assertEqual(TestModel.get_n_rows(), 0) |
840 | |
841 | + @mock.patch('friends.utils.authentication.manager') |
842 | + @mock.patch('friends.utils.authentication.Accounts') |
843 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
844 | @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
845 | @mock.patch('friends.utils.http.Soup.Message', |
846 | @@ -95,6 +97,8 @@ |
847 | # AccessToken, but this fails. |
848 | self.assertRaises(AuthorizationError, self.protocol.receive) |
849 | |
850 | + @mock.patch('friends.utils.authentication.manager') |
851 | + @mock.patch('friends.utils.authentication.Accounts') |
852 | @mock.patch('friends.utils.http.Soup.Message', |
853 | FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat')) |
854 | @mock.patch('friends.utils.authentication.Authentication.login', |
855 | @@ -102,7 +106,7 @@ |
856 | user_nsid='bob', |
857 | AccessToken='123', |
858 | TokenSecret='abc')) |
859 | - def test_login_successful_authentication(self, mock): |
860 | + def test_login_successful_authentication(self, *mocks): |
861 | # Logging in required communication with the account service to get an |
862 | # AccessToken, but this fails. |
863 | self.protocol.receive() |
864 | @@ -131,7 +135,7 @@ |
865 | extras='date_upload,owner_name,icon_server,geo', |
866 | format='json', |
867 | nojsoncallback='1', |
868 | - api_key='fake', |
869 | + api_key='consume', |
870 | method='flickr.photos.getContactsPhotos', |
871 | ), |
872 | headers={}) |
873 | |
874 | === modified file 'friends/tests/test_foursquare.py' |
875 | --- friends/tests/test_foursquare.py 2013-03-14 19:14:03 +0000 |
876 | +++ friends/tests/test_foursquare.py 2013-03-25 10:33:12 +0000 |
877 | @@ -50,6 +50,8 @@ |
878 | # The set of public features. |
879 | self.assertEqual(FourSquare.get_features(), ['receive']) |
880 | |
881 | + @mock.patch('friends.utils.authentication.manager') |
882 | + @mock.patch('friends.utils.authentication.Accounts') |
883 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
884 | @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
885 | @mock.patch('friends.utils.http.Downloader.get_json', |
886 | @@ -59,6 +61,8 @@ |
887 | self.assertIsNone(self.account.user_name) |
888 | self.assertIsNone(self.account.user_id) |
889 | |
890 | + @mock.patch('friends.utils.authentication.manager') |
891 | + @mock.patch('friends.utils.authentication.Accounts') |
892 | @mock.patch('friends.utils.authentication.Authentication.login', |
893 | return_value=dict(AccessToken='tokeny goodness')) |
894 | @mock.patch('friends.protocols.foursquare.Downloader.get_json', |
895 | |
896 | === modified file 'friends/tests/test_identica.py' |
897 | --- friends/tests/test_identica.py 2013-03-14 19:14:03 +0000 |
898 | +++ friends/tests/test_identica.py 2013-03-25 10:33:12 +0000 |
899 | @@ -53,6 +53,8 @@ |
900 | self.log_mock.stop() |
901 | shutil.rmtree(self._temp_cache) |
902 | |
903 | + @mock.patch('friends.utils.authentication.manager') |
904 | + @mock.patch('friends.utils.authentication.Accounts') |
905 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
906 | @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
907 | @mock.patch('friends.utils.http.Downloader.get_json', |
908 | @@ -62,6 +64,8 @@ |
909 | self.assertIsNone(self.account.user_name) |
910 | self.assertIsNone(self.account.user_id) |
911 | |
912 | + @mock.patch('friends.utils.authentication.manager') |
913 | + @mock.patch('friends.utils.authentication.Accounts') |
914 | @mock.patch('friends.utils.authentication.Authentication.login', |
915 | return_value=dict(AccessToken='some clever fake data', |
916 | TokenSecret='sssssshhh!')) |
917 | |
918 | === modified file 'friends/tests/test_twitter.py' |
919 | --- friends/tests/test_twitter.py 2013-03-19 04:17:58 +0000 |
920 | +++ friends/tests/test_twitter.py 2013-03-25 10:33:12 +0000 |
921 | @@ -57,15 +57,19 @@ |
922 | self.log_mock.stop() |
923 | shutil.rmtree(self._temp_cache) |
924 | |
925 | + @mock.patch('friends.utils.authentication.manager') |
926 | + @mock.patch('friends.utils.authentication.Accounts') |
927 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
928 | @mock.patch('friends.utils.authentication.Signon.AuthSession.new') |
929 | @mock.patch('friends.protocols.twitter.Downloader.get_json', |
930 | return_value=None) |
931 | - def test_unsuccessful_authentication(self, dload, login): |
932 | + def test_unsuccessful_authentication(self, dload, login, *mocks): |
933 | self.assertRaises(AuthorizationError, self.protocol._login) |
934 | self.assertIsNone(self.account.user_name) |
935 | self.assertIsNone(self.account.user_id) |
936 | |
937 | + @mock.patch('friends.utils.authentication.manager') |
938 | + @mock.patch('friends.utils.authentication.Accounts') |
939 | @mock.patch('friends.utils.authentication.Authentication.login', |
940 | return_value=dict(AccessToken='some clever fake data', |
941 | TokenSecret='sssssshhh!', |
942 | @@ -86,11 +90,12 @@ |
943 | def test_signatures(self, dload): |
944 | self.account.secret_token = 'alpha' |
945 | self.account.access_token = 'omega' |
946 | - self.account.auth.id = 6 |
947 | - self.account.auth.method = 'oauth2' |
948 | - self.account.auth.mechanism = 'HMAC-SHA1' |
949 | - self.account.auth.parameters = dict(ConsumerKey='consume', |
950 | - ConsumerSecret='obey') |
951 | + self.account.consumer_secret = 'obey' |
952 | + self.account.consumer_key = 'consume' |
953 | + self.account.auth.get_credentials_id = lambda *ignore: 6 |
954 | + self.account.auth.get_method = lambda *ignore: 'oauth2' |
955 | + self.account.auth.get_mechanism = lambda *ignore: 'HMAC-SHA1' |
956 | + |
957 | result = '''\ |
958 | OAuth oauth_nonce="once%20upon%20a%20nonce", \ |
959 | oauth_timestamp="1348690628", \ |
960 | @@ -122,9 +127,6 @@ |
961 | def test_home(self, *mocks): |
962 | self.account.access_token = 'access' |
963 | self.account.secret_token = 'secret' |
964 | - self.account.auth.parameters = dict( |
965 | - ConsumerKey='key', |
966 | - ConsumerSecret='secret') |
967 | self.assertEqual(0, TestModel.get_n_rows()) |
968 | self.assertEqual(self.protocol.home(), 3) |
969 | self.assertEqual(3, TestModel.get_n_rows()) |
970 | @@ -171,9 +173,6 @@ |
971 | def test_home_since_id(self, *mocks): |
972 | self.account.access_token = 'access' |
973 | self.account.secret_token = 'secret' |
974 | - self.account.auth.parameters = dict( |
975 | - ConsumerKey='key', |
976 | - ConsumerSecret='secret') |
977 | self.assertEqual(self.protocol.home(), 3) |
978 | |
979 | with open(self._root.format('twitter_ids'), 'r') as fd: |
980 | @@ -196,9 +195,6 @@ |
981 | self.account.access_token = 'access' |
982 | self.account.secret_token = 'secret' |
983 | self.account.user_name = 'oauth_dancer' |
984 | - self.account.auth.parameters = dict( |
985 | - ConsumerKey='key', |
986 | - ConsumerSecret='secret') |
987 | self.assertEqual(0, TestModel.get_n_rows()) |
988 | self.assertEqual( |
989 | self.protocol.send('some message'), |
990 | @@ -399,9 +395,6 @@ |
991 | def test_send_thread_prepend_nick(self, *mocks): |
992 | self.account.access_token = 'access' |
993 | self.account.secret_token = 'secret' |
994 | - self.account.auth.parameters = dict( |
995 | - ConsumerKey='key', |
996 | - ConsumerSecret='secret') |
997 | self.assertEqual(0, TestModel.get_n_rows()) |
998 | self.assertEqual(self.protocol.home(), 3) |
999 | self.assertEqual(3, TestModel.get_n_rows()) |
1000 | @@ -703,9 +696,6 @@ |
1001 | def test_protocol_rate_limiting(self, time, sleep, login): |
1002 | self.account.access_token = 'access' |
1003 | self.account.secret_token = 'secret' |
1004 | - self.account.auth.parameters = dict( |
1005 | - ConsumerKey='key', |
1006 | - ConsumerSecret='secret') |
1007 | # Test rate limiting via the Twitter plugin API. |
1008 | # |
1009 | # The first call doesn't get rate limited. |
1010 | |
1011 | === modified file 'friends/utils/account.py' |
1012 | --- friends/utils/account.py 2013-03-13 02:03:12 +0000 |
1013 | +++ friends/utils/account.py 2013-03-25 10:33:12 +0000 |
1014 | @@ -17,7 +17,7 @@ |
1015 | |
1016 | __all__ = [ |
1017 | 'Account', |
1018 | - 'AccountManager', |
1019 | + 'find_accounts', |
1020 | ] |
1021 | |
1022 | |
1023 | @@ -28,65 +28,30 @@ |
1024 | |
1025 | from friends.errors import UnsupportedProtocolError |
1026 | from friends.utils.manager import protocol_manager |
1027 | +from friends.utils.authentication import manager |
1028 | |
1029 | |
1030 | log = logging.getLogger(__name__) |
1031 | |
1032 | |
1033 | -class AccountManager: |
1034 | - """Manage the accounts that we know about.""" |
1035 | - |
1036 | - def __init__(self): |
1037 | - self._accounts = {} |
1038 | - # Ask libaccounts for a manager of the microblogging services. |
1039 | - # Connect callbacks to the manager so that we can react when accounts |
1040 | - # are added or deleted. |
1041 | - manager = Accounts.Manager.new_for_service_type('microblogging') |
1042 | - manager.connect('enabled-event', self._on_enabled_event) |
1043 | - # Add all the currently known accounts. |
1044 | - for account_service in manager.get_enabled_account_services(): |
1045 | - self._add_new_account(account_service) |
1046 | - log.info('Accounts found: {}'.format(len(self._accounts))) |
1047 | - |
1048 | - def _get_service(self, manager, account_id): |
1049 | - """Instantiate an AccountService and identify it.""" |
1050 | - account = manager.get_account(account_id) |
1051 | - for service in account.list_services(): |
1052 | - return Accounts.AccountService.new(account, service) |
1053 | - |
1054 | - def _on_enabled_event(self, manager, account_id): |
1055 | - """React to new microblogging accounts being enabled or disabled.""" |
1056 | - account_service = self._get_service(manager, account_id) |
1057 | - if account_service is not None and account_service.get_enabled(): |
1058 | - log.debug('Adding account {}'.format(account_id)) |
1059 | - account = self._add_new_account(account_service) |
1060 | - if account is not None: |
1061 | - account.protocol('receive') |
1062 | - |
1063 | - def _add_new_account(self, account_service): |
1064 | +def _find_accounts_uoa(): |
1065 | + """Consult Ubuntu Online Accounts for the accounts we have.""" |
1066 | + accounts = {} |
1067 | + for service in manager.get_enabled_account_services(): |
1068 | try: |
1069 | - new_account = Account(account_service) |
1070 | + account = Account(service) |
1071 | except UnsupportedProtocolError as error: |
1072 | log.info(error) |
1073 | else: |
1074 | - self._accounts[new_account.id] = new_account |
1075 | - return new_account |
1076 | - |
1077 | - def get_all(self): |
1078 | - return self._accounts.values() |
1079 | - |
1080 | - def get(self, account_id, default=None): |
1081 | - return self._accounts.get(int(account_id), default) |
1082 | - |
1083 | - |
1084 | -class AuthData: |
1085 | - """This class serves as a sub-namespace for Account instances.""" |
1086 | - |
1087 | - def __init__(self, auth_data): |
1088 | - self.id = auth_data.get_credentials_id() |
1089 | - self.method = auth_data.get_method() |
1090 | - self.mechanism = auth_data.get_mechanism() |
1091 | - self.parameters = auth_data.get_parameters() |
1092 | + accounts[account.id] = account |
1093 | + log.info('Accounts found: {}'.format(len(accounts))) |
1094 | + return accounts |
1095 | + |
1096 | + |
1097 | +def find_accounts(): |
1098 | + # TODO: Implement GOA support, then fill out this method with some |
1099 | + # logic for determining whether to use UOA or GOA. |
1100 | + return _find_accounts_uoa() |
1101 | |
1102 | |
1103 | class Account: |
1104 | @@ -99,6 +64,8 @@ |
1105 | ) |
1106 | |
1107 | # Defaults for the known and useful attributes. |
1108 | + consumer_secret = None |
1109 | + consumer_key = None |
1110 | access_token = None |
1111 | secret_token = None |
1112 | send_enabled = None |
1113 | @@ -108,15 +75,22 @@ |
1114 | id = None |
1115 | |
1116 | def __init__(self, account_service): |
1117 | - self.account_service = account_service |
1118 | - self.auth = AuthData(account_service.get_auth_data()) |
1119 | + self.auth = account_service.get_auth_data() |
1120 | + if self.auth is not None: |
1121 | + auth_params = self.auth.get_parameters() |
1122 | + self.consumer_key = auth_params.get('ConsumerKey') |
1123 | + self.consumer_secret = auth_params.get('ConsumerSecret') |
1124 | + else: |
1125 | + raise UnsupportedProtocolError( |
1126 | + 'This AgAccountService is missing AgAuthData!') |
1127 | + |
1128 | # The provider in libaccounts should match the name of our protocol. |
1129 | account = account_service.get_account() |
1130 | self.id = account.id |
1131 | - self.protocol_name = account.get_provider_name() |
1132 | - protocol_class = protocol_manager.protocols.get(self.protocol_name) |
1133 | + protocol_name = account.get_provider_name() |
1134 | + protocol_class = protocol_manager.protocols.get(protocol_name) |
1135 | if protocol_class is None: |
1136 | - raise UnsupportedProtocolError(self.protocol_name) |
1137 | + raise UnsupportedProtocolError(protocol_name) |
1138 | self.protocol = protocol_class(self) |
1139 | # Connect responders to changes in the account information. |
1140 | account_service.connect('changed', self._on_account_changed, account) |
1141 | @@ -152,24 +126,11 @@ |
1142 | while True: |
1143 | success, key, value = settings.next() |
1144 | if success: |
1145 | - log.debug('{} got {}: {}'.format(self.id, key, value)) |
1146 | + log.debug('{} ({}) got {}: {}'.format( |
1147 | + self.protocol._Name, self.id, key, value)) |
1148 | # Testing for tuple membership makes this easy to expand |
1149 | # later, if necessary. |
1150 | if key in Account._LIBACCOUNTS_PROPERTIES: |
1151 | setattr(self, key, value) |
1152 | else: |
1153 | break |
1154 | - |
1155 | - @property |
1156 | - def enabled(self): |
1157 | - return self.account_service.get_enabled() |
1158 | - |
1159 | - def __eq__(self, other): |
1160 | - if other is None: |
1161 | - return False |
1162 | - return self.account_service == other.account_service |
1163 | - |
1164 | - def __ne__(self, other): |
1165 | - if other is None: |
1166 | - return True |
1167 | - return self.account_service != other.account_service |
1168 | |
1169 | === modified file 'friends/utils/authentication.py' |
1170 | --- friends/utils/authentication.py 2013-03-13 17:34:26 +0000 |
1171 | +++ friends/utils/authentication.py 2013-03-25 10:33:12 +0000 |
1172 | @@ -23,7 +23,7 @@ |
1173 | import logging |
1174 | import time |
1175 | |
1176 | -from gi.repository import GObject, Signon |
1177 | +from gi.repository import GObject, Accounts, Signon |
1178 | |
1179 | from friends.errors import AuthorizationError |
1180 | |
1181 | @@ -37,17 +37,30 @@ |
1182 | LOGIN_TIMEOUT = 30 # Currently this is measured in half-seconds. |
1183 | |
1184 | |
1185 | +# Yes, this is not the most logical place to instantiate this, but I |
1186 | +# couldn't do it in account.py due to cyclical import dependencies. |
1187 | +manager = Accounts.Manager.new_for_service_type('microblogging') |
1188 | + |
1189 | + |
1190 | class Authentication: |
1191 | - def __init__(self, account): |
1192 | - self.account = account |
1193 | + def __init__(self, account_id): |
1194 | + self.account_id = account_id |
1195 | + account = manager.get_account(account_id) |
1196 | + service = account.list_services()[0] |
1197 | + self.auth = Accounts.AccountService.new( |
1198 | + account, service).get_auth_data() |
1199 | self._reply = None |
1200 | |
1201 | def login(self): |
1202 | - auth = self.account.auth |
1203 | - self.auth_session = Signon.AuthSession.new(auth.id, auth.method) |
1204 | + auth = self.auth |
1205 | + self.auth_session = Signon.AuthSession.new( |
1206 | + auth.get_credentials_id(), |
1207 | + auth.get_method()) |
1208 | self.auth_session.process( |
1209 | - auth.parameters, auth.mechanism, |
1210 | - self._login_cb, None) |
1211 | + auth.get_parameters(), |
1212 | + auth.get_mechanism(), |
1213 | + self._login_cb, |
1214 | + None) |
1215 | timeout = LOGIN_TIMEOUT |
1216 | while self._reply is None and timeout > 0: |
1217 | # We're building a synchronous API on top of an inherently |
1218 | @@ -56,17 +69,17 @@ |
1219 | time.sleep(0.5) |
1220 | timeout -= 1 |
1221 | if self._reply is None: |
1222 | - raise AuthorizationError(self.account.id, 'Login timed out.') |
1223 | + raise AuthorizationError(self.account_id, 'Login timed out.') |
1224 | if 'AccessToken' not in self._reply: |
1225 | raise AuthorizationError( |
1226 | - self.account.id, |
1227 | + self.account_id, |
1228 | 'No AccessToken found: {!r}'.format(self._reply)) |
1229 | return self._reply |
1230 | |
1231 | def _login_cb(self, session, reply, error, user_data): |
1232 | self._reply = reply |
1233 | if error: |
1234 | - exception = AuthorizationError(self.account.id, error.message) |
1235 | + exception = AuthorizationError(self.account_id, error.message) |
1236 | # Mardy says this error can happen during normal operation. |
1237 | if error.message.endswith('userActionFinished error: 10'): |
1238 | log.error(str(exception)) |
1239 | |
1240 | === modified file 'friends/utils/base.py' |
1241 | --- friends/utils/base.py 2013-03-20 01:19:39 +0000 |
1242 | +++ friends/utils/base.py 2013-03-25 10:33:12 +0000 |
1243 | @@ -460,7 +460,7 @@ |
1244 | log.debug('{} to {}'.format( |
1245 | 'Re-authenticating' if old_token else 'Logging in', self._Name)) |
1246 | |
1247 | - result = Authentication(self._account).login() |
1248 | + result = Authentication(self._account.id).login() |
1249 | |
1250 | self._account.access_token = result.get('AccessToken') |
1251 | self._whoami(result) |
1252 | @@ -469,8 +469,8 @@ |
1253 | def _get_oauth_headers(self, method, url, data=None, headers=None): |
1254 | """Basic wrapper around oauthlib that we use for Twitter and Flickr.""" |
1255 | # "Client" == "Consumer" in oauthlib parlance. |
1256 | - client_key = self._account.auth.parameters['ConsumerKey'] |
1257 | - client_secret = self._account.auth.parameters['ConsumerSecret'] |
1258 | + client_key = self._account.consumer_key |
1259 | + client_secret = self._account.consumer_secret |
1260 | |
1261 | # "resource_owner" == secret and token. |
1262 | resource_owner_key = self._get_access_token() |
1263 | |
1264 | === modified file 'tools/debug_live.py' |
1265 | --- tools/debug_live.py 2013-03-13 18:05:13 +0000 |
1266 | +++ tools/debug_live.py 2013-03-25 10:33:12 +0000 |
1267 | @@ -30,7 +30,7 @@ |
1268 | # Print all logs for debugging purposes |
1269 | initialize(debug=True, console=True) |
1270 | |
1271 | -from friends.utils.account import AccountManager |
1272 | +from friends.utils.account import find_accounts |
1273 | from friends.utils.base import initialize_caches, _OperationThread |
1274 | from friends.utils.model import Model |
1275 | |
1276 | @@ -53,12 +53,10 @@ |
1277 | |
1278 | initialize_caches() |
1279 | |
1280 | + Model.connect('row-added', row_added) |
1281 | + |
1282 | found = False |
1283 | - a = AccountManager() |
1284 | - |
1285 | - Model.connect('row-added', row_added) |
1286 | - |
1287 | - for account in a._accounts.values(): |
1288 | + for account in find_accounts().values(): |
1289 | if account.protocol._name == protocol.lower(): |
1290 | found = True |
1291 | account.protocol(*args) |
MP into wrong branch ;-)