Merge lp:~robru/friends/simplify-contacts into lp:friends
- simplify-contacts
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Ken VanDine |
Approved revision: | 236 |
Merged at revision: | 225 |
Proposed branch: | lp:~robru/friends/simplify-contacts |
Merge into: | lp:friends |
Diff against target: |
1197 lines (+410/-468) 14 files modified
friends/protocols/facebook.py (+21/-54) friends/protocols/identica.py (+1/-0) friends/protocols/linkedin.py (+24/-32) friends/protocols/twitter.py (+27/-59) friends/tests/test_dispatcher.py (+15/-12) friends/tests/test_facebook.py (+141/-51) friends/tests/test_flickr.py (+2/-1) friends/tests/test_foursquare.py (+2/-1) friends/tests/test_identica.py (+30/-47) friends/tests/test_instagram.py (+2/-1) friends/tests/test_linkedin.py (+41/-71) friends/tests/test_protocols.py (+2/-1) friends/tests/test_twitter.py (+31/-50) friends/utils/base.py (+71/-88) |
To merge this branch: | bzr merge lp:~robru/friends/simplify-contacts |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ken VanDine | Approve | ||
PS Jenkins bot (community) | continuous-integration | Approve | |
Robert Bruce Park | Approve | ||
Review via email: mp+175968@code.launchpad.net |
Commit message
Vast simplification of contact logic.
Description of the change
Great simplification of our messy contacts implementation. Lots of big changes here, but here are some highlights:
* Stop creating VCards, serializing them into strings, and then parsing them back into EDS Contact objects. Just create the Contact directly.
* Stop iterating over all EDS address books, looking for the one with the right display name. Simply request the correct address book by ID the first time. Also cache this instance through the lifetime of the script, so it doesn't need to be polled for continuously.
* Streamline a lot of the _create_contact logic, which was highly reminiscent of the old Gwibber code, where json-ish data structures were being schlepped around and copied from one form into another, for no apparent reason.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:236
http://
Executed test runs:
SUCCESS: http://
Click here to trigger a rebuild:
http://
Ken VanDine (ken-vandine) wrote : | # |
Looks like it works well, thanks!
Preview Diff
1 | === modified file 'friends/protocols/facebook.py' |
2 | --- friends/protocols/facebook.py 2013-07-17 22:15:56 +0000 |
3 | +++ friends/protocols/facebook.py 2013-07-20 03:25:30 +0000 |
4 | @@ -326,68 +326,35 @@ |
5 | else: |
6 | raise FriendsError(str(response)) |
7 | |
8 | - def _fetch_contacts(self): |
9 | - """Retrieve a list of up to 1,000 Facebook friends.""" |
10 | - limit = 1000 |
11 | - access_token = self._get_access_token() |
12 | - url = ME_URL + '/friends' |
13 | - params = dict( |
14 | - access_token=access_token, |
15 | - limit=limit) |
16 | - return self._follow_pagination(url, params, limit) |
17 | - |
18 | - def _fetch_contact(self, contact_id): |
19 | - """Fetch the full, individual contact info.""" |
20 | - access_token = self._get_access_token() |
21 | - url = API_BASE.format(id=contact_id) |
22 | - params = dict(access_token=access_token) |
23 | - return Downloader(url, params).get_json() |
24 | - |
25 | - def _create_contact(self, contact_json): |
26 | - """Build a VCard based on a dict representation of a contact.""" |
27 | - |
28 | - user_id = contact_json.get('id') |
29 | - user_fullname = contact_json.get('name') |
30 | - user_nickname = contact_json.get('username') |
31 | - user_link = contact_json.get('link') |
32 | - gender = contact_json.get('gender') |
33 | - |
34 | - attrs = {} |
35 | - attrs['facebook-id'] = user_id |
36 | - attrs['facebook-name'] = user_fullname |
37 | - attrs['X-URIS'] = user_link |
38 | - attrs['X-FOLKS-WEB-SERVICES-IDS'] = { |
39 | - 'jabber':'-{}@chat.facebook.com'.format(user_id), |
40 | - 'remote-full-name':user_fullname, |
41 | - 'facebook-id': user_id} |
42 | - if gender is not None: |
43 | - attrs['X-GENDER'] = gender |
44 | - |
45 | - return super()._create_contact(user_fullname, user_nickname, attrs) |
46 | - |
47 | @feature |
48 | def contacts(self): |
49 | - contacts = self._fetch_contacts() |
50 | - log.debug('Size of the contacts returned {}'.format(len(contacts))) |
51 | - source = self._get_eds_source() |
52 | + contacts = self._follow_pagination( |
53 | + url=ME_URL + '/friends', |
54 | + params=dict(access_token=self._get_access_token(), limit=1000), |
55 | + limit=1000) |
56 | + log.debug('Found {} contacts'.format(len(contacts))) |
57 | |
58 | for contact in contacts: |
59 | - if self._previously_stored_contact( |
60 | - source, 'facebook-id', contact['id']): |
61 | + contact_id = contact.get('id') |
62 | + if contact_id is None or self._previously_stored_contact(contact_id): |
63 | continue |
64 | - log.debug( |
65 | - 'Fetch full contact info for {} and id {}'.format( |
66 | - contact['name'], contact['id'])) |
67 | - full_contact = self._fetch_contact(contact['id']) |
68 | - eds_contact = self._create_contact(full_contact) |
69 | - self._push_to_eds(eds_contact) |
70 | + full_contact = Downloader( |
71 | + url=API_BASE.format(id=contact_id), |
72 | + params=dict(access_token=self._get_access_token())).get_json() |
73 | + self._push_to_eds({ |
74 | + 'facebook-id': contact_id, |
75 | + 'facebook-name': full_contact.get('name'), |
76 | + 'facebook-nick': full_contact.get('username'), |
77 | + 'X-URIS': full_contact.get('link'), |
78 | + 'X-GENDER': full_contact.get('gender'), |
79 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
80 | + 'jabber': '-{}@chat.facebook.com'.format(contact_id), |
81 | + 'remote-full-name': full_contact.get('name'), |
82 | + 'facebook-id': contact_id, |
83 | + }}) |
84 | |
85 | return len(contacts) |
86 | |
87 | - def delete_contacts(self): |
88 | - source = self._get_eds_source() |
89 | - return self._delete_service_contacts(source) |
90 | - |
91 | |
92 | class PostIdCache(JsonCache): |
93 | """Persist most-recent timestamps as JSON.""" |
94 | |
95 | === modified file 'friends/protocols/identica.py' |
96 | --- friends/protocols/identica.py 2013-04-03 03:47:39 +0000 |
97 | +++ friends/protocols/identica.py 2013-07-20 03:25:30 +0000 |
98 | @@ -39,6 +39,7 @@ |
99 | _favorite = _api_base.format(endpoint='favorites/create/{}') |
100 | _del_favorite = _api_base.format(endpoint='favorites/destroy/{}') |
101 | |
102 | + _user_home = 'https://identi.ca/{user_id}' |
103 | _tweet_permalink = 'http://identi.ca/notice/{tweet_id}' |
104 | |
105 | def _whoami(self, authdata): |
106 | |
107 | === modified file 'friends/protocols/linkedin.py' |
108 | --- friends/protocols/linkedin.py 2013-07-17 22:25:42 +0000 |
109 | +++ friends/protocols/linkedin.py 2013-07-20 03:25:30 +0000 |
110 | @@ -26,12 +26,16 @@ |
111 | from friends.utils.base import Base, feature |
112 | from friends.utils.http import Downloader |
113 | from friends.utils.time import iso8601utc |
114 | -from friends.errors import FriendsError |
115 | |
116 | |
117 | log = logging.getLogger(__name__) |
118 | |
119 | |
120 | +def make_fullname(firstName=None, lastName=None, **ignored): |
121 | + """Converts dict(firstName='Bob', lastName='Loblaw') into 'Bob Loblaw'.""" |
122 | + return ' '.join(name for name in (firstName, lastName) if name) |
123 | + |
124 | + |
125 | class LinkedIn(Base): |
126 | _api_base = ('https://api.linkedin.com/v1/{endpoint}?format=json' + |
127 | '&secure-urls=true&oauth2_access_token={token}') |
128 | @@ -44,7 +48,7 @@ |
129 | token=self._get_access_token()) |
130 | result = Downloader(url).get_json() |
131 | self._account.user_id = result.get('id') |
132 | - self._account.user_name = '{firstName} {lastName}'.format(**result) |
133 | + self._account.user_name = make_fullname(**result) |
134 | |
135 | def _publish_entry(self, entry, stream='messages'): |
136 | """Publish a single update into the Dee.SharedModel.""" |
137 | @@ -52,7 +56,7 @@ |
138 | |
139 | content = entry.get('updateContent', {}) |
140 | person = content.get('person', {}) |
141 | - name = '{firstName} {lastName}'.format(**person) |
142 | + name = make_fullname(**person) |
143 | person_id = person.get('id', '') |
144 | status = person.get('currentStatus') |
145 | picture = person.get('pictureUrl', '') |
146 | @@ -98,40 +102,28 @@ |
147 | """Gather and publish all incoming messages.""" |
148 | return self.home() |
149 | |
150 | - def _create_contact(self, connection_json): |
151 | - """Build a VCard based on a dict representation of a contact.""" |
152 | - user_id = connection_json.get('id', '') |
153 | - |
154 | - user_fullname = '{firstName} {lastName}'.format(**connection_json) |
155 | - user_link = connection_json.get( |
156 | - 'siteStandardProfileRequest', {}).get('url', '') |
157 | - |
158 | - attrs = { 'linkedin-id': user_id, |
159 | - 'linkedin-name': user_fullname, |
160 | - 'X-URIS': user_link } |
161 | - |
162 | - return super()._create_contact(user_fullname, None, attrs) |
163 | - |
164 | @feature |
165 | def contacts(self): |
166 | """Retrieve a list of up to 500 LinkedIn connections.""" |
167 | # http://developer.linkedin.com/documents/connections-api |
168 | - url = self._api_base.format( |
169 | - endpoint='people/~/connections', |
170 | - token=self._get_access_token()) |
171 | - result = Downloader(url).get_json() |
172 | - connections = result.get('values', []) |
173 | - source = self._get_eds_source() |
174 | + connections = Downloader( |
175 | + url=self._api_base.format( |
176 | + endpoint='people/~/connections', |
177 | + token=self._get_access_token()) |
178 | + ).get_json().get('values', []) |
179 | |
180 | for connection in connections: |
181 | - connection_id = connection.get('id') |
182 | - if connection_id != 'private': |
183 | - if not self._previously_stored_contact( |
184 | - source, 'linkedin-id', connection_id): |
185 | - self._push_to_eds(self._create_contact(connection)) |
186 | + connection_id = connection.get('id', 'private') |
187 | + fullname = make_fullname(**connection) |
188 | + if connection_id != 'private' and not self._previously_stored_contact(connection_id): |
189 | + self._push_to_eds({ |
190 | + 'linkedin-id': connection_id, |
191 | + 'linkedin-name': fullname, |
192 | + 'X-URIS': connection.get( |
193 | + 'siteStandardProfileRequest', {}).get('url'), |
194 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
195 | + 'remote-full-name': fullname, |
196 | + 'linkedin-id': connection_id, |
197 | + }}) |
198 | |
199 | return len(connections) |
200 | - |
201 | - def delete_contacts(self): |
202 | - source = self._get_eds_source() |
203 | - return self._delete_service_contacts(source) |
204 | |
205 | === modified file 'friends/protocols/twitter.py' |
206 | --- friends/protocols/twitter.py 2013-07-17 22:15:56 +0000 |
207 | +++ friends/protocols/twitter.py 2013-07-20 03:25:30 +0000 |
208 | @@ -61,7 +61,8 @@ |
209 | _favorite = _api_base.format(endpoint='favorites/create') |
210 | _del_favorite = _api_base.format(endpoint='favorites/destroy') |
211 | |
212 | - _tweet_permalink = 'https://twitter.com/{user_id}/status/{tweet_id}' |
213 | + _user_home = 'https://twitter.com/{user_id}' |
214 | + _tweet_permalink = _user_home + '/status/{tweet_id}' |
215 | |
216 | def __init__(self, account): |
217 | super().__init__(account) |
218 | @@ -347,68 +348,35 @@ |
219 | self._publish_tweet(tweet, stream='search/{}'.format(query)) |
220 | return self._get_n_rows() |
221 | |
222 | -# https://dev.twitter.com/docs/api/1.1/get/friends/ids |
223 | - def _getfriendsids(self): |
224 | - """Get a list of the twitter id's of our twitter friends.""" |
225 | - url = self._api_base.format(endpoint="friends/ids") |
226 | - response = self._get_url(url) |
227 | - |
228 | - try: |
229 | |
230 | - return response["ids"] |
231 | - except TypeError: |
232 | - # Identica |
233 | - return response |
234 | - |
235 | -# https://dev.twitter.com/docs/api/1.1/get/users/show |
236 | - def _showuser(self, uid): |
237 | - """Get all the information about a twitter user.""" |
238 | - url = self._api_base.format( |
239 | - endpoint='users/show') + '?user_id={}'.format(uid) |
240 | - return self._get_url(url) |
241 | - |
242 | - def _create_contact(self, userdata): |
243 | - """Build a VCard based on a dict representation of a contact.""" |
244 | - |
245 | - if userdata.get('error'): |
246 | - raise FriendsError(userdata) |
247 | - |
248 | - user_fullname = userdata['name'] |
249 | - user_nickname = userdata['screen_name'] |
250 | - |
251 | - attrs = {} |
252 | - attrs['twitter-id'] = str(userdata['id']) |
253 | - attrs['twitter-name'] = user_fullname |
254 | - attrs['X-URIS'] = 'https://twitter.com/{}'.format(user_nickname) |
255 | - attrs['X-FOLKS-WEB-SERVICES-IDS'] = { |
256 | - 'remote-full-name': user_fullname, |
257 | - 'twitter-id': str(userdata['id']), |
258 | - } |
259 | - |
260 | - return super()._create_contact(user_fullname, user_nickname, attrs) |
261 | - |
262 | @feature |
263 | def contacts(self): |
264 | - contacts = self._getfriendsids() |
265 | - log.debug('Size of the contacts returned {}'.format(len(contacts))) |
266 | - source = self._get_eds_source() |
267 | - |
268 | - for contact in contacts: |
269 | - twitterid = str(contact) |
270 | - if self._previously_stored_contact(source, 'twitter-id', twitterid): |
271 | - continue |
272 | - full_contact = self._showuser(twitterid) |
273 | - try: |
274 | - eds_contact = self._create_contact(full_contact) |
275 | - except FriendsError: |
276 | - continue |
277 | - self._push_to_eds(eds_contact) |
278 | + # https://dev.twitter.com/docs/api/1.1/get/friends/ids |
279 | + contacts = self._get_url(self._api_base.format(endpoint='friends/ids')) |
280 | + # Twitter uses a dict with 'ids' key, Identica returns the ids directly. |
281 | + with ignored(TypeError): |
282 | + contacts = contacts['ids'] |
283 | + |
284 | + log.debug('Found {} contacts'.format(len(contacts))) |
285 | + |
286 | + for contact_id in contacts: |
287 | + contact_id = str(contact_id) |
288 | + if not self._previously_stored_contact(contact_id): |
289 | + # https://dev.twitter.com/docs/api/1.1/get/users/show |
290 | + full_contact = self._get_url(url=self._api_base.format( |
291 | + endpoint='users/show') + '?user_id=' + contact_id) |
292 | + user_fullname = full_contact.get('name') |
293 | + user_nickname = full_contact.get('screen_name') |
294 | + self._push_to_eds({ |
295 | + '{}-id'.format(self._name): contact_id, |
296 | + '{}-name'.format(self._name): user_fullname, |
297 | + '{}-nick'.format(self._name): user_nickname, |
298 | + 'X-URIS': self._user_home.format(user_id=user_nickname), |
299 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
300 | + 'remote-full-name': user_fullname, |
301 | + '{}-id'.format(self._name): contact_id, |
302 | + }}) |
303 | return len(contacts) |
304 | |
305 | - def delete_contacts(self): |
306 | - source = self._get_eds_source() |
307 | - return self._delete_service_contacts(source) |
308 | - |
309 | |
310 | class TweetIdCache(JsonCache): |
311 | """Persist most-recent tweet_ids as JSON.""" |
312 | |
313 | === modified file 'friends/tests/test_dispatcher.py' |
314 | --- friends/tests/test_dispatcher.py 2013-04-03 04:04:56 +0000 |
315 | +++ friends/tests/test_dispatcher.py 2013-07-20 03:25:30 +0000 |
316 | @@ -184,23 +184,26 @@ |
317 | |
318 | def test_get_features(self): |
319 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('facebook')), |
320 | - ['contacts', 'delete', 'home', 'like', 'receive', |
321 | - 'search', 'send', 'send_thread', 'unlike', 'upload', |
322 | - 'wall']) |
323 | + ['contacts', 'delete', 'delete_contacts', |
324 | + 'home', 'like', 'receive', 'search', 'send', |
325 | + 'send_thread', 'unlike', 'upload', 'wall']) |
326 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('twitter')), |
327 | - ['contacts', 'delete', 'follow', 'home', 'like', |
328 | - 'list', 'lists', 'mentions', 'private', 'receive', |
329 | - 'retweet', 'search', 'send', 'send_private', |
330 | - 'send_thread', 'tag', 'unfollow', 'unlike', 'user']) |
331 | + ['contacts', 'delete', 'delete_contacts', |
332 | + 'follow', 'home', 'like', 'list', 'lists', |
333 | + 'mentions', 'private', 'receive', 'retweet', |
334 | + 'search', 'send', 'send_private', |
335 | + 'send_thread', 'tag', 'unfollow', 'unlike', |
336 | + 'user']) |
337 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('identica')), |
338 | - ['contacts', 'delete', 'follow', 'home', 'like', |
339 | - 'mentions', 'private', 'receive', 'retweet', |
340 | - 'search', 'send', 'send_private', 'send_thread', |
341 | + ['contacts', 'delete', 'delete_contacts', |
342 | + 'follow', 'home', 'like', 'mentions', |
343 | + 'private', 'receive', 'retweet', 'search', |
344 | + 'send', 'send_private', 'send_thread', |
345 | 'unfollow', 'unlike', 'user']) |
346 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('flickr')), |
347 | - ['receive', 'upload']) |
348 | + ['delete_contacts', 'receive', 'upload']) |
349 | self.assertEqual(json.loads(self.dispatcher.GetFeatures('foursquare')), |
350 | - ['receive']) |
351 | + ['delete_contacts', 'receive']) |
352 | |
353 | @mock.patch('friends.service.dispatcher.logging') |
354 | def test_urlshorten_already_shortened(self, logging_mock): |
355 | |
356 | === modified file 'friends/tests/test_facebook.py' |
357 | --- friends/tests/test_facebook.py 2013-07-17 22:15:56 +0000 |
358 | +++ friends/tests/test_facebook.py 2013-07-20 03:25:30 +0000 |
359 | @@ -26,13 +26,13 @@ |
360 | import unittest |
361 | import shutil |
362 | |
363 | -from gi.repository import GLib |
364 | +from gi.repository import GLib, EDataServer |
365 | from pkg_resources import resource_filename |
366 | |
367 | from friends.protocols.facebook import Facebook |
368 | from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
369 | from friends.tests.mocks import TestModel, mock |
370 | -from friends.tests.mocks import EDSBookClientMock, EDSSource, EDSRegistry |
371 | +from friends.tests.mocks import EDSBookClientMock, EDSRegistry |
372 | from friends.errors import ContactsError, FriendsError, AuthorizationError |
373 | from friends.utils.cache import JsonCache |
374 | |
375 | @@ -57,8 +57,9 @@ |
376 | def test_features(self): |
377 | # The set of public features. |
378 | self.assertEqual(Facebook.get_features(), |
379 | - ['contacts', 'delete', 'home', 'like', 'receive', 'search', 'send', |
380 | - 'send_thread', 'unlike', 'upload', 'wall']) |
381 | + ['contacts', 'delete', 'delete_contacts', 'home', |
382 | + 'like', 'receive', 'search', 'send', 'send_thread', |
383 | + 'unlike', 'upload', 'wall']) |
384 | |
385 | @mock.patch('friends.utils.authentication.manager') |
386 | @mock.patch('friends.utils.authentication.Accounts') |
387 | @@ -474,23 +475,66 @@ |
388 | params=dict(access_token='face')) |
389 | unpublish.assert_called_once_with('post_id') |
390 | |
391 | - @mock.patch('friends.utils.http.Soup.Message', |
392 | - FakeSoupMessage('friends.tests.data', 'facebook-contacts.dat')) |
393 | - @mock.patch('friends.protocols.facebook.Facebook._login', |
394 | - return_value=True) |
395 | - def test_fetch_contacts(self, *mocks): |
396 | - # Receive the users friends. |
397 | - results = self.protocol._fetch_contacts() |
398 | - self.assertEqual(len(results), 8) |
399 | - self.assertEqual(results[7]['name'], 'John Smith') |
400 | - self.assertEqual(results[7]['id'], '444444') |
401 | + @mock.patch('friends.protocols.facebook.Downloader') |
402 | + def test_contacts(self, downloader): |
403 | + downloader().get_json.return_value = dict( |
404 | + name='Joe Blow', username='jblow', link='example.com', gender='male') |
405 | + downloader.reset_mock() |
406 | + self.protocol._get_access_token = mock.Mock(return_value='broken') |
407 | + follow = self.protocol._follow_pagination = mock.Mock( |
408 | + return_value=[dict(id='contact1'), dict(id='contact2')]) |
409 | + prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False) |
410 | + push = self.protocol._push_to_eds = mock.Mock() |
411 | + self.assertEqual(self.protocol.contacts(), 2) |
412 | + follow.assert_called_once_with( |
413 | + params={'access_token': 'broken', 'limit': 1000}, |
414 | + url='https://graph.facebook.com/me/friends', |
415 | + limit=1000) |
416 | + self.assertEqual( |
417 | + prev.call_args_list, |
418 | + [mock.call('contact1'), mock.call('contact2')]) |
419 | + self.assertEqual( |
420 | + downloader.call_args_list, |
421 | + [mock.call(url='https://graph.facebook.com/contact1', |
422 | + params={'access_token': 'broken'}), |
423 | + mock.call(url='https://graph.facebook.com/contact2', |
424 | + params={'access_token': 'broken'})]) |
425 | + self.assertEqual( |
426 | + push.call_args_list, |
427 | + [mock.call({ |
428 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
429 | + 'jabber': '-contact1@chat.facebook.com', |
430 | + 'facebook-id': 'contact1', |
431 | + 'remote-full-name': 'Joe Blow'}, |
432 | + 'X-GENDER': 'male', |
433 | + 'facebook-name': 'Joe Blow', |
434 | + 'facebook-id': 'contact1', |
435 | + 'X-URIS': 'example.com', |
436 | + 'facebook-nick': 'jblow'}), |
437 | + mock.call({ |
438 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
439 | + 'jabber': '-contact2@chat.facebook.com', |
440 | + 'facebook-id': 'contact2', |
441 | + 'remote-full-name': 'Joe Blow'}, |
442 | + 'X-GENDER': 'male', |
443 | + 'facebook-name': 'Joe Blow', |
444 | + 'facebook-id': 'contact2', |
445 | + 'X-URIS': 'example.com', |
446 | + 'facebook-nick': 'jblow'})]) |
447 | |
448 | def test_create_contact(self, *mocks): |
449 | # Receive the users friends. |
450 | - bare_contact = {'name': 'Lucy Baron', |
451 | - 'id': '555555555', |
452 | - 'username': 'lucy.baron5', |
453 | - 'link': 'http:www.facebook.com/lucy.baron5'} |
454 | + bare_contact = { |
455 | + 'facebook-id': '555555555', |
456 | + 'facebook-name': 'Lucy Baron', |
457 | + 'facebook-nick': 'lucy.baron5', |
458 | + 'X-URIS': 'http:www.facebook.com/lucy.baron5', |
459 | + 'X-GENDER': 'female', |
460 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
461 | + 'jabber': '-555555555@chat.facebook.com', |
462 | + 'remote-full-name': 'Lucy Baron', |
463 | + 'facebook-id': '555555555', |
464 | + }} |
465 | eds_contact = self.protocol._create_contact(bare_contact) |
466 | facebook_id_attr = eds_contact.get_attribute('facebook-id') |
467 | self.assertEqual(facebook_id_attr.get_value(), '555555555') |
468 | @@ -521,7 +565,7 @@ |
469 | # Finally test to ensure all key value pairs were tested |
470 | self.assertTrue(test_jabber and test_remote_name and test_facebook_id) |
471 | |
472 | - @mock.patch('friends.utils.base.Base._get_eds_source', |
473 | + @mock.patch('friends.utils.base.Base._prepare_eds_connections', |
474 | return_value=True) |
475 | @mock.patch('gi.repository.EBook.BookClient.new', |
476 | return_value=EDSBookClientMock()) |
477 | @@ -530,54 +574,100 @@ |
478 | 'id': '555555555', |
479 | 'username': 'lucy.baron5', |
480 | 'link': 'http:www.facebook.com/lucy.baron5'} |
481 | - eds_contact = self.protocol._create_contact(bare_contact) |
482 | self.protocol._address_book = 'test-address-book' |
483 | + client = self.protocol._book_client = mock.Mock() |
484 | + client.add_contact_sync.return_value = True |
485 | # Implicitely fail test if the following raises any exceptions |
486 | - self.protocol._push_to_eds(eds_contact) |
487 | + self.protocol._push_to_eds(bare_contact) |
488 | |
489 | - @mock.patch('friends.utils.base.Base._get_eds_source', |
490 | - return_value=None) |
491 | - @mock.patch('friends.utils.base.Base._create_eds_source', |
492 | + @mock.patch('friends.utils.base.Base._prepare_eds_connections', |
493 | return_value=None) |
494 | def test_unsuccessfull_push_to_eds(self, *mocks): |
495 | bare_contact = {'name': 'Lucy Baron', |
496 | 'id': '555555555', |
497 | 'username': 'lucy.baron5', |
498 | 'link': 'http:www.facebook.com/lucy.baron5'} |
499 | - eds_contact = self.protocol._create_contact(bare_contact) |
500 | self.protocol._address_book = 'test-address-book' |
501 | + client = self.protocol._book_client = mock.Mock() |
502 | + client.add_contact_sync.return_value = False |
503 | self.assertRaises( |
504 | ContactsError, |
505 | self.protocol._push_to_eds, |
506 | - eds_contact, |
507 | + bare_contact, |
508 | ) |
509 | |
510 | - @mock.patch('gi.repository.EDataServer.Source.new', |
511 | - return_value=EDSSource('foo', 'bar')) |
512 | - def test_create_eds_source(self, *mocks): |
513 | - regmock = self.protocol._source_registry = mock.Mock() |
514 | - regmock.ref_source = lambda x: x |
515 | - |
516 | - result = self.protocol._create_eds_source() |
517 | - self.assertEqual(result, 'test-source-uid') |
518 | - |
519 | - @mock.patch('gi.repository.EBook.BookClient.new', |
520 | + @mock.patch('gi.repository.EBook.BookClient.connect_sync', |
521 | return_value=EDSBookClientMock()) |
522 | def test_successful_previously_stored_contact(self, *mocks): |
523 | - result = self.protocol._previously_stored_contact( |
524 | - True, 'facebook-id', '11111') |
525 | + result = self.protocol._previously_stored_contact('11111') |
526 | self.assertEqual(result, True) |
527 | |
528 | - def test_successful_get_eds_source(self, *mocks): |
529 | - class FakeSource: |
530 | - def get_display_name(self): |
531 | - return 'test-facebook-contacts' |
532 | - def get_uid(self): |
533 | - return 1345245 |
534 | - |
535 | - reg_mock = self.protocol._source_registry = mock.Mock() |
536 | - reg_mock.list_sources.return_value = [FakeSource()] |
537 | - reg_mock.ref_source = lambda x: x |
538 | - self.protocol._address_book = 'test-facebook-contacts' |
539 | - result = self.protocol._get_eds_source() |
540 | - self.assertEqual(result, 1345245) |
541 | + def test_first_run_prepare_eds_connections(self): |
542 | + self.protocol._name = 'testsuite' |
543 | + self.assertIsNone(self.protocol._address_book_name) |
544 | + self.assertIsNone(self.protocol._eds_source_registry) |
545 | + self.assertIsNone(self.protocol._eds_source) |
546 | + self.assertIsNone(self.protocol._book_client) |
547 | + self.protocol._prepare_eds_connections() |
548 | + self.assertEqual(self.protocol._address_book_name, |
549 | + 'friends-testsuite-contacts') |
550 | + self.assertEqual(self.protocol._eds_source.get_display_name(), |
551 | + 'friends-testsuite-contacts') |
552 | + self.assertEqual(self.protocol._eds_source.get_uid(), |
553 | + 'friends-testsuite-contacts') |
554 | + self.protocol.delete_contacts() |
555 | + |
556 | + @mock.patch('gi.repository.EDataServer.SourceRegistry') |
557 | + @mock.patch('gi.repository.EDataServer.Source') |
558 | + @mock.patch('gi.repository.EBook.BookClient') |
559 | + def test_mocked_prepare_eds_connections(self, client, source, registry): |
560 | + self.protocol._name = 'testsuite' |
561 | + self.assertIsNone(self.protocol._address_book_name) |
562 | + self.protocol._prepare_eds_connections() |
563 | + self.protocol._prepare_eds_connections() # Second time harmlessly ignored |
564 | + self.assertEqual(self.protocol._address_book_name, |
565 | + 'friends-testsuite-contacts') |
566 | + registry.new_sync.assert_called_once_with(None) |
567 | + self.assertEqual(self.protocol._eds_source_registry, |
568 | + registry.new_sync()) |
569 | + registry.new_sync().ref_source.assert_called_once_with( |
570 | + 'friends-testsuite-contacts') |
571 | + self.assertEqual(self.protocol._eds_source, |
572 | + registry.new_sync().ref_source()) |
573 | + client.connect_sync.assert_called_once_with( |
574 | + registry.new_sync().ref_source(), None) |
575 | + self.assertEqual(self.protocol._book_client, |
576 | + client.connect_sync()) |
577 | + |
578 | + @mock.patch('gi.repository.EDataServer.SourceRegistry') |
579 | + @mock.patch('gi.repository.EDataServer.Source') |
580 | + @mock.patch('gi.repository.EBook.BookClient') |
581 | + def test_create_new_eds_book(self, client, source, registry): |
582 | + self.protocol._name = 'testsuite' |
583 | + self.assertIsNone(self.protocol._address_book_name) |
584 | + registry.new_sync().ref_source.return_value = None |
585 | + registry.reset_mock() |
586 | + self.protocol._prepare_eds_connections() |
587 | + self.protocol._prepare_eds_connections() # Second time harmlessly ignored |
588 | + self.assertEqual(self.protocol._address_book_name, |
589 | + 'friends-testsuite-contacts') |
590 | + registry.new_sync.assert_called_once_with(None) |
591 | + self.assertEqual(self.protocol._eds_source_registry, |
592 | + registry.new_sync()) |
593 | + registry.new_sync().ref_source.assert_called_once_with( |
594 | + 'friends-testsuite-contacts') |
595 | + source.new_with_uid.assert_called_once_with( |
596 | + 'friends-testsuite-contacts', None) |
597 | + self.assertEqual(self.protocol._eds_source, |
598 | + source.new_with_uid()) |
599 | + source.new_with_uid().set_display_name.assert_called_once_with( |
600 | + 'friends-testsuite-contacts') |
601 | + source.new_with_uid().set_parent.assert_called_once_with('local-stub') |
602 | + source.new_with_uid().get_extension.assert_called_once_with( |
603 | + EDataServer.SOURCE_EXTENSION_ADDRESS_BOOK) |
604 | + registry.new_sync().commit_source_sync.assert_called_once_with( |
605 | + source.new_with_uid(), None) |
606 | + client.connect_sync.assert_called_once_with( |
607 | + source.new_with_uid(), None) |
608 | + self.assertEqual(self.protocol._book_client, |
609 | + client.connect_sync()) |
610 | |
611 | === modified file 'friends/tests/test_flickr.py' |
612 | --- friends/tests/test_flickr.py 2013-06-18 22:00:44 +0000 |
613 | +++ friends/tests/test_flickr.py 2013-07-20 03:25:30 +0000 |
614 | @@ -49,7 +49,8 @@ |
615 | |
616 | def test_features(self): |
617 | # The set of public features. |
618 | - self.assertEqual(Flickr.get_features(), ['receive', 'upload']) |
619 | + self.assertEqual(Flickr.get_features(), |
620 | + ['delete_contacts', 'receive', 'upload']) |
621 | |
622 | @mock.patch('friends.utils.http.Soup.Message', |
623 | FakeSoupMessage('friends.tests.data', 'flickr-nophotos.dat')) |
624 | |
625 | === modified file 'friends/tests/test_foursquare.py' |
626 | --- friends/tests/test_foursquare.py 2013-06-18 22:00:44 +0000 |
627 | +++ friends/tests/test_foursquare.py 2013-07-20 03:25:30 +0000 |
628 | @@ -48,7 +48,8 @@ |
629 | |
630 | def test_features(self): |
631 | # The set of public features. |
632 | - self.assertEqual(FourSquare.get_features(), ['receive']) |
633 | + self.assertEqual(FourSquare.get_features(), |
634 | + ['delete_contacts', 'receive']) |
635 | |
636 | @mock.patch('friends.utils.authentication.manager') |
637 | @mock.patch('friends.utils.authentication.Accounts') |
638 | |
639 | === modified file 'friends/tests/test_identica.py' |
640 | --- friends/tests/test_identica.py 2013-04-17 06:13:49 +0000 |
641 | +++ friends/tests/test_identica.py 2013-07-20 03:25:30 +0000 |
642 | @@ -221,24 +221,6 @@ |
643 | get_url.assert_called_with( |
644 | 'http://identi.ca/api/search.json?q=hello') |
645 | |
646 | - def test_getfriendsids(self): |
647 | - get_url = self.protocol._get_url = mock.Mock(return_value=[1,2,3]) |
648 | - ids = self.protocol._getfriendsids() |
649 | - |
650 | - get_url.assert_called_with( |
651 | - 'http://identi.ca/api/friends/ids.json' |
652 | - ) |
653 | - self.assertEqual(ids, [1,2,3]) |
654 | - |
655 | - def test_showuser(self): |
656 | - get_url = self.protocol._get_url = mock.Mock(return_value={"name":"Alice"}) |
657 | - userdata = self.protocol._showuser(1) |
658 | - |
659 | - get_url.assert_called_with( |
660 | - 'http://identi.ca/api/users/show.json?user_id=1' |
661 | - ) |
662 | - self.assertEqual(userdata, {"name":"Alice"}) |
663 | - |
664 | def test_like(self): |
665 | get_url = self.protocol._get_url = mock.Mock() |
666 | inc_cell = self.protocol._inc_cell = mock.Mock() |
667 | @@ -265,32 +247,33 @@ |
668 | 'http://identi.ca/api/favorites/destroy/1234.json', |
669 | dict(id='1234')) |
670 | |
671 | - def test_create_contact(self, *mocks): |
672 | - # Receive the users friends. |
673 | - bare_contact = {'name': 'Alice Bob', |
674 | - 'screen_name': 'alice_bob', |
675 | - 'id': 13579} |
676 | - |
677 | - eds_contact = self.protocol._create_contact(bare_contact) |
678 | - twitter_id_attr = eds_contact.get_attribute('twitter-id') |
679 | - self.assertEqual(twitter_id_attr.get_value(), '13579') |
680 | - twitter_name_attr = eds_contact.get_attribute('twitter-name') |
681 | - self.assertEqual(twitter_name_attr.get_value(), 'Alice Bob') |
682 | - web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS') |
683 | - params= web_service_addrs.get_params() |
684 | - self.assertEqual(len(params), 2) |
685 | - |
686 | - test_remote_name = False |
687 | - test_twitter_id = False |
688 | - |
689 | - for p in params: |
690 | - if p.get_name() == 'remote-full-name': |
691 | - self.assertEqual(len(p.get_values()), 1) |
692 | - self.assertEqual(p.get_values()[0], 'Alice Bob') |
693 | - test_remote_name = True |
694 | - if p.get_name() == 'twitter-id': |
695 | - self.assertEqual(len(p.get_values()), 1) |
696 | - self.assertEqual(p.get_values()[0], '13579') |
697 | - test_twitter_id = True |
698 | - |
699 | - self.assertTrue(test_remote_name and test_twitter_id) |
700 | + def test_contacts(self): |
701 | + get = self.protocol._get_url = mock.Mock( |
702 | + return_value=dict(ids=[1,2],name='Bob',screen_name='bobby')) |
703 | + prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False) |
704 | + push = self.protocol._push_to_eds = mock.Mock() |
705 | + self.assertEqual(self.protocol.contacts(), 2) |
706 | + self.assertEqual( |
707 | + get.call_args_list, |
708 | + [mock.call('http://identi.ca/api/friends/ids.json'), |
709 | + mock.call(url='http://identi.ca/api/users/show.json?user_id=1'), |
710 | + mock.call(url='http://identi.ca/api/users/show.json?user_id=2')]) |
711 | + self.assertEqual( |
712 | + prev.call_args_list, |
713 | + [mock.call('1'), mock.call('2')]) |
714 | + self.assertEqual( |
715 | + push.call_args_list, |
716 | + [mock.call({'identica-id': '1', |
717 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
718 | + 'identica-id': '1', |
719 | + 'remote-full-name': 'Bob'}, |
720 | + 'X-URIS': 'https://identi.ca/bobby', |
721 | + 'identica-name': 'Bob', |
722 | + 'identica-nick': 'bobby'}), |
723 | + mock.call({'identica-id': '2', |
724 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
725 | + 'identica-id': '2', |
726 | + 'remote-full-name': 'Bob'}, |
727 | + 'X-URIS': 'https://identi.ca/bobby', |
728 | + 'identica-name': 'Bob', |
729 | + 'identica-nick': 'bobby'})]) |
730 | |
731 | === modified file 'friends/tests/test_instagram.py' |
732 | --- friends/tests/test_instagram.py 2013-07-08 20:54:34 +0000 |
733 | +++ friends/tests/test_instagram.py 2013-07-20 03:25:30 +0000 |
734 | @@ -54,7 +54,8 @@ |
735 | def test_features(self): |
736 | # The set of public features. |
737 | self.assertEqual(Instagram.get_features(), |
738 | - ['home', 'like', 'receive', 'send_thread', 'unlike']) |
739 | + ['delete_contacts', 'home', 'like', 'receive', |
740 | + 'send_thread', 'unlike']) |
741 | |
742 | @mock.patch('friends.utils.authentication.manager') |
743 | @mock.patch('friends.utils.authentication.Accounts') |
744 | |
745 | === modified file 'friends/tests/test_linkedin.py' |
746 | --- friends/tests/test_linkedin.py 2013-07-18 03:35:03 +0000 |
747 | +++ friends/tests/test_linkedin.py 2013-07-20 03:25:30 +0000 |
748 | @@ -23,7 +23,7 @@ |
749 | |
750 | import unittest |
751 | |
752 | -from friends.protocols.linkedin import LinkedIn |
753 | +from friends.protocols.linkedin import LinkedIn, make_fullname |
754 | from friends.tests.mocks import FakeAccount, FakeSoupMessage, LogMock |
755 | from friends.tests.mocks import TestModel, mock |
756 | from friends.errors import AuthorizationError |
757 | @@ -46,6 +46,19 @@ |
758 | # as to isolate out test logger from other tests. |
759 | self.log_mock.stop() |
760 | |
761 | + def test_name_logic(self): |
762 | + self.assertEqual('', make_fullname()) |
763 | + self.assertEqual('', make_fullname(irrelevant_key='foo')) |
764 | + self.assertEqual('Bob', make_fullname(**dict(firstName='Bob'))) |
765 | + self.assertEqual('LastOnly', make_fullname(**dict(lastName='LastOnly'))) |
766 | + self.assertEqual( |
767 | + 'Bob Loblaw', |
768 | + make_fullname(**dict(firstName='Bob', lastName='Loblaw'))) |
769 | + self.assertEqual( |
770 | + 'Bob Loblaw', |
771 | + make_fullname(**dict(firstName='Bob', lastName='Loblaw', |
772 | + extra='ignored'))) |
773 | + |
774 | @mock.patch('friends.utils.authentication.manager') |
775 | @mock.patch('friends.utils.authentication.Accounts') |
776 | @mock.patch.dict('friends.utils.authentication.__dict__', LOGIN_TIMEOUT=1) |
777 | @@ -97,14 +110,6 @@ |
778 | '&authToken=-LNy&trk=api*a26127*s26893*', |
779 | 1, False, '', '', '', '', '', '', '', 0.0, 0.0]) |
780 | |
781 | - @mock.patch('friends.utils.base.Base._create_contact') |
782 | - def test_create_contact(self, base_mock): |
783 | - self.protocol._create_contact( |
784 | - dict(id='jb89', firstName='Joe', lastName='Blow')) |
785 | - base_mock.assert_called_once_with( |
786 | - 'Joe Blow', None, |
787 | - {'X-URIS': '', 'linkedin-id': 'jb89', 'linkedin-name': 'Joe Blow'}) |
788 | - |
789 | @mock.patch('friends.utils.http.Soup.Message', |
790 | FakeSoupMessage('friends.tests.data', 'linkedin_contacts.json')) |
791 | @mock.patch('friends.protocols.linkedin.LinkedIn._login', |
792 | @@ -118,65 +123,30 @@ |
793 | self.assertEqual( |
794 | push.mock_calls, |
795 | [mock.call( |
796 | - {'siteStandardProfileRequest': |
797 | - {'url': 'https://www.linkedin.com'}, |
798 | - 'pictureUrl': 'http://m.c.lnkd.licdn.com', |
799 | - 'apiStandardProfileRequest': |
800 | - {'url': 'http://api.linkedin.com', |
801 | - 'headers': {'_total': 1, 'values': |
802 | - [{'value': 'name:', 'name': 'x-li-auth-token'}]}}, |
803 | - 'industry': 'Computer Network Security', |
804 | - 'lastName': 'A', |
805 | - 'firstName': 'H', |
806 | - 'headline': 'Unix Administrator at NVIDIA', |
807 | - 'location': {'name': 'Pune Area, India', |
808 | - 'country': {'code': 'in'}}, |
809 | - 'id': 'IFDI'}), |
810 | - |
811 | - mock.call( |
812 | - {'siteStandardProfileRequest': |
813 | - {'url': 'https://www.linkedin.com'}, |
814 | - 'pictureUrl': 'http://m.c.lnkd.licdn.com', |
815 | - 'apiStandardProfileRequest': |
816 | - {'url': 'http://api.linkedin.com', |
817 | - 'headers': {'_total': 1, 'values': |
818 | - [{'value': 'name:', 'name': 'x-li-auth-token'}]}}, |
819 | - 'industry': 'Food Production', |
820 | - 'lastName': 'A', |
821 | - 'firstName': 'C', |
822 | - 'headline': 'Recent Graduate, Simon Fraser University', |
823 | - 'location': {'name': 'Vancouver, Canada Area', |
824 | - 'country': {'code': 'ca'}}, |
825 | - 'id': 'AefF'}), |
826 | - |
827 | - mock.call( |
828 | - {'siteStandardProfileRequest': |
829 | - {'url': 'https://www.linkedin.com'}, |
830 | - 'pictureUrl': 'http://m.c.lnkd.licdn.com', |
831 | - 'apiStandardProfileRequest': |
832 | - {'url': 'http://api.linkedin.com', |
833 | - 'headers': {'_total': 1, 'values': |
834 | - [{'value': 'name:', 'name': 'x-li-auth-token'}]}}, |
835 | - 'industry': 'Computer Software', |
836 | - 'lastName': 'A', |
837 | - 'firstName': 'R', |
838 | - 'headline': 'Technical Lead at Canonical Ltd.', |
839 | - 'location': {'name': 'Auckland, New Zealand', |
840 | - 'country': {'code': 'nz'}}, |
841 | - 'id': 'DFdV'}), |
842 | - |
843 | - mock.call( |
844 | - {'siteStandardProfileRequest': |
845 | - {'url': 'https://www.linkedin.com'}, |
846 | - 'pictureUrl': 'http://m.c.lnkd.licdn.com', |
847 | - 'apiStandardProfileRequest': |
848 | - {'url': 'http://api.linkedin.com', |
849 | - 'headers': {'_total': 1, 'values': |
850 | - [{'value': 'name:', 'name': 'x-li-auth-token'}]}}, |
851 | - 'industry': 'Photography', |
852 | - 'lastName': 'Z', |
853 | - 'firstName': 'A', |
854 | - 'headline': 'Sales manager at McBain Camera', |
855 | - 'location': {'name': 'Edmonton, Canada Area', |
856 | - 'country': {'code': 'ca'}}, |
857 | - 'id': 'xkBU'})]) |
858 | + {'X-URIS': 'https://www.linkedin.com', |
859 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
860 | + 'linkedin-id': 'IFDI', |
861 | + 'remote-full-name': 'H A'}, |
862 | + 'linkedin-name': 'H A', |
863 | + 'linkedin-id': 'IFDI'}), |
864 | + mock.call( |
865 | + {'X-URIS': 'https://www.linkedin.com', |
866 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
867 | + 'linkedin-id': 'AefF', |
868 | + 'remote-full-name': 'C A'}, |
869 | + 'linkedin-name': 'C A', |
870 | + 'linkedin-id': 'AefF'}), |
871 | + mock.call( |
872 | + {'X-URIS': 'https://www.linkedin.com', |
873 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
874 | + 'linkedin-id': 'DFdV', |
875 | + 'remote-full-name': 'R A'}, |
876 | + 'linkedin-name': 'R A', |
877 | + 'linkedin-id': 'DFdV'}), |
878 | + mock.call( |
879 | + {'X-URIS': 'https://www.linkedin.com', |
880 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
881 | + 'linkedin-id': 'xkBU', |
882 | + 'remote-full-name': 'A Z'}, |
883 | + 'linkedin-name': 'A Z', |
884 | + 'linkedin-id': 'xkBU'})]) |
885 | |
886 | === modified file 'friends/tests/test_protocols.py' |
887 | --- friends/tests/test_protocols.py 2013-06-21 20:17:07 +0000 |
888 | +++ friends/tests/test_protocols.py 2013-07-20 03:25:30 +0000 |
889 | @@ -440,7 +440,8 @@ |
890 | # with-statement in Base._login(). |
891 | |
892 | def test_features(self): |
893 | - self.assertEqual(MyProtocol.get_features(), ['feature_1', 'feature_2']) |
894 | + self.assertEqual(MyProtocol.get_features(), |
895 | + ['delete_contacts', 'feature_1', 'feature_2']) |
896 | |
897 | def test_linkify_string(self): |
898 | # String with no URL is unchanged. |
899 | |
900 | === modified file 'friends/tests/test_twitter.py' |
901 | --- friends/tests/test_twitter.py 2013-06-18 22:00:44 +0000 |
902 | +++ friends/tests/test_twitter.py 2013-07-20 03:25:30 +0000 |
903 | @@ -552,56 +552,6 @@ |
904 | get_url.assert_called_with( |
905 | 'https://api.twitter.com/1.1/search/tweets.json?q=hello') |
906 | |
907 | - def test_getfriendsids(self): |
908 | - get_url = self.protocol._get_url = mock.Mock( |
909 | - return_value={"ids":[1,2,3]}) |
910 | - ids = self.protocol._getfriendsids() |
911 | - |
912 | - get_url.assert_called_with( |
913 | - 'https://api.twitter.com/1.1/friends/ids.json' |
914 | - ) |
915 | - self.assertEqual(ids, [1,2,3]) |
916 | - |
917 | - def test_showuser(self): |
918 | - get_url = self.protocol._get_url = mock.Mock( |
919 | - return_value={"name":"Alice"}) |
920 | - userdata = self.protocol._showuser(1) |
921 | - |
922 | - get_url.assert_called_with( |
923 | - 'https://api.twitter.com/1.1/users/show.json?user_id=1' |
924 | - ) |
925 | - self.assertEqual(userdata, {"name":"Alice"}) |
926 | - |
927 | - def test_create_contact(self, *mocks): |
928 | - # Receive the users friends. |
929 | - bare_contact = {'name': 'Alice Bob', |
930 | - 'screen_name': 'alice_bob', |
931 | - 'id': 13579} |
932 | - |
933 | - eds_contact = self.protocol._create_contact(bare_contact) |
934 | - twitter_id_attr = eds_contact.get_attribute('twitter-id') |
935 | - self.assertEqual(twitter_id_attr.get_value(), '13579') |
936 | - twitter_name_attr = eds_contact.get_attribute('twitter-name') |
937 | - self.assertEqual(twitter_name_attr.get_value(), 'Alice Bob') |
938 | - web_service_addrs = eds_contact.get_attribute('X-FOLKS-WEB-SERVICES-IDS') |
939 | - params= web_service_addrs.get_params() |
940 | - self.assertEqual(len(params), 2) |
941 | - |
942 | - test_remote_name = False |
943 | - test_twitter_id = False |
944 | - |
945 | - for p in params: |
946 | - if p.get_name() == 'remote-full-name': |
947 | - self.assertEqual(len(p.get_values()), 1) |
948 | - self.assertEqual(p.get_values()[0], 'Alice Bob') |
949 | - test_remote_name = True |
950 | - if p.get_name() == 'twitter-id': |
951 | - self.assertEqual(len(p.get_values()), 1) |
952 | - self.assertEqual(p.get_values()[0], '13579') |
953 | - test_twitter_id = True |
954 | - |
955 | - self.assertTrue(test_remote_name and test_twitter_id) |
956 | - |
957 | @mock.patch('friends.protocols.twitter.time.sleep') |
958 | def test_rate_limiter_first_time(self, sleep): |
959 | # The first time we see a URL, there is no rate limiting. |
960 | @@ -747,3 +697,34 @@ |
961 | # sleep 100 seconds between each call. |
962 | self.protocol.home() |
963 | sleep.assert_called_with(100.0) |
964 | + |
965 | + def test_contacts(self): |
966 | + get = self.protocol._get_url = mock.Mock( |
967 | + return_value=dict(ids=[1,2],name='Bob',screen_name='bobby')) |
968 | + prev = self.protocol._previously_stored_contact = mock.Mock(return_value=False) |
969 | + push = self.protocol._push_to_eds = mock.Mock() |
970 | + self.assertEqual(self.protocol.contacts(), 2) |
971 | + self.assertEqual( |
972 | + get.call_args_list, |
973 | + [mock.call('https://api.twitter.com/1.1/friends/ids.json'), |
974 | + mock.call(url='https://api.twitter.com/1.1/users/show.json?user_id=1'), |
975 | + mock.call(url='https://api.twitter.com/1.1/users/show.json?user_id=2')]) |
976 | + self.assertEqual( |
977 | + prev.call_args_list, |
978 | + [mock.call('1'), mock.call('2')]) |
979 | + self.assertEqual( |
980 | + push.call_args_list, |
981 | + [mock.call({'twitter-id': '1', |
982 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
983 | + 'twitter-id': '1', |
984 | + 'remote-full-name': 'Bob'}, |
985 | + 'X-URIS': 'https://twitter.com/bobby', |
986 | + 'twitter-name': 'Bob', |
987 | + 'twitter-nick': 'bobby'}), |
988 | + mock.call({'twitter-id': '2', |
989 | + 'X-FOLKS-WEB-SERVICES-IDS': { |
990 | + 'twitter-id': '2', |
991 | + 'remote-full-name': 'Bob'}, |
992 | + 'X-URIS': 'https://twitter.com/bobby', |
993 | + 'twitter-name': 'Bob', |
994 | + 'twitter-nick': 'bobby'})]) |
995 | |
996 | === modified file 'friends/utils/base.py' |
997 | --- friends/utils/base.py 2013-07-17 22:15:56 +0000 |
998 | +++ friends/utils/base.py 2013-07-20 03:25:30 +0000 |
999 | @@ -33,7 +33,7 @@ |
1000 | |
1001 | from gi.repository import GLib, GObject, EDataServer, EBook, EBookContacts |
1002 | |
1003 | -from friends.errors import FriendsError, ContactsError |
1004 | +from friends.errors import FriendsError, ContactsError, ignored |
1005 | from friends.utils.authentication import Authentication |
1006 | from friends.utils.model import Schema, Model, persist_model |
1007 | from friends.utils.notify import notify |
1008 | @@ -186,8 +186,11 @@ |
1009 | The code in this class has been tested against Facebook, Twitter, |
1010 | Flickr, Identica, and Foursquare, and works well with all of them. |
1011 | """ |
1012 | - # Used for EDS stuff. |
1013 | - _source_registry = None |
1014 | + # Lazily populated when needed for contact syncing. |
1015 | + _book_client = None |
1016 | + _address_book_name = None |
1017 | + _eds_source_registry = None |
1018 | + _eds_source = None |
1019 | |
1020 | # This number serves a guideline (not a hard limit) for the protocol |
1021 | # subclasses to download in each refresh. |
1022 | @@ -201,7 +204,6 @@ |
1023 | self._account = account |
1024 | self._Name = self.__class__.__name__ |
1025 | self._name = self._Name.lower() |
1026 | - self._address_book = 'friends-{}-contacts'.format(self._name) |
1027 | |
1028 | def _whoami(self, result): |
1029 | """Use OAuth login results to identify the authenticating user. |
1030 | @@ -540,102 +542,83 @@ |
1031 | Model.get_row(row_id)[col_idx] -= 1 |
1032 | persist_model() |
1033 | |
1034 | - def _new_book_client(self, source): |
1035 | - client = EBook.BookClient.new(source) |
1036 | - client.open_sync(False, None) |
1037 | - return client |
1038 | + def _prepare_eds_connections(self, allow_creation=True): |
1039 | + """Lazily establish a connection to EDS.""" |
1040 | + if None not in (self._address_book_name, self._eds_source_registry, |
1041 | + self._eds_source, self._book_client): |
1042 | + return |
1043 | + |
1044 | + self._address_book_name = 'friends-{}-contacts'.format(self._name) |
1045 | + self._eds_source_registry = EDataServer.SourceRegistry.new_sync(None) |
1046 | + |
1047 | + self._eds_source = self._eds_source_registry.ref_source( |
1048 | + self._address_book_name) |
1049 | + |
1050 | + # First run? Might need to create the EDS source! |
1051 | + if allow_creation and self._eds_source is None: |
1052 | + self._eds_source = EDataServer.Source.new_with_uid( |
1053 | + self._address_book_name, None) |
1054 | + self._eds_source.set_display_name(self._address_book_name) |
1055 | + self._eds_source.set_parent('local-stub') |
1056 | + self._eds_source.get_extension( |
1057 | + EDataServer.SOURCE_EXTENSION_ADDRESS_BOOK |
1058 | + ).set_backend_name('local') |
1059 | + self._eds_source_registry.commit_source_sync(self._eds_source, None) |
1060 | + |
1061 | + if self._eds_source is not None: |
1062 | + self._book_client = EBook.BookClient.connect_sync(self._eds_source, None) |
1063 | |
1064 | def _push_to_eds(self, contact): |
1065 | - source_match = self._get_eds_source() |
1066 | - if source_match is None: |
1067 | + self._prepare_eds_connections() |
1068 | + contact = self._create_contact(contact) |
1069 | + if not self._book_client.add_contact_sync(contact, None): |
1070 | + raise ContactsError('Failed to save contact {!r}'.format(contact)) |
1071 | + |
1072 | + def _previously_stored_contact(self, search_term): |
1073 | + self._prepare_eds_connections() |
1074 | + query = EBookContacts.BookQuery.vcard_field_test( |
1075 | + '{}-id'.format(self._name), EBookContacts.BookQueryTest.IS, search_term) |
1076 | + success, result = self._book_client.get_contacts_sync(query.to_string(), None) |
1077 | + if not success: |
1078 | raise ContactsError( |
1079 | - '{} does not have an address book.'.format( |
1080 | - self._address_book)) |
1081 | - client = self._new_book_client(source_match) |
1082 | - success = client.add_contact_sync(contact, None) |
1083 | - if not success: |
1084 | - raise ContactsError('Failed to save contact {!r}', contact) |
1085 | - |
1086 | - def _get_eds_source_registry(self): |
1087 | - if self._source_registry is None: |
1088 | - self._source_registry = EDataServer.SourceRegistry.new_sync(None) |
1089 | - |
1090 | - def _create_eds_source(self): |
1091 | - self._get_eds_source_registry() |
1092 | - source = EDataServer.Source.new(None, None) |
1093 | - source.set_display_name(self._address_book) |
1094 | - source.set_parent('local-stub') |
1095 | - extension = source.get_extension( |
1096 | - EDataServer.SOURCE_EXTENSION_ADDRESS_BOOK) |
1097 | - extension.set_backend_name('local') |
1098 | - if self._source_registry.commit_source_sync(source, None): |
1099 | - # https://bugzilla.gnome.org/show_bug.cgi?id=685986 |
1100 | - # Potential race condition - need to sleep for a |
1101 | - # couple of cycles to ensure the registry will return |
1102 | - # a valid source object after commiting. Evolution fix |
1103 | - # on the way but for now we need this. |
1104 | - time.sleep(2) |
1105 | - return self._source_registry.ref_source(source.get_uid()) |
1106 | - |
1107 | - def _get_eds_source(self): |
1108 | - self._get_eds_source_registry() |
1109 | - for previous_source in self._source_registry.list_sources(None): |
1110 | - if previous_source.get_display_name() == self._address_book: |
1111 | - return self._source_registry.ref_source( |
1112 | - previous_source.get_uid()) |
1113 | - # if we got this far, then the source doesn't exist; create it |
1114 | - return self._create_eds_source() |
1115 | - |
1116 | - def _previously_stored_contact(self, source, field, search_term): |
1117 | - client = self._new_book_client(source) |
1118 | - query = EBookContacts.BookQuery.vcard_field_test( |
1119 | - field, EBookContacts.BookQueryTest.IS, search_term) |
1120 | - success, result = client.get_contacts_sync(query.to_string(), None) |
1121 | - if not success: |
1122 | - raise ContactsError('Search failed on field {}'.format(field)) |
1123 | + 'Id field is missing in {} address book.'.format(self._Name)) |
1124 | return len(result) > 0 |
1125 | |
1126 | - def _delete_service_contacts(self, source): |
1127 | - client = self._new_book_client(source) |
1128 | - query = EBook.book_query_any_field_contains('') |
1129 | - success, results = client.get_contacts_sync(query.to_string(), None) |
1130 | - if not success: |
1131 | - raise ContactsError('Search for delete all contacts failed') |
1132 | - log.debug('Found {} contacts to delete'.format(len(results))) |
1133 | - for contact in results: |
1134 | - log.debug( |
1135 | - 'Deleting contact {}'.format( |
1136 | - contact.get_property('full-name'))) |
1137 | - client.remove_contact_sync(contact, None) |
1138 | - return True |
1139 | - |
1140 | - def _create_contact(self, user_fullname, user_nickname, |
1141 | - social_network_attrs): |
1142 | + def _create_contact(self, info): |
1143 | """Build a VCard based on a dict representation of a contact.""" |
1144 | - vcard = EBookContacts.VCard.new() |
1145 | - info = social_network_attrs |
1146 | + contact = EBookContacts.Contact.new() |
1147 | |
1148 | - for key, val in info.items(): |
1149 | + for key, value in info.items(): |
1150 | attr = EBookContacts.VCardAttribute.new( |
1151 | 'social-networking-attributes', key) |
1152 | - if isinstance(val, dict): |
1153 | - for subkey, subval in val.items(): |
1154 | - param = EBookContacts.VCardAttributeParam.new(subkey) |
1155 | - param.add_value(subval) |
1156 | - attr.add_param(param); |
1157 | + if hasattr(value, 'items'): |
1158 | + for subkey, subval in value.items(): |
1159 | + if subval is not None: |
1160 | + param = EBookContacts.VCardAttributeParam.new(subkey) |
1161 | + param.add_value(subval) |
1162 | + attr.add_param(param); |
1163 | + elif value is not None: |
1164 | + attr.add_value(value) |
1165 | else: |
1166 | - attr.add_value(val) |
1167 | - vcard.add_attribute(attr) |
1168 | - |
1169 | - contact = EBookContacts.Contact.new_from_vcard( |
1170 | - vcard.to_string(EBookContacts.VCardFormat(1))) |
1171 | - contact.set_property('full-name', user_fullname) |
1172 | - if user_nickname is not None: |
1173 | - contact.set_property('nickname', user_nickname) |
1174 | - |
1175 | - log.debug('Creating new contact for {}'.format(user_fullname)) |
1176 | + continue |
1177 | + contact.add_attribute(attr) |
1178 | + |
1179 | + properties = (('full-name', '{}-name'), |
1180 | + ('nickname', '{}-nick')) |
1181 | + for prop, key in properties: |
1182 | + value = info.get(key.format(self._name)) |
1183 | + if value is not None: |
1184 | + contact.set_property(prop, value) |
1185 | + log.debug('New contact got {}: {}'.format(prop, value)) |
1186 | return contact |
1187 | |
1188 | + @feature |
1189 | + def delete_contacts(self): |
1190 | + """Remove all synced contacts from this social network.""" |
1191 | + self._prepare_eds_connections(allow_creation=False) |
1192 | + with ignored(GLib.GError, AttributeError): |
1193 | + return self._eds_source.remove_sync(None) |
1194 | + |
1195 | @classmethod |
1196 | def get_features(cls): |
1197 | """Report what public operations we expose over DBus.""" |
Ok, I'm pretty happy with this at this point. A lot of mysterious methods are gone, test coverage is good, and the major inefficiencies have been streamlined. If you could review this on Monday, Ken, that'd be super.