Re-implement Facebook service to use OAuth2 and Graph API. This allows
authorAndrew Flegg <andrew@bleb.org>
Thu, 30 Dec 2010 16:05:39 +0000 (16:05 +0000)
committerAndrew Flegg <andrew@bleb.org>
Sun, 2 Jan 2011 19:53:46 +0000 (19:53 +0000)
the requesting of additional permissions when authorising the account.
Fixes MB#11103.

package/debian/control
package/debian/hermes.postinst
package/src/oauth2.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/facebook/api.py [new file with mode: 0644]
package/src/org/maemo/hermes/engine/facebook/provider.py
package/src/org/maemo/hermes/engine/facebook/service.py
package/test/automatic_tests.py
package/test/integration/test_facebook.py [new file with mode: 0644]
package/test/integration/test_oauth2.py [new file with mode: 0644]
package/test/unit/test_facebook.py

index 2489622..3c08435 100644 (file)
@@ -10,8 +10,7 @@ Architecture: all
 Depends: ${shlibs:Depends}, ${misc:Depends}, python-imaging |
  python2.5-imaging, python-osso | python2.5-osso, python-hildon |
  python2.5-hildon, python-simplejson, python-evolution, python-conic,
- python-facebook (>= 0.svn20090225-0maemo2), python-evolution,
- python-gobject (>= 2.16), gnome-python
+ python-evolution, python-gobject (>= 2.16), gnome-python
 Description: Enrich contacts' information from social networks
  Hermes, the Greek god of communication, will fill in the gaps in your
  contacts' address book. Photos and birthdays for your friends on
index 5208d64..605c66e 100644 (file)
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 set -e
-gconftool-2 -s /apps/maemo/hermes/facebook_app 5916f12942feea4b3247d42a84371112 --type string
+gconftool-2 -s /apps/maemo/hermes/facebook_key 5916f12942feea4b3247d42a84371112 --type string
 gconftool-2 -s /apps/maemo/hermes/facebook_secret 19f7538edd96b6870f2da7e84a6390a4 --type string
 gconftool-2 -s /apps/maemo/hermes/gravatar_email maemohermes@wendt.se --type string
 gconftool-2 -s /apps/maemo/hermes/gravatar_key b14ec179822b --type string
diff --git a/package/src/oauth2.py b/package/src/oauth2.py
new file mode 100644 (file)
index 0000000..f47dee0
--- /dev/null
@@ -0,0 +1,132 @@
+import webbrowser
+import urllib, urllib2
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import urlparse, cgi
+
+class OAuth2:
+    """Implementation of OAuth2 protocol, as described by Facebook:
+    
+           http://developers.facebook.com/docs/authentication/#web_server_auth
+
+       Uses a local web server to retrieve the code and hence the access token.
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+
+
+    # -----------------------------------------------------------------------
+    def __init__(self, client_id, client_secret, access_token = None):
+        '''Create a new OAuth 2.0 client, with the folowing arguments:
+        
+               client_id - Identifier of the application.
+               client_secret - Secret API key of the application.
+               access_token - Current access token, if any.'''
+        
+        self._client_id     = client_id
+        self._client_secret = client_secret
+        self._access_token  = access_token
+
+
+    # -----------------------------------------------------------------------
+    def authorise(self, authorise_url, access_token_url, args = None):
+        '''Open a browser window to allow the user to log in and
+           authorise this client.'''
+
+        redirect_uri = 'http://localhost:3435/success'
+        webbrowser.open_new('%s?client_id=%s&redirect_uri=%s&%s' % (authorise_url, self._client_id, redirect_uri, args and urllib.urlencode(args) or ''))
+        handler = OAuthCodeHandler()
+        code = handler.run()
+
+        result = urllib2.urlopen('%s?client_id=%s&redirect_uri=%s&client_secret=%s&code=%s' % (access_token_url, self._client_id, redirect_uri, self._client_secret, code)).read()
+        params = cgi.parse_qs(result)
+        if 'access_token' in params:
+            self._access_token = params['access_token'][0]
+        else:
+            print result, params
+            raise Exception('Unable to retrieve access_token from Facebook')
+
+
+    # -----------------------------------------------------------------------
+    def request(self, url, args = None):
+        '''Make an authenticated request to the given URL. If no
+           access token is currently set, an exception will be thrown.
+           
+           An optional dictionary of parameters can be specified.'''
+           
+        if not self._access_token:
+            raise Exception("Unauthorised")
+        
+        query_url = '%s?access_token=%s&%s' % (url, self._access_token, args and urllib.urlencode(args) or '')
+#        print query_url
+        result = urllib2.urlopen(query_url).read()
+        return result
+    
+
+    # -----------------------------------------------------------------------
+    def get_access_token(self):
+        """Get the access token in use by this OAuth 2.0 client,
+           so that it can be persisted."""
+           
+        return self._access_token
+
+
+# ---------------------------------------------------------------------------
+class OAuthCodeHandler(HTTPServer):
+    """Handles the response from an OAuth2 handler and allows the
+       retrieval of the code."""
+
+    # -----------------------------------------------------------------------
+    def __init__(self, success_response = '<h1>Success</h1><p>Please now close this window.</p>',
+                       failure_response = '<h1>Failed</h1><p>%s</p>'):
+        '''Create a new handler, with optional overrides of the
+           success and failure response messages.'''
+        
+        HTTPServer.__init__(self, ('127.0.0.1', 3435), OAuthHttpRequestHandler)
+        self._success_response = success_response
+        self._failure_response = failure_response
+        self._code = None
+        
+        
+    # -----------------------------------------------------------------------
+    def run(self):
+        '''Start a server and wait for a redirect. Return the code
+           if/when successful.'''
+           
+        self.handle_request()
+        return self._code
+
+
+    # -----------------------------------------------------------------------
+    def set_code(self, code):
+        '''Called by the handler to feed us back the code.'''
+        
+        self._code = code
+        
+    def get_success_response(self):
+        return self._success_response
+
+    def get_failure_response(self):
+        return self._failure_response
+
+
+# ---------------------------------------------------------------------------
+class OAuthHttpRequestHandler(BaseHTTPRequestHandler):
+    def do_GET(self):
+        qs = urlparse.urlparse(self.path).query
+        params = cgi.parse_qs(qs)
+        
+        if 'code' in params:
+            self.server.set_code(params['code'][0])
+
+            self.send_response(200)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            self.wfile.write(self.server.get_success_response())
+        else:
+            print qs, params
+            self.send_response(500)
+            self.send_header('Content-type', 'text/html')
+            self.end_headers()
+            self.wfile.write(self.server.get_failure_response())
+        
+        return
diff --git a/package/src/org/maemo/hermes/engine/facebook/api.py b/package/src/org/maemo/hermes/engine/facebook/api.py
new file mode 100644 (file)
index 0000000..19d57ed
--- /dev/null
@@ -0,0 +1,64 @@
+import urllib, urllib2
+import simplejson
+
+class FacebookApi():
+    """Facebook backend for Hermes, using the Graph API:
+    
+          http://developers.facebook.com/docs/reference/api/
+       
+       Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
+       Released under the Artistic Licence."""
+       
+       
+    # -----------------------------------------------------------------------
+    def __init__(self, oauth):
+        self._oauth = oauth
+
+
+    # -----------------------------------------------------------------------
+    def authenticate(self):
+        '''Authenticate the user with Facebook.'''
+        
+        self._oauth.authorise('https://graph.facebook.com/oauth/authorize',
+                              'https://graph.facebook.com/oauth/access_token',
+                              {'scope': 'user_about_me,friends_about_me,user_birthday,friends_birthday,user_website,friends_website,user_work_history,friends_work_history'})
+
+
+    # -----------------------------------------------------------------------
+    def get_user(self):
+        '''Return the name of the authenticated user.'''
+        
+        data = self._request('https://graph.facebook.com/me')
+        return data['name']
+
+
+    # -----------------------------------------------------------------------
+    def get_friends(self):
+        '''Return the full list of people being followed by the user.
+        
+           The result is a list of users:
+           http://developers.facebook.com/docs/reference/api/user/'''
+
+        def copy(data, from_key, to, to_key = None):
+            if not to_key:
+                to_key = from_key
+                
+            if from_key in data:
+                to[to_key] = data[from_key]
+
+        users = self._request('https://graph.facebook.com/me/friends', {'fields': 'id,name,link,birthday,website,picture', 'type': 'large'})
+        return users['data']
+    
+    
+    # -----------------------------------------------------------------------
+    def _request(self, url, args = None):
+        """Make an authenticated request to Facebook and check the
+           JSON response. Return the dictionary if no errors."""
+        
+        json = self._oauth.request(url, args)
+#        print json
+        data = simplejson.loads(json)
+        if 'error' in data:
+            raise Exception(data['error'])
+        
+        return data
index 375db39..e693795 100644 (file)
@@ -1,8 +1,9 @@
-from pythonfacebook import Facebook, FacebookError
 import gnome.gconf
 import gtk, hildon
 import org.maemo.hermes.engine.provider
-import org.maemo.hermes.engine.facebook.service
+from org.maemo.hermes.engine.facebook.service import Service
+from org.maemo.hermes.engine.facebook.api import FacebookApi
+import oauth2
 
 class Provider(org.maemo.hermes.engine.provider.Provider):
     """Facebook provider for Hermes. 
@@ -20,18 +21,14 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
 
         self._gc = gnome.gconf.client_get_default()
 
-        key_app    = self._gc.get_string('/apps/maemo/hermes/facebook_app')
+        key_app    = self._gc.get_string('/apps/maemo/hermes/facebook_key')
         key_secret = self._gc.get_string('/apps/maemo/hermes/facebook_secret')
         if key_app is None or key_secret is None:
             raise Exception('No Facebook application keys found. Installation error.')
         
-        self.fb = Facebook(key_app, key_secret)
-        self.fb.desktop = True
-
-        if self.fb.session_key is None:
-            self.fb.session_key = self._gc.get_string('/apps/maemo/hermes/facebook_session_key')
-            self.fb.secret = self._gc.get_string('/apps/maemo/hermes/facebook_secret_key')
-            self.fb.uid = self._gc.get_string('/apps/maemo/hermes/facebook_uid')
+        access_token = self._gc.get_string('/apps/maemo/hermes/facebook_access_token')
+        self.oauth = oauth2.OAuth2(key_app, key_secret, access_token)
+        self.api = FacebookApi(self.oauth)
 
 
     # -----------------------------------------------------------------------
@@ -46,8 +43,7 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
     def get_account_detail(self):
         """Return the email address associated with the user, if available."""
         
-        name = self._gc.get_string('/apps/maemo/hermes/facebook_user')
-        return name and name or _('Pending authorisation')
+        return self._gc.get_string('/apps/maemo/hermes/facebook_user')
     
     
     # -----------------------------------------------------------------------
@@ -64,19 +60,19 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
 
         dialog = gtk.Dialog(self.get_name(), parent)
         dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
-        dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
+        enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
+
+        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
+                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
+        self._handle_button(None, button, enable)
+        button.connect('clicked', self._handle_button, button, enable)
+        dialog.vbox.add(button)
         
         checkbox = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
         checkbox.set_label(_('Create birthday-only contacts'))
         checkbox.set_active(self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
         dialog.vbox.add(checkbox)
-        dialog.vbox.add(gtk.Label("\n" + _('Note: authentication via web page') + "\n"))
-        
-        clear = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
-                              hildon.BUTTON_ARRANGEMENT_VERTICAL,
-                              title = _("Clear authorisation"))
-        clear.connect('clicked', self._clear_auth)
-        dialog.vbox.add(clear)
+        dialog.vbox.add(gtk.Label(""))
         
         dialog.show_all()
         result = dialog.run()
@@ -89,14 +85,22 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
 
 
     # -----------------------------------------------------------------------
-    def _clear_auth(self, event):
-        """Clear Facebook authorisation information. Triggered by pressing
-           the 'clear' button in the preferences dialogue."""
+    def _handle_button(self, e, button, enable):
+        """Ensure the button state is correct."""
+        
+        authenticated = self._gc.get_string('/apps/maemo/hermes/facebook_access_token') is not None
+        if e is not None:
+            if authenticated:
+                self._gc.unset('/apps/maemo/hermes/facebook_access_token')
+            else:
+                self.api.authenticate()
+                self._gc.set_string('/apps/maemo/hermes/facebook_access_token', self.oauth.get_access_token())
+                self._gc.set_string('/apps/maemo/hermes/facebook_user', self.api.get_user())
         
-        self._gc.unset('/apps/maemo/hermes/facebook_session_key')
-        self._gc.unset('/apps/maemo/hermes/facebook_secret_key')
-        self._gc.unset('/apps/maemo/hermes/facebook_uid')
-        self._gc.unset('/apps/maemo/hermes/facebook_user')
+            authenticated = self._gc.get_string('/apps/maemo/hermes/facebook_access_token') is not None
+        
+        button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
+        enable.set_sensitive(authenticated)
 
     
     # -----------------------------------------------------------------------
@@ -104,44 +108,4 @@ class Provider(org.maemo.hermes.engine.provider.Provider):
         """Return a service instance."""
         
         self._gui = gui_callback
-        
-        # Check the available session is still valid...
-        while True:
-            try:
-                if self.fb.users.getLoggedInUser() and self.fb.session_key:
-                    break
-            except FacebookError:
-                pass
-            self._do_fb_login()
-
-        return org.maemo.hermes.engine.facebook.service.Service(self.get_id(), self.fb, self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
-
-
-    # -----------------------------------------------------------------------
-    def _do_fb_login(self):
-        """Perform authentication against Facebook and store the result in gconf
-             for later use. Uses the 'need_auth' and 'block_for_auth' methods on
-             the callback class. The former allows a message to warn the user
-             about what is about to happen to be shown; the second is to wait
-             for the user to confirm they have logged in."""
-        self.fb.session_key = None
-        self.fb.secret = None
-        self.fb.uid = None
-        
-        if self._gui:
-            self._gui.need_auth()
-            
-        self.fb.auth.createToken()
-        self.fb.login()
-        
-        if self._gui:
-            self._gui.block_for_auth()
-          
-        session = self.fb.auth.getSession()
-        self._gc.set_string('/apps/maemo/hermes/facebook_session_key', session['session_key'])
-        self._gc.set_string('/apps/maemo/hermes/facebook_secret_key', session['secret'])
-        self._gc.set_string('/apps/maemo/hermes/facebook_uid', str(session['uid']))
-
-        info = self.fb.users.getInfo([self.fb.uid], ['name'])
-        self._gc.set_string('/apps/maemo/hermes/facebook_user', info[0]['name'])
-
+        return Service(self.get_id(), self.api, self._gc.get_bool('/apps/maemo/hermes/facebook_birthday_only'))
index 0db44ca..ff46c26 100644 (file)
@@ -32,7 +32,8 @@ class Service(org.maemo.hermes.engine.service.Service):
         friends = []
         if self._create_birthday_only:
             for friend in self._friends_without_contact:
-                friends.append(friend)
+                if friend.is_interesting():
+                    friends.append(friend)
                     
         return friends
     
@@ -77,20 +78,20 @@ class Service(org.maemo.hermes.engine.service.Service):
             if key in data and data[key]:
                 callback(data[key])
         
-        friends_data = self._get_friends_data()
+        friends_data = self.fb.get_friends()
         for data in friends_data:
             friend = Friend(data['name'])
         
-            if 'profile_url' not in data:
-                data['profile_url'] = "http://www.facebook.com/profile.php?id=" + str(data['uid'])
+            if 'link' not in data:
+                data['link'] = "http://www.facebook.com/profile.php?id=" + str(data['id'])
         
             if_defined(data, 'website', friend.add_url)
-            if_defined(data, 'profile_url', friend.add_url)
-            if_defined(data, 'birthday_date', friend.set_birthday_date)
+            if_defined(data, 'link', friend.add_url)
+            if_defined(data, 'birthday', friend.set_birthday_date)
 
-            if_defined(data, 'pic_big', friend.set_photo_url)
+            if_defined(data, 'picture', friend.set_photo_url)
             
-            url = data['profile_url']
+            url = data['link']
             friend.add_url(url)
             self._register_friend(friend)
 
@@ -157,11 +158,3 @@ class Service(org.maemo.hermes.engine.service.Service):
         self._friends_without_contact.discard(friend)
         self._friends_by_contact[contact] = friend
         self._contacts_by_friend[friend] = contact
-
-
-    # -----------------------------------------------------------------------
-    def _get_friends_data(self):
-        """Returns a list of dicts, where each dict represents the raw data
-           of a friend"""
-        
-        return self.fb.users.getInfo(self.fb.friends.get(), Service.attrs)
index 0e26884..5f7ee62 100644 (file)
@@ -2,7 +2,7 @@
 
 import unittest
 
-from unit.test_facebook import TestFacebookService
+from unit.test_facebook import TestFacebookService, TestFacebookAPI
 from unit.test_gravatar import TestGravatarService
 from unit.test_linkedin import TestLinkedInService
 from unit.test_twitter import TestTwitterService
@@ -13,6 +13,9 @@ from unit.test_phonenumber import TestPhoneNumber
 from integration.test_gravatar import IntegrationTestGravatarService
 from integration.test_linkedinapi import IntegrationTestLinkedInApi
 from integration.test_twitter import IntegrationTestTwitterService
+from integration.test_oauth2 import IntegrationTestOAuth2
+from integration.test_facebook import IntegrationTestFacebook
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/package/test/integration/test_facebook.py b/package/test/integration/test_facebook.py
new file mode 100644 (file)
index 0000000..ca2973b
--- /dev/null
@@ -0,0 +1,36 @@
+import unittest
+import oauth2
+from org.maemo.hermes.engine.facebook.api import FacebookApi
+import httplib
+httplib.HTTPConnection.debuglevel = 1
+
+class IntegrationTestFacebook(unittest.TestCase):
+    access_token = None
+    
+    # -----------------------------------------------------------------------
+    def setUp(self):
+        self.oauth = oauth2.OAuth2('5916f12942feea4b3247d42a84371112', '19f7538edd96b6870f2da7e84a6390a4', IntegrationTestFacebook.access_token)
+        self.facebook = FacebookApi(self.oauth)
+
+    # -----------------------------------------------------------------------
+    def test_authenticate(self):
+        self.facebook.authenticate()
+        IntegrationTestFacebook.access_token = self.oauth.get_access_token()
+        
+        
+    # -----------------------------------------------------------------------
+    def test_get_user(self):
+        user = self.facebook.get_user()
+        print user
+        assert user
+
+
+    # -----------------------------------------------------------------------
+    def test_get_friends(self):
+        friends = self.facebook.get_friends()
+        print friends
+        assert friends
+        
+    
+if __name__ == '__main__':
+    unittest.main()
diff --git a/package/test/integration/test_oauth2.py b/package/test/integration/test_oauth2.py
new file mode 100644 (file)
index 0000000..f2f6c0d
--- /dev/null
@@ -0,0 +1,41 @@
+import unittest
+import oauth2
+import httplib
+import simplejson
+httplib.HTTPConnection.debuglevel = 1
+
+class IntegrationTestOAuth2(unittest.TestCase):
+    access_token = None
+    
+    # -----------------------------------------------------------------------
+    def setUp(self):
+        self.oauth = oauth2.OAuth2('5916f12942feea4b3247d42a84371112', '19f7538edd96b6870f2da7e84a6390a4', IntegrationTestOAuth2.access_token)
+        
+        
+    # -----------------------------------------------------------------------
+    def test_authorisation(self):
+        self.oauth.authorise('https://graph.facebook.com/oauth/authorize',
+                             'https://graph.facebook.com/oauth/access_token',
+                             {'scope': 'user_about_me,friends_about_me,user_birthday,friends_birthday,user_website,friends_website,user_work_history,friends_work_history'})
+
+        IntegrationTestOAuth2.access_token = self.oauth.get_access_token()
+        assert IntegrationTestOAuth2.access_token
+
+
+    # -----------------------------------------------------------------------
+    def test_request(self):
+        if not IntegrationTestOAuth2.access_token:
+            self.test_authorisation()
+            
+        response = self.oauth.request('https://graph.facebook.com/me')
+        data = simplejson.loads(response)
+        if 'error' in data:
+            print response
+            raise Exception(data['error'])
+
+        print response
+        assert 'name' in data and data['name']
+
+    
+if __name__ == '__main__':
+    unittest.main()
index b766d2f..2b3e958 100644 (file)
@@ -1,4 +1,5 @@
 from org.maemo.hermes.engine.facebook.service import Service
+from org.maemo.hermes.engine.facebook.api import FacebookApi
 from org.maemo.hermes.engine.names import canonical
 from org.maemo.hermes.engine.friend import Friend
 import unittest
@@ -18,38 +19,58 @@ class FakeContact():
     def get_identifiers(self):
         return [canonical(self.name)]
     
+class FakeFacebookApi():
+    friends = []
+    
+    def get_friends(self):
+        return FakeFacebookApi.friends
+    
+class FakeOAuth():
+    def authorise(self, authorise_url, access_token_url, args = None):
+        pass
+    
+    def request(self, url, args = None):
+        if url == 'https://graph.facebook.com/me':
+            return '{"id":"1234567","name":"Maemo Hermes"}'
+        
+        elif url == 'https://graph.facebook.com/me/friends':
+            return '{"data":[{"id":"1","name":"J Smith","website":"http:\/\/www.example.org"},{"id":"2","name":"P Barnum"}]}'
+        
+        else:
+            raise Exception("Unknown URL: %s" % (url))
+        
 
 class TestFacebookService(unittest.TestCase):
     
     def setUp(self):
-        self.testee = Service("facebook", None)
+        self.testee = Service("facebook", FakeFacebookApi())
         
     
     def test_that_get_friends_to_create_contacts_for_works(self):
         def run_test(expected_length):
-            self._fake_server_response([{'uid':'123456','name':'Facebook Junkie', 'birthday_date':'now'}])
+            self._fake_server_response([{'id':'123456','name':'Facebook Junkie', 'birthday':'now'}])
             self._run_service([])
             friends = self.testee.get_friends_to_create_contacts_for()
             assert len(friends) == expected_length
             
         # default is to NOT create contacts
-        self.testee = Service("facebook", None)
+        self.testee = Service("facebook", FakeFacebookApi())
         run_test(0)
         
         # passing False explicitly
-        self.testee = Service("facebook", None, False)
+        self.testee = Service("facebook", FakeFacebookApi(), False)
         run_test(0)
         
         # passing True to constructor
-        self.testee = Service("facebook", None, True)
+        self.testee = Service("facebook", FakeFacebookApi(), True)
         run_test(1)
         
     
     def test_that_gftccf_returns_friends_with_birth_date(self):
-        self.testee = Service("facebook", None, True)
+        self.testee = Service("facebook", FakeFacebookApi(), True)
         bday = '1980-10-15'
-        props_with_bday = {'uid':'123456','name':'Facebook Junkie', 'birthday_date':bday}
-        props_without = {'uid':'123457','name':'Johnny Secret'}
+        props_with_bday = {'id':'123456','name':'Facebook Junkie', 'birthday':bday}
+        props_without = {'id':'123457','name':'Johnny Secret'}
         self._fake_server_response([props_with_bday, props_without])
         self._run_service([])
         
@@ -63,7 +84,7 @@ class TestFacebookService(unittest.TestCase):
     def test_that_process_contact_returns_friend_object_for_known_contact(self):
         known_url = 'http://www.facebook.com/profile.php?id=123456'
         known_contact = FakeContact('Facebook Junkie', [known_url])
-        self._fake_server_response([{'uid':'123456','name':'Facebook Junkie'}])
+        self._fake_server_response([{'id':'123456','name':'Facebook Junkie'}])
         
         self.testee.process_friends()
         result = self.testee.process_contact(known_contact)
@@ -83,11 +104,11 @@ class TestFacebookService(unittest.TestCase):
         # arrange
         self.existing_address = 'http://www.facebook.com/profile.php?id=123456'
         self.existing_contact = FakeContact("Facebook Person", [self.existing_address])
-        existing_fake = {'uid':'123456','name':'Name Doesnt Match but URL Does'}
+        existing_fake = {'id':'123456','name':'Name Doesnt Match but URL Does'}
         
         self.other_address = 'http://twitter.com/not/correct/site'
         self.other_contact = FakeContact("Twitter Person", [self.other_address])
-        other_fake = {'uid':'123','name':self.other_contact.get_name()}
+        other_fake = {'id':'123','name':self.other_contact.get_name()}
         
         self.none_contact = FakeContact("No URLson", [])
         
@@ -114,7 +135,7 @@ class TestFacebookService(unittest.TestCase):
         contact_do_match = FakeContact(name, ["http://www.facebook.com/profile.php?id=123"], 1);
         contact_no_match = FakeContact(name, [None], 2)
         
-        data = [{'uid':'123','name':name}]
+        data = [{'id':'123','name':name}]
         self._fake_server_response(data)
         
         self._run_service([contact_no_match, contact_do_match])
@@ -130,7 +151,7 @@ class TestFacebookService(unittest.TestCase):
         contact_do_match = FakeContact(name, ["Contact 1"]);
         contact_no_match = FakeContact(name, ["Contact 2"])
         
-        data = [{'uid':'123','name':name}]
+        data = [{'id':'123','name':name}]
         self._fake_server_response(data)
         
         self._run_service([contact_no_match, contact_do_match])
@@ -148,12 +169,28 @@ class TestFacebookService(unittest.TestCase):
             self.testee.process_contact(contact)
         
     def _fake_server_response(self, data):
-        self.testee._get_friends_data = self._get_friends_data
-        self._server_response = data
-    
-    def _get_friends_data(self):
-        return self._server_response
+        FakeFacebookApi.friends = data
+
 
+class TestFacebookAPI(unittest.TestCase):
+
+    def setUp(self):
+        self.oauth = FakeOAuth()
+        self.api = FacebookApi(self.oauth)
+
+    def test_authenticate(self):
+        pass
+    
+    
+    def test_get_user(self):
+        assert self.api.get_user() == 'Maemo Hermes'
+       
+        
+    def test_get_friends(self):
+        friends = self.api.get_friends()
+        assert len(friends) == 2
+        assert friends[0]['name'] == 'J Smith'
+        assert friends[1]['id'] == '2'
 
     
 if __name__ == '__main__':