Merge lp:~jpds/friends/linkedin-protocol into lp:friends

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
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.
Revision history for this message
Robert Bruce Park (robru) wrote :

MP into wrong branch ;-)

review: Needs Resubmitting
lp:~jpds/friends/linkedin-protocol updated
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
=== modified file 'Makefile'
--- Makefile 2013-02-05 01:11:35 +0000
+++ Makefile 2013-03-25 10:33:12 +0000
@@ -14,7 +14,7 @@
14# along with this program. If not, see <http://www.gnu.org/licenses/>.14# along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16check:16check:
17 python3 -m unittest discover -vv17 python3 -m unittest discover
1818
19install:19install:
20 python3 setup.py install20 python3 setup.py install
2121
=== modified file 'debian/changelog'
--- debian/changelog 2013-03-22 05:02:25 +0000
+++ debian/changelog 2013-03-25 10:33:12 +0000
@@ -1,3 +1,4 @@
1<<<<<<< TREE
1friends (0.1.3daily13.03.22-0ubuntu1) raring; urgency=low2friends (0.1.3daily13.03.22-0ubuntu1) raring; urgency=low
23
3 * Automatic snapshot from revision 1684 * Automatic snapshot from revision 168
@@ -14,18 +15,30 @@
1415
15 -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 21 Mar 2013 05:02:17 +000016 -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Thu, 21 Mar 2013 05:02:17 +0000
1617
18=======
19friends (0.2.0-0ubuntu2) UNRELEASED; urgency=low
20
21 [ Robert Bruce Park ]
22 * Version bump for the next development series.
23
24 [ Jonathan Davies ]
25 * Added friends-linkedin.
26
27 -- Jonathan Davies <jonathan.davies@canonical.com> Thu, 21 Mar 2013 22:52:07 +0000
28
29>>>>>>> MERGE-SOURCE
17friends (0.1.3daily13.03.20-0ubuntu1) raring; urgency=low30friends (0.1.3daily13.03.20-0ubuntu1) raring; urgency=low
1831
19 [ Robert Bruce Park ]32 [ Robert Bruce Park ]
20 * Keep the Dispatcher alive for 30s beyond the return of the final33 * Keep the Dispatcher alive for 30s beyond the return of the final
21 method invocation.34 method invocation.
22 * Stop deduplicating messages across protocols, simplifying model 35 * Stop deduplicating messages across protocols, simplifying model
23 schema (LP: #1156941)36 schema (LP: #1156941)
24 * Add schema columns for latitude, longitude, and location name.37 * Add schema columns for latitude, longitude, and location name.
25 * Fix 'likes' column from gdouble to guint64.38 * Fix 'likes' column from gdouble to guint64.
26 * Add geotagging support from foursquare, facebook, flickr.39 * Add geotagging support from foursquare, facebook, flickr.
27 * Implement since= for Facebook, reducing bandwidth usage.40 * Implement since= for Facebook, reducing bandwidth usage.
28 * Automatically prepend the required @mention to Twitter 41 * Automatically prepend the required @mention to Twitter
29 replies (LP: #1156829)42 replies (LP: #1156829)
30 * Automatically linkify URLs that get published to the model.43 * Automatically linkify URLs that get published to the model.
31 * Fix the publishing of Facebook Stories (LP: #1155785)44 * Fix the publishing of Facebook Stories (LP: #1155785)
3245
=== modified file 'debian/control'
--- debian/control 2013-02-22 00:06:42 +0000
+++ debian/control 2013-03-25 10:33:12 +0000
@@ -103,3 +103,9 @@
103Depends: friends, ${misc:Depends}, ${python3:Depends}103Depends: friends, ${misc:Depends}, ${python3:Depends}
104Description: Social integration with the desktop - Flickr104Description: Social integration with the desktop - Flickr
105 Provides social networking integration with the desktop105 Provides social networking integration with the desktop
106
107Package: friends-linkedin
108Architecture: all
109Depends: friends, ${misc:Depends}, ${python3:Depends}
110Description: Social integration with the desktop - LinkedIn
111 Provides social networking integration with the desktop
106112
=== added file 'debian/friends-linkedin.install'
--- debian/friends-linkedin.install 1970-01-01 00:00:00 +0000
+++ debian/friends-linkedin.install 2013-03-25 10:33:12 +0000
@@ -0,0 +1,1 @@
1usr/lib/python3/dist-packages/friends/protocols/linkedin*
02
=== modified file 'friends/protocols/flickr.py'
--- friends/protocols/flickr.py 2013-03-12 21:08:21 +0000
+++ friends/protocols/flickr.py 2013-03-25 10:33:12 +0000
@@ -82,7 +82,7 @@
82# http://www.flickr.com/services/api/flickr.people.getInfo.html82# http://www.flickr.com/services/api/flickr.people.getInfo.html
83 def _get_avatar(self, nsid):83 def _get_avatar(self, nsid):
84 args = dict(84 args = dict(
85 api_key=self._account.auth.parameters.get('ConsumerKey'),85 api_key=self._account.consumer_key,
86 method='flickr.people.getInfo',86 method='flickr.people.getInfo',
87 format='json',87 format='json',
88 nojsoncallback='1',88 nojsoncallback='1',
@@ -109,7 +109,7 @@
109 self._get_access_token()109 self._get_access_token()
110110
111 args = dict(111 args = dict(
112 api_key=self._account.auth.parameters.get('ConsumerKey'),112 api_key=self._account.consumer_key,
113 method='flickr.photos.getContactsPhotos',113 method='flickr.photos.getContactsPhotos',
114 format='json',114 format='json',
115 nojsoncallback='1',115 nojsoncallback='1',
@@ -179,7 +179,7 @@
179 self._get_access_token()179 self._get_access_token()
180180
181 args = dict(181 args = dict(
182 api_key=self._account.auth.parameters.get('ConsumerKey'),182 api_key=self._account.consumer_key,
183 title=title,183 title=title,
184 )184 )
185185
186186
=== added file 'friends/protocols/linkedin.py'
--- friends/protocols/linkedin.py 1970-01-01 00:00:00 +0000
+++ friends/protocols/linkedin.py 2013-03-25 10:33:12 +0000
@@ -0,0 +1,133 @@
1# friends-dispatcher -- send & receive messages from any social network
2# Copyright (C) 2013 Canonical Ltd
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, version 3 of the License.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""The LinkedIn protocol plugin."""
17
18
19__all__ = [
20 'LinkedIn',
21 ]
22
23import time
24import logging
25
26from friends.utils.avatar import Avatar
27from friends.utils.base import Base, feature
28from friends.utils.cache import JsonCache
29from friends.utils.http import Downloader, Uploader
30from friends.utils.time import parsetime, iso8601utc
31from friends.errors import FriendsError
32
33log = logging.getLogger(__name__)
34
35class LinkedIn(Base):
36 _api_base = 'https://api.linkedin.com/v1/{endpoint}?format=json&secure-urls=true&oauth2_access_token={token}'
37
38 def _whoami(self, authdata):
39 """Identify the authenticating user."""
40 # http://developer.linkedin.com/documents/profile-fields
41 url = self._api_base.format(
42 endpoint='people/~:(id,first-name,last-name)',
43 token=self._get_access_token())
44 result = Downloader(url).get_json()
45 self._account.user_id = result.get('id')
46 self._account.user_full_name = '{firstName} {lastName}'.format(**result)
47
48 def _publish_entry(self, entry, stream='messages'):
49 """Publish a single update into the Dee.SharedModel."""
50 message_id = entry.get('updateKey')
51
52 if message_id is None:
53 # We can't do much with this entry.
54 return
55
56 content = entry.get('updateContent')
57 person = content.get('person')
58 name = '{firstName} {lastName}'.format(**person)
59 person_id = person.get('id')
60 status = person.get('currentStatus')
61 picture = person.get('pictureUrl', '')
62 url = person.get('siteStandardProfileRequest').get('url')
63 timestamp = entry.get('timestamp')
64 # We need to divide by 1000 here, as LinkedIn's timestamps have
65 # milliseconds.
66 iso_time = iso8601utc(int(timestamp/1000))
67
68 # Posts gives us a likes dict, while replies give us an int.
69 likes = entry.get('numLikes', 0)
70
71 args = dict(
72 message_id=message_id,
73 stream=stream,
74 message=status,
75 likes=likes,
76 sender_id=person_id,
77 sender=name,
78 icon_uri=picture,
79 link_url=url,
80 timestamp=iso_time
81 )
82
83 self._publish(**args)
84
85 @feature
86 def home(self):
87 """Gather and publish public timeline messages."""
88 url = self._api_base.format(
89 endpoint='people/~/network/updates',
90 token=self._get_access_token()) + '&type=STAT'
91 result = Downloader(url).get_json()
92 values = result.get('values')
93 for update in values:
94 self._publish_entry(update)
95
96 @feature
97 def receive(self):
98 """Gather and publish all incoming messages."""
99 self.home()
100 return self._get_n_rows()
101
102 def _create_contact(self, connection_json):
103 """Build a VCard based on a dict representation of a contact."""
104 user_id = connection_json.get('id')
105
106 user_fullname = '{firstName} {lastName}'.format(**connection_json)
107 user_link = connection_json.get('siteStandardProfileRequest').get('url')
108
109 attrs = {}
110 attrs['linkined-id'] = user_id
111 attrs['linkedin-name'] = user_fullname
112 attrs['X-URIS'] = user_link
113
114 contact = Base._create_contact(
115 self, user_fullname, None, attrs)
116
117 return contact
118
119 @feature
120 def contacts(self):
121 """Retrieve a list of up to 500 LinkedIn connections."""
122 # http://developer.linkedin.com/documents/connections-api
123 url = self._api_base.format(
124 endpoint='people/~/connections',
125 token=self._get_access_token())
126 result = Downloader(url).get_json()
127 connections = result.get('values')
128
129 for connection in connections:
130 if connection.get('id') != 'private':
131 # We cannot access information on profiles that are set to
132 # private.
133 self._create_contact(connection)
0134
=== modified file 'friends/service/dispatcher.py'
--- friends/service/dispatcher.py 2013-03-20 01:19:39 +0000
+++ friends/service/dispatcher.py 2013-03-25 10:33:12 +0000
@@ -31,7 +31,7 @@
31from contextlib import ContextDecorator31from contextlib import ContextDecorator
3232
33from friends.utils.avatar import Avatar33from friends.utils.avatar import Avatar
34from friends.utils.account import AccountManager34from friends.utils.account import find_accounts
35from friends.utils.manager import protocol_manager35from friends.utils.manager import protocol_manager
36from friends.utils.menus import MenuManager36from friends.utils.menus import MenuManager
37from friends.utils.model import Model, persist_model37from friends.utils.model import Model, persist_model
@@ -96,7 +96,8 @@
96 bus_name = dbus.service.BusName(DBUS_INTERFACE, bus=self.bus)96 bus_name = dbus.service.BusName(DBUS_INTERFACE, bus=self.bus)
97 super().__init__(bus_name, self.__dbus_object_path__)97 super().__init__(bus_name, self.__dbus_object_path__)
98 self.mainloop = mainloop98 self.mainloop = mainloop
99 self.account_manager = AccountManager()99
100 self.accounts = find_accounts()
100101
101 self._unread_count = 0102 self._unread_count = 0
102 self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit)103 self.menu_manager = MenuManager(self.Refresh, self.mainloop.quit)
@@ -119,7 +120,7 @@
119 # account.protocol() starts a new thread and then returns120 # account.protocol() starts a new thread and then returns
120 # immediately, so there is no delay or blocking during the121 # immediately, so there is no delay or blocking during the
121 # execution of this method.122 # execution of this method.
122 for account in self.account_manager.get_all():123 for account in self.accounts.values():
123 try:124 try:
124 account.protocol('receive')125 account.protocol('receive')
125 except NotImplementedError:126 except NotImplementedError:
@@ -163,14 +164,14 @@
163 service.Do('list', '6', 'list_id') # Fetch a single list.164 service.Do('list', '6', 'list_id') # Fetch a single list.
164 """165 """
165 if account_id:166 if account_id:
166 accounts = [self.account_manager.get(account_id)]167 accounts = [self.accounts.get(int(account_id))]
167 if None in accounts:168 if None in accounts:
168 message = 'Could not find account: {}'.format(account_id)169 message = 'Could not find account: {}'.format(account_id)
169 failure(message)170 failure(message)
170 log.error(message)171 log.error(message)
171 return172 return
172 else:173 else:
173 accounts = list(self.account_manager.get_all())174 accounts = list(self.accounts.values())
174175
175 called = False176 called = False
176 for account in accounts:177 for account in accounts:
@@ -205,7 +206,7 @@
205 service.SendMessage('Your message')206 service.SendMessage('Your message')
206 """207 """
207 sent = False208 sent = False
208 for account in self.account_manager.get_all():209 for account in self.accounts.values():
209 if account.send_enabled:210 if account.send_enabled:
210 sent = True211 sent = True
211 log.debug(212 log.debug(
@@ -240,7 +241,7 @@
240 service.SendReply('6', '34245645347345626', 'Your reply')241 service.SendReply('6', '34245645347345626', 'Your reply')
241 """242 """
242 log.debug('Replying to {}, {}'.format(account_id, message_id))243 log.debug('Replying to {}, {}'.format(account_id, message_id))
243 account = self.account_manager.get(account_id)244 account = self.accounts.get(int(account_id))
244 if account is not None:245 if account is not None:
245 account.protocol(246 account.protocol(
246 'send_thread',247 'send_thread',
@@ -298,7 +299,7 @@
298 free to ignore error conditions at your peril.299 free to ignore error conditions at your peril.
299 """300 """
300 log.debug('Uploading {} to {}'.format(uri, account_id))301 log.debug('Uploading {} to {}'.format(uri, account_id))
301 account = self.account_manager.get(account_id)302 account = self.accounts.get(int(account_id))
302 if account is not None:303 if account is not None:
303 account.protocol(304 account.protocol(
304 'upload',305 'upload',
305306
=== modified file 'friends/tests/mocks.py'
--- friends/tests/mocks.py 2013-03-14 19:44:13 +0000
+++ friends/tests/mocks.py 2013-03-25 10:33:12 +0000
@@ -100,16 +100,21 @@
100100
101101
102class FakeAuth:102class FakeAuth:
103 id = 'fakeauth id'103 get_credentials_id = lambda *ignore: 'fakeauth id'
104 method = 'fakeauth method'104 get_method = lambda *ignore: 'fakeauth method'
105 parameters = {'ConsumerKey': 'fake', 'ConsumerSecret': 'alsofake'}105 get_mechanism = lambda *ignore: 'fakeauth mechanism'
106 mechanism = 'fakeauth mechanism'106 get_parameters = lambda *ignore: {
107 'ConsumerKey': 'fake',
108 'ConsumerSecret': 'alsofake',
109 }
107110
108111
109class FakeAccount:112class FakeAccount:
110 """A fake account object for testing purposes."""113 """A fake account object for testing purposes."""
111114
112 def __init__(self, service=None, account_id=88):115 def __init__(self, service=None, account_id=88):
116 self.consumer_secret = 'secret'
117 self.consumer_key = 'consume'
113 self.access_token = None118 self.access_token = None
114 self.secret_token = None119 self.secret_token = None
115 self.user_full_name = None120 self.user_full_name = None
116121
=== modified file 'friends/tests/test_account.py'
--- friends/tests/test_account.py 2013-03-14 19:14:03 +0000
+++ friends/tests/test_account.py 2013-03-25 10:33:12 +0000
@@ -17,7 +17,6 @@
1717
18__all__ = [18__all__ = [
19 'TestAccount',19 'TestAccount',
20 'TestAccountManager',
21 ]20 ]
2221
2322
@@ -26,14 +25,15 @@
26from friends.errors import UnsupportedProtocolError25from friends.errors import UnsupportedProtocolError
27from friends.protocols.flickr import Flickr26from friends.protocols.flickr import Flickr
28from friends.tests.mocks import FakeAccount, LogMock, SettingsIterMock27from friends.tests.mocks import FakeAccount, LogMock, SettingsIterMock
29from friends.tests.mocks import TestModel, mock28from friends.tests.mocks import TestModel, LogMock, mock
30from friends.utils.account import Account, AccountManager29from friends.utils.account import Account, _find_accounts_uoa
3130
3231
33class TestAccount(unittest.TestCase):32class TestAccount(unittest.TestCase):
34 """Test Account class."""33 """Test Account class."""
3534
36 def setUp(self):35 def setUp(self):
36 self.log_mock = LogMock('friends.utils.account')
37 def connect_side_effect(signal, callback, account):37 def connect_side_effect(signal, callback, account):
38 # The account service provides a .connect method that connects a38 # The account service provides a .connect method that connects a
39 # signal to a callback. We have to mock a side effect into the39 # signal to a callback. We have to mock a side effect into the
@@ -49,7 +49,9 @@
49 'get_credentials_id.return_value': 'fake credentials',49 'get_credentials_id.return_value': 'fake credentials',
50 'get_method.return_value': 'fake method',50 'get_method.return_value': 'fake method',
51 'get_mechanism.return_value': 'fake mechanism',51 'get_mechanism.return_value': 'fake mechanism',
52 'get_parameters.return_value': 'fake parameters',52 'get_parameters.return_value': {
53 'ConsumerKey': 'fake_key',
54 'ConsumerSecret': 'fake_secret'},
53 }),55 }),
54 'get_account.return_value': mock.Mock(**{56 'get_account.return_value': mock.Mock(**{
55 'get_settings_iter.return_value': SettingsIterMock(),57 'get_settings_iter.return_value': SettingsIterMock(),
@@ -63,17 +65,21 @@
63 })65 })
64 self.account = Account(self.account_service)66 self.account = Account(self.account_service)
6567
68 def tearDown(self):
69 self.log_mock.stop()
70
66 def test_account_auth(self):71 def test_account_auth(self):
67 # Test that the constructor initializes the 'auth' attribute.72 # Test that the constructor initializes the 'auth' attribute.
68 auth = self.account.auth73 auth = self.account.auth
69 self.assertEqual(auth.id, 'fake credentials')74 self.assertEqual(auth.get_credentials_id(), 'fake credentials')
70 self.assertEqual(auth.method, 'fake method')75 self.assertEqual(auth.get_method(), 'fake method')
71 self.assertEqual(auth.mechanism, 'fake mechanism')76 self.assertEqual(auth.get_mechanism(), 'fake mechanism')
72 self.assertEqual(auth.parameters, 'fake parameters')77 self.assertEqual(auth.get_parameters(),
78 dict(ConsumerKey='fake_key',
79 ConsumerSecret='fake_secret'))
7380
74 def test_account_id(self):81 def test_account_id(self):
75 self.assertEqual(self.account.id, 'fake_id')82 self.assertEqual(self.account.id, 'fake_id')
76 self.assertEqual(self.account.protocol_name, 'flickr')
7783
78 def test_account_service(self):84 def test_account_service(self):
79 # The protocol attribute refers directly to the protocol used.85 # The protocol attribute refers directly to the protocol used.
@@ -122,102 +128,18 @@
122 self.assertFalse(hasattr(self.account, 'bee'))128 self.assertFalse(hasattr(self.account, 'bee'))
123 self.assertFalse(hasattr(self.account, 'cat'))129 self.assertFalse(hasattr(self.account, 'cat'))
124130
125 def test_enabled(self):131 @mock.patch('friends.utils.account.manager')
126 # .enabled() just passes through from the account service.132 @mock.patch('friends.utils.account.Account')
127 self.account_service.get_enabled.return_value = True
128 self.assertTrue(self.account.enabled)
129 self.account_service.get_enabled.return_value = False
130 self.assertFalse(self.account.enabled)
131
132 def test_equal(self):
133 # Two accounts are equal if their account services are equal.
134 other = Account(self.account_service)
135 self.assertEqual(self.account, other)
136 assert not self.account == None
137
138 def test_unequal(self):
139 # Two accounts are unequal if their account services are unequal. The
140 # other mock service has to at least support the basic required API.
141 other = Account(mock.Mock(**{
142 'get_account.return_value': mock.Mock(**{
143 'get_settings_iter.return_value': SettingsIterMock(),
144 # It's okay if the provider names are the same; the test
145 # is for whether the account services are the same or not,
146 # and in this test, they'll be different mock instances.
147 'get_provider_name.return_value': 'flickr',
148 }),
149 }))
150 self.assertNotEqual(self.account, other)
151 assert self.account != None
152
153
154accounts_manager = mock.Mock()
155accounts_manager.new_for_service_type(
156 'microblogging').get_enabled_account_services.return_value = []
157
158
159@mock.patch('gi.repository.Accounts.Manager', accounts_manager)
160@mock.patch('friends.utils.account.Account', FakeAccount)
161class TestAccountManager(unittest.TestCase):
162 """Test the AccountManager API."""
163
164 def setUp(self):
165 TestModel.clear()
166 self.account_service = mock.Mock()
167
168 @mock.patch('friends.utils.account.Accounts')133 @mock.patch('friends.utils.account.Accounts')
169 def test_get_service(self, accounts_mock):134 def test_find_accounts(self, accts, acct, manager):
170 manager = AccountManager()135 service = mock.Mock()
171 manager_mock = mock.Mock()136 get_enabled = manager.get_enabled_account_services
172 account_mock = mock.Mock()137 get_enabled.return_value = [service]
173 service_mock = mock.Mock()138 manager.reset_mock()
174 manager_mock.get_account.return_value = account_mock139 accounts = _find_accounts_uoa()
175 account_mock.list_services.return_value = [service_mock]140 get_enabled.assert_called_once_with()
176 account_service_mock = accounts_mock.AccountService.new(account_mock,141 acct.assert_called_once_with(service)
177 service_mock)142 self.assertEqual(accounts, {acct().id: acct()})
178 account_service_mock.get_service(143 self.assertEqual(self.log_mock.empty(),
179 ).get_display_name().lower.return_value = 'protocol'144 'Flickr (fake_id) got send_enabled: True\n'
180145 'Accounts found: 1\n')
181 service = manager._get_service(manager_mock, 10)
182
183 manager_mock.get_account.assert_called_once_with(10)
184 account_mock.list_services.assert_called_once_with()
185 accounts_mock.AccountService.new.assert_called_with(account_mock,
186 service_mock)
187
188 def test_account_manager_add_new_account(self):
189 # Explicitly adding a new account puts the account's global_id into
190 # the account manager's mapping.
191 manager = AccountManager()
192 manager._add_new_account(self.account_service)
193 self.assertIn(88, manager._accounts)
194
195 def test_account_manager_enabled_event(self):
196 manager = AccountManager()
197 manager._get_service = mock.Mock()
198 manager._get_service.return_value = mock.Mock()
199 manager._add_new_account = mock.Mock()
200 manager._add_new_account.return_value = account = mock.Mock()
201 manager._on_enabled_event(accounts_manager, 2)
202 account.protocol.assert_called_once_with('receive')
203
204
205@mock.patch('gi.repository.Accounts.Manager', accounts_manager)
206class TestAccountManagerRealAccount(unittest.TestCase):
207 """Test of the AccountManager API requiring the real Account class.
208
209 You'll need to guarantee other mocks are in place such that the real
210 accounts are not touched.
211 """
212 def setUp(self):
213 self.account_service = mock.Mock()
214
215 def test_account_manager_add_new_account_unsupported(self):
216 fake_account = self.account_service.get_account()
217 fake_account.get_provider_name.return_value = 'no service'
218 manager = AccountManager()
219 with LogMock('friends.utils.account') as log_mock:
220 manager._add_new_account(self.account_service)
221 log_contents = log_mock.empty(trim=False)
222 self.assertNotIn('no service', manager._accounts)
223 self.assertEqual(log_contents, 'Unsupported protocol: no service\n')
224146
=== modified file 'friends/tests/test_authentication.py'
--- friends/tests/test_authentication.py 2013-02-05 01:11:35 +0000
+++ friends/tests/test_authentication.py 2013-03-25 10:33:12 +0000
@@ -27,7 +27,7 @@
27import unittest27import unittest
2828
29from friends.utils.authentication import Authentication29from friends.utils.authentication import Authentication
30from friends.tests.mocks import FakeAccount, mock30from friends.tests.mocks import FakeAccount, LogMock, mock
31from friends.errors import AuthorizationError31from friends.errors import AuthorizationError
3232
3333
@@ -43,7 +43,11 @@
43 # error, and user_data arguments. We'll use the parameters43 # error, and user_data arguments. We'll use the parameters
44 # argument as a way to specify whether an error occurred during44 # argument as a way to specify whether an error occurred during
45 # authentication or not.45 # authentication or not.
46 callback(None, self.results, parameters, None)46 callback(
47 None,
48 self.results,
49 parameters if hasattr(parameters, 'message') else None,
50 None)
4751
4852
49class FakeSignon:53class FakeSignon:
@@ -56,59 +60,49 @@
56 results = dict(NoAccessToken='fail')60 results = dict(NoAccessToken='fail')
5761
5862
59class Logger:
60 def __init__(self):
61 self.debug_messages = []
62 self.error_messages = []
63
64 def debug(self, message, *args):
65 self.debug_messages.append(message.format(*args))
66
67 def error(self, message, *args):
68 self.error_messages.append(message.format(*args))
69
70 reset = __init__
71
72
73logger = Logger()
74
75
76class TestAuthentication(unittest.TestCase):63class TestAuthentication(unittest.TestCase):
77 """Test authentication."""64 """Test authentication."""
7865
79 def setUp(self):66 def setUp(self):
67 self.log_mock = LogMock('friends.utils.authentication')
80 self.account = FakeAccount()68 self.account = FakeAccount()
81 self.account.auth.id = 'my id'69 self.account.auth.get_credentials_id = lambda *ignore: 'my id'
82 self.account.auth.method = 'some method'70 self.account.auth.get_method = lambda *ignore: 'some method'
83 self.account.auth.parameters = 'change me'71 self.account.auth.get_parameters = lambda *ignore: 'change me'
84 self.account.auth.mechanism = ['whatever']72 self.account.auth.get_mechanism = lambda *ignore: 'whatever'
85 logger.reset()73
8674 def tearDown(self):
87 @mock.patch('friends.utils.authentication.log', logger)75 self.log_mock.stop()
76
77 @mock.patch('friends.utils.authentication.manager')
88 @mock.patch('friends.utils.authentication.Signon', FakeSignon)78 @mock.patch('friends.utils.authentication.Signon', FakeSignon)
89 def test_successful_login(self):79 @mock.patch('friends.utils.authentication.Accounts')
80 def test_successful_login(self, accounts, *mocks):
90 # Prevent an error in the callback.81 # Prevent an error in the callback.
91 self.account.auth.parameters = False82 accounts.AccountService.new().get_auth_data(
92 authenticator = Authentication(self.account)83 ).get_parameters.return_value = False
84 authenticator = Authentication(self.account.id)
93 reply = authenticator.login()85 reply = authenticator.login()
94 self.assertEqual(reply, dict(AccessToken='auth reply'))86 self.assertEqual(reply, dict(AccessToken='auth reply'))
95 self.assertEqual(logger.debug_messages, ['Login completed'])87 self.assertEqual(self.log_mock.empty(), 'Login completed\n')
96 self.assertEqual(logger.error_messages, [])
9788
98 @mock.patch('friends.utils.authentication.log', logger)89 @mock.patch('friends.utils.authentication.manager')
90 @mock.patch('friends.utils.authentication.Accounts')
99 @mock.patch('friends.utils.authentication.Signon', FailingSignon)91 @mock.patch('friends.utils.authentication.Signon', FailingSignon)
100 def test_missing_access_token(self):92 def test_missing_access_token(self, *mocks):
101 # Prevent an error in the callback.93 # Prevent an error in the callback.
102 self.account.auth.parameters = False94 self.account.auth.get_parameters = lambda *ignore: False
103 authenticator = Authentication(self.account)95 authenticator = Authentication(self.account.id)
104 self.assertRaises(AuthorizationError, authenticator.login)96 self.assertRaises(AuthorizationError, authenticator.login)
10597
106 @mock.patch('friends.utils.authentication.log', logger)98 @mock.patch('friends.utils.authentication.manager')
107 @mock.patch('friends.utils.authentication.Signon', FakeSignon)99 @mock.patch('friends.utils.authentication.Signon', FakeSignon)
108 def test_failed_login(self):100 @mock.patch('friends.utils.authentication.Accounts')
101 def test_failed_login(self, accounts, *mocks):
109 # Trigger an error in the callback.102 # Trigger an error in the callback.
110 class Error:103 class Error:
111 message = 'who are you?'104 message = 'who are you?'
112 self.account.auth.parameters = Error105 accounts.AccountService.new(
113 authenticator = Authentication(self.account)106 ).get_auth_data().get_parameters.return_value = Error
107 authenticator = Authentication(self.account.id)
114 self.assertRaises(AuthorizationError, authenticator.login)108 self.assertRaises(AuthorizationError, authenticator.login)
115109
=== modified file 'friends/tests/test_dispatcher.py'
--- friends/tests/test_dispatcher.py 2013-03-20 01:19:39 +0000
+++ friends/tests/test_dispatcher.py 2013-03-25 10:33:12 +0000
@@ -38,13 +38,13 @@
38 """Test the dispatcher's ability to dispatch."""38 """Test the dispatcher's ability to dispatch."""
3939
40 @mock.patch('dbus.service.BusName')40 @mock.patch('dbus.service.BusName')
41 @mock.patch('friends.service.dispatcher.AccountManager')41 @mock.patch('friends.service.dispatcher.find_accounts')
42 @mock.patch('friends.service.dispatcher.Dispatcher.Refresh')
43 @mock.patch('dbus.service.Object.__init__')42 @mock.patch('dbus.service.Object.__init__')
44 def setUp(self, *mocks):43 def setUp(self, *mocks):
45 self.log_mock = LogMock('friends.service.dispatcher',44 self.log_mock = LogMock('friends.service.dispatcher',
46 'friends.utils.account')45 'friends.utils.account')
47 self.dispatcher = Dispatcher(mock.Mock(), mock.Mock())46 self.dispatcher = Dispatcher(mock.Mock(), mock.Mock())
47 self.dispatcher.accounts = {}
4848
49 def tearDown(self):49 def tearDown(self):
50 self.log_mock.stop()50 self.log_mock.stop()
@@ -53,12 +53,12 @@
53 def test_refresh(self, threading_mock):53 def test_refresh(self, threading_mock):
54 account = mock.Mock()54 account = mock.Mock()
55 threading_mock.activeCount.return_value = 155 threading_mock.activeCount.return_value = 1
56 self.dispatcher.account_manager = mock.Mock()56 self.dispatcher.accounts = mock.Mock()
57 self.dispatcher.account_manager.get_all.return_value = [account]57 self.dispatcher.accounts.values.return_value = [account]
5858
59 self.assertIsNone(self.dispatcher.Refresh())59 self.assertIsNone(self.dispatcher.Refresh())
6060
61 self.dispatcher.account_manager.get_all.assert_called_once_with()61 self.dispatcher.accounts.values.assert_called_once_with()
62 account.protocol.assert_called_once_with('receive')62 account.protocol.assert_called_once_with('receive')
6363
64 self.assertEqual(self.log_mock.empty(),64 self.assertEqual(self.log_mock.empty(),
@@ -75,12 +75,11 @@
75 def test_do(self):75 def test_do(self):
76 account = mock.Mock()76 account = mock.Mock()
77 account.id = '345'77 account.id = '345'
78 self.dispatcher.account_manager = mock.Mock()78 self.dispatcher.accounts = mock.Mock()
79 self.dispatcher.account_manager.get.return_value = account79 self.dispatcher.accounts.get.return_value = account
8080
81 self.dispatcher.Do('like', '345', '23346356767354626')81 self.dispatcher.Do('like', '345', '23346356767354626')
82 self.dispatcher.account_manager.get.assert_called_once_with(82 self.dispatcher.accounts.get.assert_called_once_with(345)
83 '345')
84 account.protocol.assert_called_once_with(83 account.protocol.assert_called_once_with(
85 'like', '23346356767354626', success=STUB, failure=STUB)84 'like', '23346356767354626', success=STUB, failure=STUB)
8685
@@ -92,11 +91,11 @@
9291
93 def test_failing_do(self):92 def test_failing_do(self):
94 account = mock.Mock()93 account = mock.Mock()
95 self.dispatcher.account_manager = mock.Mock()94 self.dispatcher.accounts = mock.Mock()
96 self.dispatcher.account_manager.get.return_value = None95 self.dispatcher.accounts.get.return_value = None
9796
98 self.dispatcher.Do('unlike', '6', '23346356767354626')97 self.dispatcher.Do('unlike', '6', '23346356767354626')
99 self.dispatcher.account_manager.get.assert_called_once_with('6')98 self.dispatcher.accounts.get.assert_called_once_with(6)
100 self.assertEqual(account.protocol.call_count, 0)99 self.assertEqual(account.protocol.call_count, 0)
101100
102 self.assertEqual(self.log_mock.empty(),101 self.assertEqual(self.log_mock.empty(),
@@ -111,15 +110,15 @@
111 account3 = mock.Mock()110 account3 = mock.Mock()
112 account2.send_enabled = False111 account2.send_enabled = False
113112
114 self.dispatcher.account_manager = mock.Mock()113 self.dispatcher.accounts = mock.Mock()
115 self.dispatcher.account_manager.get_all.return_value = [114 self.dispatcher.accounts.values.return_value = [
116 account1,115 account1,
117 account2,116 account2,
118 account3,117 account3,
119 ]118 ]
120119
121 self.dispatcher.SendMessage('Howdy friends!')120 self.dispatcher.SendMessage('Howdy friends!')
122 self.dispatcher.account_manager.get_all.assert_called_once_with()121 self.dispatcher.accounts.values.assert_called_once_with()
123 account1.protocol.assert_called_once_with(122 account1.protocol.assert_called_once_with(
124 'send', 'Howdy friends!', success=STUB, failure=STUB)123 'send', 'Howdy friends!', success=STUB, failure=STUB)
125 account3.protocol.assert_called_once_with(124 account3.protocol.assert_called_once_with(
@@ -128,11 +127,11 @@
128127
129 def test_send_reply(self):128 def test_send_reply(self):
130 account = mock.Mock()129 account = mock.Mock()
131 self.dispatcher.account_manager = mock.Mock()130 self.dispatcher.accounts = mock.Mock()
132 self.dispatcher.account_manager.get.return_value = account131 self.dispatcher.accounts.get.return_value = account
133132
134 self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')133 self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')
135 self.dispatcher.account_manager.get.assert_called_once_with('2')134 self.dispatcher.accounts.get.assert_called_once_with(2)
136 account.protocol.assert_called_once_with(135 account.protocol.assert_called_once_with(
137 'send_thread', 'objid', '[Hilarious Response]',136 'send_thread', 'objid', '[Hilarious Response]',
138 success=STUB, failure=STUB)137 success=STUB, failure=STUB)
@@ -145,11 +144,11 @@
145144
146 def test_send_reply_failed(self):145 def test_send_reply_failed(self):
147 account = mock.Mock()146 account = mock.Mock()
148 self.dispatcher.account_manager = mock.Mock()147 self.dispatcher.accounts = mock.Mock()
149 self.dispatcher.account_manager.get.return_value = None148 self.dispatcher.accounts.get.return_value = None
150149
151 self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')150 self.dispatcher.SendReply('2', 'objid', '[Hilarious Response]')
152 self.dispatcher.account_manager.get.assert_called_once_with('2')151 self.dispatcher.accounts.get.assert_called_once_with(2)
153 self.assertEqual(account.protocol.call_count, 0)152 self.assertEqual(account.protocol.call_count, 0)
154153
155 self.assertEqual(self.log_mock.empty(),154 self.assertEqual(self.log_mock.empty(),
@@ -161,8 +160,8 @@
161160
162 def test_upload_async(self):161 def test_upload_async(self):
163 account = mock.Mock()162 account = mock.Mock()
164 self.dispatcher.account_manager = mock.Mock()163 self.dispatcher.accounts = mock.Mock()
165 self.dispatcher.account_manager.get.return_value = account164 self.dispatcher.accounts.get.return_value = account
166165
167 success = mock.Mock()166 success = mock.Mock()
168 failure = mock.Mock()167 failure = mock.Mock()
@@ -172,7 +171,7 @@
172 'A thousand words',171 'A thousand words',
173 success=success,172 success=success,
174 failure=failure)173 failure=failure)
175 self.dispatcher.account_manager.get.assert_called_once_with('2')174 self.dispatcher.accounts.get.assert_called_once_with(2)
176 account.protocol.assert_called_once_with(175 account.protocol.assert_called_once_with(
177 'upload',176 'upload',
178 'file://path/to/image.png',177 'file://path/to/image.png',
179178
=== modified file 'friends/tests/test_facebook.py'
--- friends/tests/test_facebook.py 2013-03-16 00:54:45 +0000
+++ friends/tests/test_facebook.py 2013-03-25 10:33:12 +0000
@@ -60,11 +60,13 @@
60 ['contacts', 'delete', 'home', 'like', 'receive', 'search', 'send',60 ['contacts', 'delete', 'home', 'like', 'receive', 'search', 'send',
61 'send_thread', 'unlike', 'upload', 'wall'])61 'send_thread', 'unlike', 'upload', 'wall'])
6262
63 @mock.patch('friends.utils.authentication.manager')
64 @mock.patch('friends.utils.authentication.Accounts')
63 @mock.patch('friends.utils.authentication.Authentication.login',65 @mock.patch('friends.utils.authentication.Authentication.login',
64 return_value=dict(AccessToken='abc'))66 return_value=dict(AccessToken='abc'))
65 @mock.patch('friends.utils.http.Soup.Message',67 @mock.patch('friends.utils.http.Soup.Message',
66 FakeSoupMessage('friends.tests.data', 'facebook-login.dat'))68 FakeSoupMessage('friends.tests.data', 'facebook-login.dat'))
67 def test_successful_login(self, mock):69 def test_successful_login(self, *mocks):
68 # Test that a successful response from graph.facebook.com returning70 # Test that a successful response from graph.facebook.com returning
69 # the user's data, sets up the account dict correctly.71 # the user's data, sets up the account dict correctly.
70 self.protocol._login()72 self.protocol._login()
@@ -72,14 +74,18 @@
72 self.assertEqual(self.account.user_name, 'Bart Person')74 self.assertEqual(self.account.user_name, 'Bart Person')
73 self.assertEqual(self.account.user_id, '801')75 self.assertEqual(self.account.user_id, '801')
7476
77 @mock.patch('friends.utils.authentication.manager')
78 @mock.patch('friends.utils.authentication.Accounts')
75 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)79 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
76 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')80 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
77 def test_login_unsuccessful_authentication(self, mock):81 def test_login_unsuccessful_authentication(self, *mocks):
78 # The user is not already logged in, but the act of logging in fails.82 # The user is not already logged in, but the act of logging in fails.
79 self.assertRaises(AuthorizationError, self.protocol._login)83 self.assertRaises(AuthorizationError, self.protocol._login)
80 self.assertIsNone(self.account.access_token)84 self.assertIsNone(self.account.access_token)
81 self.assertIsNone(self.account.user_name)85 self.assertIsNone(self.account.user_name)
8286
87 @mock.patch('friends.utils.authentication.manager')
88 @mock.patch('friends.utils.authentication.Accounts')
83 @mock.patch('friends.utils.authentication.Authentication.login',89 @mock.patch('friends.utils.authentication.Authentication.login',
84 return_value=dict(AccessToken='abc'))90 return_value=dict(AccessToken='abc'))
85 @mock.patch('friends.protocols.facebook.Downloader.get_json',91 @mock.patch('friends.protocols.facebook.Downloader.get_json',
@@ -237,9 +243,6 @@
237 def test_home_since_id(self, *mocks):243 def test_home_since_id(self, *mocks):
238 self.account.access_token = 'access'244 self.account.access_token = 'access'
239 self.account.secret_token = 'secret'245 self.account.secret_token = 'secret'
240 self.account.auth.parameters = dict(
241 ConsumerKey='key',
242 ConsumerSecret='secret')
243 self.assertEqual(self.protocol.home(), 12)246 self.assertEqual(self.protocol.home(), 12)
244247
245 with open(self._root.format('facebook_ids'), 'r') as fd:248 with open(self._root.format('facebook_ids'), 'r') as fd:
246249
=== modified file 'friends/tests/test_flickr.py'
--- friends/tests/test_flickr.py 2013-03-20 23:57:40 +0000
+++ friends/tests/test_flickr.py 2013-03-25 10:33:12 +0000
@@ -86,6 +86,8 @@
86 # But also no photos.86 # But also no photos.
87 self.assertEqual(TestModel.get_n_rows(), 0)87 self.assertEqual(TestModel.get_n_rows(), 0)
8888
89 @mock.patch('friends.utils.authentication.manager')
90 @mock.patch('friends.utils.authentication.Accounts')
89 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)91 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
90 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')92 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
91 @mock.patch('friends.utils.http.Soup.Message',93 @mock.patch('friends.utils.http.Soup.Message',
@@ -95,6 +97,8 @@
95 # AccessToken, but this fails.97 # AccessToken, but this fails.
96 self.assertRaises(AuthorizationError, self.protocol.receive)98 self.assertRaises(AuthorizationError, self.protocol.receive)
9799
100 @mock.patch('friends.utils.authentication.manager')
101 @mock.patch('friends.utils.authentication.Accounts')
98 @mock.patch('friends.utils.http.Soup.Message',102 @mock.patch('friends.utils.http.Soup.Message',
99 FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))103 FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat'))
100 @mock.patch('friends.utils.authentication.Authentication.login',104 @mock.patch('friends.utils.authentication.Authentication.login',
@@ -102,7 +106,7 @@
102 user_nsid='bob',106 user_nsid='bob',
103 AccessToken='123',107 AccessToken='123',
104 TokenSecret='abc'))108 TokenSecret='abc'))
105 def test_login_successful_authentication(self, mock):109 def test_login_successful_authentication(self, *mocks):
106 # Logging in required communication with the account service to get an110 # Logging in required communication with the account service to get an
107 # AccessToken, but this fails.111 # AccessToken, but this fails.
108 self.protocol.receive()112 self.protocol.receive()
@@ -131,7 +135,7 @@
131 extras='date_upload,owner_name,icon_server,geo',135 extras='date_upload,owner_name,icon_server,geo',
132 format='json',136 format='json',
133 nojsoncallback='1',137 nojsoncallback='1',
134 api_key='fake',138 api_key='consume',
135 method='flickr.photos.getContactsPhotos',139 method='flickr.photos.getContactsPhotos',
136 ),140 ),
137 headers={})141 headers={})
138142
=== modified file 'friends/tests/test_foursquare.py'
--- friends/tests/test_foursquare.py 2013-03-14 19:14:03 +0000
+++ friends/tests/test_foursquare.py 2013-03-25 10:33:12 +0000
@@ -50,6 +50,8 @@
50 # The set of public features.50 # The set of public features.
51 self.assertEqual(FourSquare.get_features(), ['receive'])51 self.assertEqual(FourSquare.get_features(), ['receive'])
5252
53 @mock.patch('friends.utils.authentication.manager')
54 @mock.patch('friends.utils.authentication.Accounts')
53 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)55 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
54 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')56 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
55 @mock.patch('friends.utils.http.Downloader.get_json',57 @mock.patch('friends.utils.http.Downloader.get_json',
@@ -59,6 +61,8 @@
59 self.assertIsNone(self.account.user_name)61 self.assertIsNone(self.account.user_name)
60 self.assertIsNone(self.account.user_id)62 self.assertIsNone(self.account.user_id)
6163
64 @mock.patch('friends.utils.authentication.manager')
65 @mock.patch('friends.utils.authentication.Accounts')
62 @mock.patch('friends.utils.authentication.Authentication.login',66 @mock.patch('friends.utils.authentication.Authentication.login',
63 return_value=dict(AccessToken='tokeny goodness'))67 return_value=dict(AccessToken='tokeny goodness'))
64 @mock.patch('friends.protocols.foursquare.Downloader.get_json',68 @mock.patch('friends.protocols.foursquare.Downloader.get_json',
6569
=== modified file 'friends/tests/test_identica.py'
--- friends/tests/test_identica.py 2013-03-14 19:14:03 +0000
+++ friends/tests/test_identica.py 2013-03-25 10:33:12 +0000
@@ -53,6 +53,8 @@
53 self.log_mock.stop()53 self.log_mock.stop()
54 shutil.rmtree(self._temp_cache)54 shutil.rmtree(self._temp_cache)
5555
56 @mock.patch('friends.utils.authentication.manager')
57 @mock.patch('friends.utils.authentication.Accounts')
56 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)58 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
57 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')59 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
58 @mock.patch('friends.utils.http.Downloader.get_json',60 @mock.patch('friends.utils.http.Downloader.get_json',
@@ -62,6 +64,8 @@
62 self.assertIsNone(self.account.user_name)64 self.assertIsNone(self.account.user_name)
63 self.assertIsNone(self.account.user_id)65 self.assertIsNone(self.account.user_id)
6466
67 @mock.patch('friends.utils.authentication.manager')
68 @mock.patch('friends.utils.authentication.Accounts')
65 @mock.patch('friends.utils.authentication.Authentication.login',69 @mock.patch('friends.utils.authentication.Authentication.login',
66 return_value=dict(AccessToken='some clever fake data',70 return_value=dict(AccessToken='some clever fake data',
67 TokenSecret='sssssshhh!'))71 TokenSecret='sssssshhh!'))
6872
=== modified file 'friends/tests/test_twitter.py'
--- friends/tests/test_twitter.py 2013-03-19 04:17:58 +0000
+++ friends/tests/test_twitter.py 2013-03-25 10:33:12 +0000
@@ -57,15 +57,19 @@
57 self.log_mock.stop()57 self.log_mock.stop()
58 shutil.rmtree(self._temp_cache)58 shutil.rmtree(self._temp_cache)
5959
60 @mock.patch('friends.utils.authentication.manager')
61 @mock.patch('friends.utils.authentication.Accounts')
60 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)62 @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1)
61 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')63 @mock.patch('friends.utils.authentication.Signon.AuthSession.new')
62 @mock.patch('friends.protocols.twitter.Downloader.get_json',64 @mock.patch('friends.protocols.twitter.Downloader.get_json',
63 return_value=None)65 return_value=None)
64 def test_unsuccessful_authentication(self, dload, login):66 def test_unsuccessful_authentication(self, dload, login, *mocks):
65 self.assertRaises(AuthorizationError, self.protocol._login)67 self.assertRaises(AuthorizationError, self.protocol._login)
66 self.assertIsNone(self.account.user_name)68 self.assertIsNone(self.account.user_name)
67 self.assertIsNone(self.account.user_id)69 self.assertIsNone(self.account.user_id)
6870
71 @mock.patch('friends.utils.authentication.manager')
72 @mock.patch('friends.utils.authentication.Accounts')
69 @mock.patch('friends.utils.authentication.Authentication.login',73 @mock.patch('friends.utils.authentication.Authentication.login',
70 return_value=dict(AccessToken='some clever fake data',74 return_value=dict(AccessToken='some clever fake data',
71 TokenSecret='sssssshhh!',75 TokenSecret='sssssshhh!',
@@ -86,11 +90,12 @@
86 def test_signatures(self, dload):90 def test_signatures(self, dload):
87 self.account.secret_token = 'alpha'91 self.account.secret_token = 'alpha'
88 self.account.access_token = 'omega'92 self.account.access_token = 'omega'
89 self.account.auth.id = 693 self.account.consumer_secret = 'obey'
90 self.account.auth.method = 'oauth2'94 self.account.consumer_key = 'consume'
91 self.account.auth.mechanism = 'HMAC-SHA1'95 self.account.auth.get_credentials_id = lambda *ignore: 6
92 self.account.auth.parameters = dict(ConsumerKey='consume',96 self.account.auth.get_method = lambda *ignore: 'oauth2'
93 ConsumerSecret='obey')97 self.account.auth.get_mechanism = lambda *ignore: 'HMAC-SHA1'
98
94 result = '''\99 result = '''\
95OAuth oauth_nonce="once%20upon%20a%20nonce", \100OAuth oauth_nonce="once%20upon%20a%20nonce", \
96oauth_timestamp="1348690628", \101oauth_timestamp="1348690628", \
@@ -122,9 +127,6 @@
122 def test_home(self, *mocks):127 def test_home(self, *mocks):
123 self.account.access_token = 'access'128 self.account.access_token = 'access'
124 self.account.secret_token = 'secret'129 self.account.secret_token = 'secret'
125 self.account.auth.parameters = dict(
126 ConsumerKey='key',
127 ConsumerSecret='secret')
128 self.assertEqual(0, TestModel.get_n_rows())130 self.assertEqual(0, TestModel.get_n_rows())
129 self.assertEqual(self.protocol.home(), 3)131 self.assertEqual(self.protocol.home(), 3)
130 self.assertEqual(3, TestModel.get_n_rows())132 self.assertEqual(3, TestModel.get_n_rows())
@@ -171,9 +173,6 @@
171 def test_home_since_id(self, *mocks):173 def test_home_since_id(self, *mocks):
172 self.account.access_token = 'access'174 self.account.access_token = 'access'
173 self.account.secret_token = 'secret'175 self.account.secret_token = 'secret'
174 self.account.auth.parameters = dict(
175 ConsumerKey='key',
176 ConsumerSecret='secret')
177 self.assertEqual(self.protocol.home(), 3)176 self.assertEqual(self.protocol.home(), 3)
178177
179 with open(self._root.format('twitter_ids'), 'r') as fd:178 with open(self._root.format('twitter_ids'), 'r') as fd:
@@ -196,9 +195,6 @@
196 self.account.access_token = 'access'195 self.account.access_token = 'access'
197 self.account.secret_token = 'secret'196 self.account.secret_token = 'secret'
198 self.account.user_name = 'oauth_dancer'197 self.account.user_name = 'oauth_dancer'
199 self.account.auth.parameters = dict(
200 ConsumerKey='key',
201 ConsumerSecret='secret')
202 self.assertEqual(0, TestModel.get_n_rows())198 self.assertEqual(0, TestModel.get_n_rows())
203 self.assertEqual(199 self.assertEqual(
204 self.protocol.send('some message'),200 self.protocol.send('some message'),
@@ -399,9 +395,6 @@
399 def test_send_thread_prepend_nick(self, *mocks):395 def test_send_thread_prepend_nick(self, *mocks):
400 self.account.access_token = 'access'396 self.account.access_token = 'access'
401 self.account.secret_token = 'secret'397 self.account.secret_token = 'secret'
402 self.account.auth.parameters = dict(
403 ConsumerKey='key',
404 ConsumerSecret='secret')
405 self.assertEqual(0, TestModel.get_n_rows())398 self.assertEqual(0, TestModel.get_n_rows())
406 self.assertEqual(self.protocol.home(), 3)399 self.assertEqual(self.protocol.home(), 3)
407 self.assertEqual(3, TestModel.get_n_rows())400 self.assertEqual(3, TestModel.get_n_rows())
@@ -703,9 +696,6 @@
703 def test_protocol_rate_limiting(self, time, sleep, login):696 def test_protocol_rate_limiting(self, time, sleep, login):
704 self.account.access_token = 'access'697 self.account.access_token = 'access'
705 self.account.secret_token = 'secret'698 self.account.secret_token = 'secret'
706 self.account.auth.parameters = dict(
707 ConsumerKey='key',
708 ConsumerSecret='secret')
709 # Test rate limiting via the Twitter plugin API.699 # Test rate limiting via the Twitter plugin API.
710 #700 #
711 # The first call doesn't get rate limited.701 # The first call doesn't get rate limited.
712702
=== modified file 'friends/utils/account.py'
--- friends/utils/account.py 2013-03-13 02:03:12 +0000
+++ friends/utils/account.py 2013-03-25 10:33:12 +0000
@@ -17,7 +17,7 @@
1717
18__all__ = [18__all__ = [
19 'Account',19 'Account',
20 'AccountManager',20 'find_accounts',
21 ]21 ]
2222
2323
@@ -28,65 +28,30 @@
2828
29from friends.errors import UnsupportedProtocolError29from friends.errors import UnsupportedProtocolError
30from friends.utils.manager import protocol_manager30from friends.utils.manager import protocol_manager
31from friends.utils.authentication import manager
3132
3233
33log = logging.getLogger(__name__)34log = logging.getLogger(__name__)
3435
3536
36class AccountManager:37def _find_accounts_uoa():
37 """Manage the accounts that we know about."""38 """Consult Ubuntu Online Accounts for the accounts we have."""
3839 accounts = {}
39 def __init__(self):40 for service in manager.get_enabled_account_services():
40 self._accounts = {}
41 # Ask libaccounts for a manager of the microblogging services.
42 # Connect callbacks to the manager so that we can react when accounts
43 # are added or deleted.
44 manager = Accounts.Manager.new_for_service_type('microblogging')
45 manager.connect('enabled-event', self._on_enabled_event)
46 # Add all the currently known accounts.
47 for account_service in manager.get_enabled_account_services():
48 self._add_new_account(account_service)
49 log.info('Accounts found: {}'.format(len(self._accounts)))
50
51 def _get_service(self, manager, account_id):
52 """Instantiate an AccountService and identify it."""
53 account = manager.get_account(account_id)
54 for service in account.list_services():
55 return Accounts.AccountService.new(account, service)
56
57 def _on_enabled_event(self, manager, account_id):
58 """React to new microblogging accounts being enabled or disabled."""
59 account_service = self._get_service(manager, account_id)
60 if account_service is not None and account_service.get_enabled():
61 log.debug('Adding account {}'.format(account_id))
62 account = self._add_new_account(account_service)
63 if account is not None:
64 account.protocol('receive')
65
66 def _add_new_account(self, account_service):
67 try:41 try:
68 new_account = Account(account_service)42 account = Account(service)
69 except UnsupportedProtocolError as error:43 except UnsupportedProtocolError as error:
70 log.info(error)44 log.info(error)
71 else:45 else:
72 self._accounts[new_account.id] = new_account46 accounts[account.id] = account
73 return new_account47 log.info('Accounts found: {}'.format(len(accounts)))
7448 return accounts
75 def get_all(self):49
76 return self._accounts.values()50
7751def find_accounts():
78 def get(self, account_id, default=None):52 # TODO: Implement GOA support, then fill out this method with some
79 return self._accounts.get(int(account_id), default)53 # logic for determining whether to use UOA or GOA.
8054 return _find_accounts_uoa()
81
82class AuthData:
83 """This class serves as a sub-namespace for Account instances."""
84
85 def __init__(self, auth_data):
86 self.id = auth_data.get_credentials_id()
87 self.method = auth_data.get_method()
88 self.mechanism = auth_data.get_mechanism()
89 self.parameters = auth_data.get_parameters()
9055
9156
92class Account:57class Account:
@@ -99,6 +64,8 @@
99 )64 )
10065
101 # Defaults for the known and useful attributes.66 # Defaults for the known and useful attributes.
67 consumer_secret = None
68 consumer_key = None
102 access_token = None69 access_token = None
103 secret_token = None70 secret_token = None
104 send_enabled = None71 send_enabled = None
@@ -108,15 +75,22 @@
108 id = None75 id = None
10976
110 def __init__(self, account_service):77 def __init__(self, account_service):
111 self.account_service = account_service78 self.auth = account_service.get_auth_data()
112 self.auth = AuthData(account_service.get_auth_data())79 if self.auth is not None:
80 auth_params = self.auth.get_parameters()
81 self.consumer_key = auth_params.get('ConsumerKey')
82 self.consumer_secret = auth_params.get('ConsumerSecret')
83 else:
84 raise UnsupportedProtocolError(
85 'This AgAccountService is missing AgAuthData!')
86
113 # The provider in libaccounts should match the name of our protocol.87 # The provider in libaccounts should match the name of our protocol.
114 account = account_service.get_account()88 account = account_service.get_account()
115 self.id = account.id89 self.id = account.id
116 self.protocol_name = account.get_provider_name()90 protocol_name = account.get_provider_name()
117 protocol_class = protocol_manager.protocols.get(self.protocol_name)91 protocol_class = protocol_manager.protocols.get(protocol_name)
118 if protocol_class is None:92 if protocol_class is None:
119 raise UnsupportedProtocolError(self.protocol_name)93 raise UnsupportedProtocolError(protocol_name)
120 self.protocol = protocol_class(self)94 self.protocol = protocol_class(self)
121 # Connect responders to changes in the account information.95 # Connect responders to changes in the account information.
122 account_service.connect('changed', self._on_account_changed, account)96 account_service.connect('changed', self._on_account_changed, account)
@@ -152,24 +126,11 @@
152 while True:126 while True:
153 success, key, value = settings.next()127 success, key, value = settings.next()
154 if success:128 if success:
155 log.debug('{} got {}: {}'.format(self.id, key, value))129 log.debug('{} ({}) got {}: {}'.format(
130 self.protocol._Name, self.id, key, value))
156 # Testing for tuple membership makes this easy to expand131 # Testing for tuple membership makes this easy to expand
157 # later, if necessary.132 # later, if necessary.
158 if key in Account._LIBACCOUNTS_PROPERTIES:133 if key in Account._LIBACCOUNTS_PROPERTIES:
159 setattr(self, key, value)134 setattr(self, key, value)
160 else:135 else:
161 break136 break
162
163 @property
164 def enabled(self):
165 return self.account_service.get_enabled()
166
167 def __eq__(self, other):
168 if other is None:
169 return False
170 return self.account_service == other.account_service
171
172 def __ne__(self, other):
173 if other is None:
174 return True
175 return self.account_service != other.account_service
176137
=== modified file 'friends/utils/authentication.py'
--- friends/utils/authentication.py 2013-03-13 17:34:26 +0000
+++ friends/utils/authentication.py 2013-03-25 10:33:12 +0000
@@ -23,7 +23,7 @@
23import logging23import logging
24import time24import time
2525
26from gi.repository import GObject, Signon26from gi.repository import GObject, Accounts, Signon
2727
28from friends.errors import AuthorizationError28from friends.errors import AuthorizationError
2929
@@ -37,17 +37,30 @@
37LOGIN_TIMEOUT = 30 # Currently this is measured in half-seconds.37LOGIN_TIMEOUT = 30 # Currently this is measured in half-seconds.
3838
3939
40# Yes, this is not the most logical place to instantiate this, but I
41# couldn't do it in account.py due to cyclical import dependencies.
42manager = Accounts.Manager.new_for_service_type('microblogging')
43
44
40class Authentication:45class Authentication:
41 def __init__(self, account):46 def __init__(self, account_id):
42 self.account = account47 self.account_id = account_id
48 account = manager.get_account(account_id)
49 service = account.list_services()[0]
50 self.auth = Accounts.AccountService.new(
51 account, service).get_auth_data()
43 self._reply = None52 self._reply = None
4453
45 def login(self):54 def login(self):
46 auth = self.account.auth55 auth = self.auth
47 self.auth_session = Signon.AuthSession.new(auth.id, auth.method)56 self.auth_session = Signon.AuthSession.new(
57 auth.get_credentials_id(),
58 auth.get_method())
48 self.auth_session.process(59 self.auth_session.process(
49 auth.parameters, auth.mechanism,60 auth.get_parameters(),
50 self._login_cb, None)61 auth.get_mechanism(),
62 self._login_cb,
63 None)
51 timeout = LOGIN_TIMEOUT64 timeout = LOGIN_TIMEOUT
52 while self._reply is None and timeout > 0:65 while self._reply is None and timeout > 0:
53 # We're building a synchronous API on top of an inherently66 # We're building a synchronous API on top of an inherently
@@ -56,17 +69,17 @@
56 time.sleep(0.5)69 time.sleep(0.5)
57 timeout -= 170 timeout -= 1
58 if self._reply is None:71 if self._reply is None:
59 raise AuthorizationError(self.account.id, 'Login timed out.')72 raise AuthorizationError(self.account_id, 'Login timed out.')
60 if 'AccessToken' not in self._reply:73 if 'AccessToken' not in self._reply:
61 raise AuthorizationError(74 raise AuthorizationError(
62 self.account.id,75 self.account_id,
63 'No AccessToken found: {!r}'.format(self._reply))76 'No AccessToken found: {!r}'.format(self._reply))
64 return self._reply77 return self._reply
6578
66 def _login_cb(self, session, reply, error, user_data):79 def _login_cb(self, session, reply, error, user_data):
67 self._reply = reply80 self._reply = reply
68 if error:81 if error:
69 exception = AuthorizationError(self.account.id, error.message)82 exception = AuthorizationError(self.account_id, error.message)
70 # Mardy says this error can happen during normal operation.83 # Mardy says this error can happen during normal operation.
71 if error.message.endswith('userActionFinished error: 10'):84 if error.message.endswith('userActionFinished error: 10'):
72 log.error(str(exception))85 log.error(str(exception))
7386
=== modified file 'friends/utils/base.py'
--- friends/utils/base.py 2013-03-20 01:19:39 +0000
+++ friends/utils/base.py 2013-03-25 10:33:12 +0000
@@ -460,7 +460,7 @@
460 log.debug('{} to {}'.format(460 log.debug('{} to {}'.format(
461 'Re-authenticating' if old_token else 'Logging in', self._Name))461 'Re-authenticating' if old_token else 'Logging in', self._Name))
462462
463 result = Authentication(self._account).login()463 result = Authentication(self._account.id).login()
464464
465 self._account.access_token = result.get('AccessToken')465 self._account.access_token = result.get('AccessToken')
466 self._whoami(result)466 self._whoami(result)
@@ -469,8 +469,8 @@
469 def _get_oauth_headers(self, method, url, data=None, headers=None):469 def _get_oauth_headers(self, method, url, data=None, headers=None):
470 """Basic wrapper around oauthlib that we use for Twitter and Flickr."""470 """Basic wrapper around oauthlib that we use for Twitter and Flickr."""
471 # "Client" == "Consumer" in oauthlib parlance.471 # "Client" == "Consumer" in oauthlib parlance.
472 client_key = self._account.auth.parameters['ConsumerKey']472 client_key = self._account.consumer_key
473 client_secret = self._account.auth.parameters['ConsumerSecret']473 client_secret = self._account.consumer_secret
474474
475 # "resource_owner" == secret and token.475 # "resource_owner" == secret and token.
476 resource_owner_key = self._get_access_token()476 resource_owner_key = self._get_access_token()
477477
=== modified file 'tools/debug_live.py'
--- tools/debug_live.py 2013-03-13 18:05:13 +0000
+++ tools/debug_live.py 2013-03-25 10:33:12 +0000
@@ -30,7 +30,7 @@
30# Print all logs for debugging purposes30# Print all logs for debugging purposes
31initialize(debug=True, console=True)31initialize(debug=True, console=True)
3232
33from friends.utils.account import AccountManager33from friends.utils.account import find_accounts
34from friends.utils.base import initialize_caches, _OperationThread34from friends.utils.base import initialize_caches, _OperationThread
35from friends.utils.model import Model35from friends.utils.model import Model
3636
@@ -53,12 +53,10 @@
5353
54 initialize_caches()54 initialize_caches()
5555
56 Model.connect('row-added', row_added)
57
56 found = False58 found = False
57 a = AccountManager()59 for account in find_accounts().values():
58
59 Model.connect('row-added', row_added)
60
61 for account in a._accounts.values():
62 if account.protocol._name == protocol.lower():60 if account.protocol._name == protocol.lower():
63 found = True61 found = True
64 account.protocol(*args)62 account.protocol(*args)

Subscribers

People subscribed via source and target branches

to all changes: