Adding a .revoke() to Credentials. Closes issue 98.

Reviewed in https://codereview.appspot.com/7033052/
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 37e69ea..6dc2729 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -29,13 +29,10 @@
 import unittest
 import urlparse
 
-try:
-    from urlparse import parse_qs
-except ImportError:
-    from cgi import parse_qs
-
 from apiclient.http import HttpMock
 from apiclient.http import HttpMockSequence
+from oauth2client import GOOGLE_REVOKE_URI
+from oauth2client import GOOGLE_TOKEN_URI
 from oauth2client.anyjson import simplejson
 from oauth2client.client import AccessTokenCredentials
 from oauth2client.client import AccessTokenCredentialsError
@@ -49,12 +46,17 @@
 from oauth2client.client import OAuth2WebServerFlow
 from oauth2client.client import OOB_CALLBACK_URN
 from oauth2client.client import REFRESH_STATUS_CODES
+from oauth2client.client import Storage
+from oauth2client.client import TokenRevokeError
 from oauth2client.client import VerifyJwtTokenError
 from oauth2client.client import _extract_id_token
+from oauth2client.client import _update_query_params
 from oauth2client.client import credentials_from_clientsecrets_and_code
 from oauth2client.client import credentials_from_code
 from oauth2client.client import flow_from_clientsecrets
 from oauth2client.clientsecrets import _loadfile
+from test_discovery import assertUrisEqual
+
 
 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
 
@@ -89,20 +91,54 @@
     restored = Credentials.new_from_json(json)
 
 
+class DummyDeleteStorage(Storage):
+  delete_called = False
+
+  def locked_delete(self):
+    self.delete_called = True
+
+
+def _token_revoke_test_helper(testcase, status, revoke_raise,
+                              valid_bool_value, token_attr):
+  current_store = getattr(testcase.credentials, 'store', None)
+
+  dummy_store = DummyDeleteStorage()
+  testcase.credentials.set_store(dummy_store)
+
+  actual_do_revoke = testcase.credentials._do_revoke
+  testcase.token_from_revoke = None
+  def do_revoke_stub(http_request, token):
+    testcase.token_from_revoke = token
+    return actual_do_revoke(http_request, token)
+  testcase.credentials._do_revoke = do_revoke_stub
+
+  http = HttpMock(headers={'status': status})
+  if revoke_raise:
+    testcase.assertRaises(TokenRevokeError, testcase.credentials.revoke, http)
+  else:
+    testcase.credentials.revoke(http)
+
+  testcase.assertEqual(getattr(testcase.credentials, token_attr),
+                       testcase.token_from_revoke)
+  testcase.assertEqual(valid_bool_value, testcase.credentials.invalid)
+  testcase.assertEqual(valid_bool_value, dummy_store.delete_called)
+
+  testcase.credentials.set_store(current_store)
+
+
 class BasicCredentialsTests(unittest.TestCase):
 
   def setUp(self):
-    access_token = "foo"
-    client_id = "some_client_id"
-    client_secret = "cOuDdkfjxxnv+"
-    refresh_token = "1/0/a.df219fjls0"
+    access_token = 'foo'
+    client_id = 'some_client_id'
+    client_secret = 'cOuDdkfjxxnv+'
+    refresh_token = '1/0/a.df219fjls0'
     token_expiry = datetime.datetime.utcnow()
-    token_uri = "https://www.google.com/accounts/o8/oauth2/token"
-    user_agent = "refresh_checker/1.0"
+    user_agent = 'refresh_checker/1.0'
     self.credentials = OAuth2Credentials(
-      access_token, client_id, client_secret,
-      refresh_token, token_expiry, token_uri,
-      user_agent)
+        access_token, client_id, client_secret,
+        refresh_token, token_expiry, GOOGLE_TOKEN_URI,
+        user_agent, revoke_uri=GOOGLE_REVOKE_URI)
 
   def test_token_refresh_success(self):
     for status_code in REFRESH_STATUS_CODES:
@@ -112,7 +148,7 @@
         ({'status': '200'}, 'echo_request_headers'),
         ])
       http = self.credentials.authorize(http)
-      resp, content = http.request("http://example.com")
+      resp, content = http.request('http://example.com')
       self.assertEqual('Bearer 1/3w', content['Authorization'])
       self.assertFalse(self.credentials.access_token_expired)
 
@@ -124,18 +160,28 @@
         ])
       http = self.credentials.authorize(http)
       try:
-        http.request("http://example.com")
-        self.fail("should raise AccessTokenRefreshError exception")
+        http.request('http://example.com')
+        self.fail('should raise AccessTokenRefreshError exception')
       except AccessTokenRefreshError:
         pass
       self.assertTrue(self.credentials.access_token_expired)
 
+  def test_token_revoke_success(self):
+    _token_revoke_test_helper(
+        self, '200', revoke_raise=False,
+        valid_bool_value=True, token_attr='refresh_token')
+
+  def test_token_revoke_failure(self):
+    _token_revoke_test_helper(
+        self, '400', revoke_raise=True,
+        valid_bool_value=False, token_attr='refresh_token')
+
   def test_non_401_error_response(self):
     http = HttpMockSequence([
       ({'status': '400'}, ''),
       ])
     http = self.credentials.authorize(http)
-    resp, content = http.request("http://example.com")
+    resp, content = http.request('http://example.com')
     self.assertEqual(400, resp.status)
 
   def test_to_from_json(self):
@@ -153,17 +199,16 @@
     client_secret = u'cOuDdkfjxxnv+'
     refresh_token = u'1/0/a.df219fjls0'
     token_expiry = unicode(datetime.datetime.utcnow())
-    token_uri = u'https://www.google.com/accounts/o8/oauth2/token'
+    token_uri = unicode(GOOGLE_TOKEN_URI)
+    revoke_uri = unicode(GOOGLE_REVOKE_URI)
     user_agent = u'refresh_checker/1.0'
     credentials = OAuth2Credentials(access_token, client_id, client_secret,
                                     refresh_token, token_expiry, token_uri,
-                                    user_agent)
+                                    user_agent, revoke_uri=revoke_uri)
 
     http = HttpMock(headers={'status': '200'})
     http = credentials.authorize(http)
-    http.request(u'http://example.com', method=u'GET', headers={
-        u'foo': u'bar'
-        })
+    http.request(u'http://example.com', method=u'GET', headers={u'foo': u'bar'})
     for k, v in http.headers.iteritems():
       self.assertEqual(str, type(k))
       self.assertEqual(str, type(v))
@@ -180,9 +225,10 @@
 class AccessTokenCredentialsTests(unittest.TestCase):
 
   def setUp(self):
-    access_token = "foo"
-    user_agent = "refresh_checker/1.0"
-    self.credentials = AccessTokenCredentials(access_token, user_agent)
+    access_token = 'foo'
+    user_agent = 'refresh_checker/1.0'
+    self.credentials = AccessTokenCredentials(access_token, user_agent,
+                                              revoke_uri=GOOGLE_REVOKE_URI)
 
   def test_token_refresh_success(self):
     for status_code in REFRESH_STATUS_CODES:
@@ -191,12 +237,22 @@
         ])
       http = self.credentials.authorize(http)
       try:
-        resp, content = http.request("http://example.com")
-        self.fail("should throw exception if token expires")
+        resp, content = http.request('http://example.com')
+        self.fail('should throw exception if token expires')
       except AccessTokenCredentialsError:
         pass
       except Exception:
-        self.fail("should only throw AccessTokenCredentialsError")
+        self.fail('should only throw AccessTokenCredentialsError')
+
+  def test_token_revoke_success(self):
+    _token_revoke_test_helper(
+        self, '200', revoke_raise=False,
+        valid_bool_value=True, token_attr='access_token')
+
+  def test_token_revoke_failure(self):
+    _token_revoke_test_helper(
+        self, '400', revoke_raise=True,
+        valid_bool_value=False, token_attr='access_token')
 
   def test_non_401_error_response(self):
     http = HttpMockSequence([
@@ -216,8 +272,8 @@
 
 
 class TestAssertionCredentials(unittest.TestCase):
-  assertion_text = "This is the assertion"
-  assertion_type = "http://www.google.com/assertionType"
+  assertion_text = 'This is the assertion'
+  assertion_type = 'http://www.google.com/assertionType'
 
   class AssertionCredentialsTestImpl(AssertionCredentials):
 
@@ -225,7 +281,7 @@
       return TestAssertionCredentials.assertion_text
 
   def setUp(self):
-    user_agent = "fun/2.0"
+    user_agent = 'fun/2.0'
     self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
         user_agent=user_agent)
 
@@ -240,11 +296,34 @@
       ({'status': '200'}, 'echo_request_headers'),
       ])
     http = self.credentials.authorize(http)
-    resp, content = http.request("http://example.com")
+    resp, content = http.request('http://example.com')
     self.assertEqual('Bearer 1/3w', content['Authorization'])
 
+  def test_token_revoke_success(self):
+    _token_revoke_test_helper(
+        self, '200', revoke_raise=False,
+        valid_bool_value=True, token_attr='access_token')
 
-class ExtractIdTokenText(unittest.TestCase):
+  def test_token_revoke_failure(self):
+    _token_revoke_test_helper(
+        self, '400', revoke_raise=True,
+        valid_bool_value=False, token_attr='access_token')
+
+
+class UpdateQueryParamsTest(unittest.TestCase):
+  def test_update_query_params_no_params(self):
+    uri = 'http://www.google.com'
+    updated = _update_query_params(uri, {'a': 'b'})
+    self.assertEqual(updated, uri + '?a=b')
+
+  def test_update_query_params_existing_params(self):
+    uri = 'http://www.google.com?x=y'
+    updated = _update_query_params(uri, {'a': 'b', 'c': 'd&'})
+    hardcoded_update = uri + '&a=b&c=d%26'
+    assertUrisEqual(self, updated, hardcoded_update)
+
+
+class ExtractIdTokenTest(unittest.TestCase):
   """Tests _extract_id_token()."""
 
   def test_extract_success(self):
@@ -272,13 +351,14 @@
         scope='foo',
         redirect_uri=OOB_CALLBACK_URN,
         user_agent='unittest-sample/1.0',
+        revoke_uri='dummy_revoke_uri',
         )
 
   def test_construct_authorize_url(self):
     authorize_url = self.flow.step1_get_authorize_url()
 
     parsed = urlparse.urlparse(authorize_url)
-    q = parse_qs(parsed[4])
+    q = urlparse.parse_qs(parsed[4])
     self.assertEqual('client_id+1', q['client_id'][0])
     self.assertEqual('code', q['response_type'][0])
     self.assertEqual('foo', q['scope'][0])
@@ -299,7 +379,7 @@
     authorize_url = flow.step1_get_authorize_url()
 
     parsed = urlparse.urlparse(authorize_url)
-    q = parse_qs(parsed[4])
+    q = urlparse.parse_qs(parsed[4])
     self.assertEqual('client_id+1', q['client_id'][0])
     self.assertEqual('token', q['response_type'][0])
     self.assertEqual('foo', q['scope'][0])
@@ -313,23 +393,23 @@
 
     try:
       credentials = self.flow.step2_exchange('some random code', http=http)
-      self.fail("should raise exception if exchange doesn't get 200")
+      self.fail('should raise exception if exchange doesn\'t get 200')
     except FlowExchangeError:
       pass
 
   def test_urlencoded_exchange_failure(self):
     http = HttpMockSequence([
-      ({'status': '400'}, "error=invalid_request"),
+      ({'status': '400'}, 'error=invalid_request'),
     ])
 
     try:
       credentials = self.flow.step2_exchange('some random code', http=http)
-      self.fail("should raise exception if exchange doesn't get 200")
+      self.fail('should raise exception if exchange doesn\'t get 200')
     except FlowExchangeError, e:
       self.assertEquals('invalid_request', str(e))
 
   def test_exchange_failure_with_json_error(self):
-    # Some providers have "error" attribute as a JSON object
+    # Some providers have 'error' attribute as a JSON object
     # in place of regular string.
     # This test makes sure no strange object-to-string coversion
     # exceptions are being raised instead of FlowExchangeError.
@@ -342,7 +422,7 @@
 
     try:
       credentials = self.flow.step2_exchange('some random code', http=http)
-      self.fail("should raise exception if exchange doesn't get 200")
+      self.fail('should raise exception if exchange doesn\'t get 200')
     except FlowExchangeError, e:
       pass
 
@@ -358,10 +438,11 @@
     self.assertEqual('SlAV32hkKG', credentials.access_token)
     self.assertNotEqual(None, credentials.token_expiry)
     self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
+    self.assertEqual('dummy_revoke_uri', credentials.revoke_uri)
 
   def test_urlencoded_exchange_success(self):
     http = HttpMockSequence([
-      ({'status': '200'}, "access_token=SlAV32hkKG&expires_in=3600"),
+      ({'status': '200'}, 'access_token=SlAV32hkKG&expires_in=3600'),
     ])
 
     credentials = self.flow.step2_exchange('some random code', http=http)
@@ -370,9 +451,9 @@
 
   def test_urlencoded_expires_param(self):
     http = HttpMockSequence([
-      # Note the "expires=3600" where you'd normally
-      # have if named "expires_in"
-      ({'status': '200'}, "access_token=SlAV32hkKG&expires=3600"),
+      # Note the 'expires=3600' where you'd normally
+      # have if named 'expires_in'
+      ({'status': '200'}, 'access_token=SlAV32hkKG&expires=3600'),
     ])
 
     credentials = self.flow.step2_exchange('some random code', http=http)
@@ -391,7 +472,7 @@
     http = HttpMockSequence([
       # This might be redundant but just to make sure
       # urlencoded access_token gets parsed correctly
-      ({'status': '200'}, "access_token=SlAV32hkKG"),
+      ({'status': '200'}, 'access_token=SlAV32hkKG'),
     ])
 
     credentials = self.flow.step2_exchange('some random code', http=http)
@@ -456,15 +537,15 @@
     self.redirect_uri = 'postmessage'
 
   def test_exchange_code_for_token(self):
+    token = 'asdfghjkl'
+    payload =simplejson.dumps({'access_token': token, 'expires_in': 3600})
     http = HttpMockSequence([
-      ({'status': '200'},
-      """{ "access_token":"asdfghjkl",
-       "expires_in":3600 }"""),
+      ({'status': '200'}, payload),
     ])
     credentials = credentials_from_code(self.client_id, self.client_secret,
         self.scope, self.code, redirect_uri=self.redirect_uri,
         http=http)
-    self.assertEquals(credentials.access_token, 'asdfghjkl')
+    self.assertEquals(credentials.access_token, token)
     self.assertNotEqual(None, credentials.token_expiry)
 
   def test_exchange_code_for_token_fail(self):
@@ -476,7 +557,7 @@
       credentials = credentials_from_code(self.client_id, self.client_secret,
           self.scope, self.code, redirect_uri=self.redirect_uri,
           http=http)
-      self.fail("should raise exception if exchange doesn't get 200")
+      self.fail('should raise exception if exchange doesn\'t get 200')
     except FlowExchangeError:
       pass
 
@@ -500,8 +581,8 @@
     load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
 
     credentials = credentials_from_clientsecrets_and_code(
-                            'some_secrets', self.scope,
-                            self.code, http=http, cache=cache_mock)
+        'some_secrets', self.scope,
+        self.code, http=http, cache=cache_mock)
     self.assertEquals(credentials.access_token, 'asdfghjkl')
 
   def test_exchange_code_and_file_for_token_fail(self):
@@ -513,7 +594,7 @@
       credentials = credentials_from_clientsecrets_and_code(
                             datafile('client_secrets.json'), self.scope,
                             self.code, http=http)
-      self.fail("should raise exception if exchange doesn't get 200")
+      self.fail('should raise exception if exchange doesn\'t get 200')
     except FlowExchangeError:
       pass