Loading of client_secrets JSON file backed by a cache.

Contributed by crhyme.

Reviwed in http://codereview.appspot.com/6349087/.
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 49433df..faccccf 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -36,6 +36,7 @@
 
 from apiclient.http import HttpMockSequence
 from oauth2client.anyjson import simplejson
+from oauth2client.clientsecrets import _loadfile
 from oauth2client.client import AccessTokenCredentials
 from oauth2client.client import AccessTokenCredentialsError
 from oauth2client.client import AccessTokenRefreshError
@@ -50,12 +51,29 @@
 from oauth2client.client import _extract_id_token
 from oauth2client.client import credentials_from_code
 from oauth2client.client import credentials_from_clientsecrets_and_code
+from oauth2client.client import flow_from_clientsecrets
 
 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
 
 def datafile(filename):
   return os.path.join(DATA_DIR, filename)
 
+def load_and_cache(existing_file, fakename, cache_mock):
+  client_type, client_info = _loadfile(datafile(existing_file))
+  cache_mock.cache[fakename] = {client_type: client_info}
+
+class CacheMock(object):
+    def __init__(self):
+      self.cache = {}
+
+    def get(self, key, namespace=''):
+      # ignoring namespace for easier testing
+      return self.cache.get(key, None)
+
+    def set(self, key, value, namespace=''):
+      # ignoring namespace for easier testing
+      self.cache[key] = value
+
 
 class CredentialsTests(unittest.TestCase):
 
@@ -375,6 +393,16 @@
     credentials = self.flow.step2_exchange('some random code', http)
     self.assertEqual(credentials.id_token, body)
 
+class FlowFromCachedClientsecrets(unittest.TestCase):  
+
+  def test_flow_from_clientsecrets_cached(self):
+    cache_mock = CacheMock()
+    load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
+    
+    # flow_from_clientsecrets(filename, scope, message=None, cache=None)
+    flow = flow_from_clientsecrets('some_secrets', '', cache=cache_mock)
+    self.assertEquals('foo_client_secret', flow.client_secret)
+
 class CredentialsFromCodeTests(unittest.TestCase):
   def setUp(self):
     self.client_id = 'client_id_abc'
@@ -421,6 +449,18 @@
     self.assertEquals(credentials.access_token, 'asdfghjkl')
     self.assertNotEqual(None, credentials.token_expiry)
 
+  def test_exchange_code_and_cached_file_for_token(self):
+    http = HttpMockSequence([
+      ({'status': '200'}, '{ "access_token":"asdfghjkl"}'),
+      ])
+    cache_mock = CacheMock()
+    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)
+    self.assertEquals(credentials.access_token, 'asdfghjkl')
+
   def test_exchange_code_and_file_for_token_fail(self):
     http = HttpMockSequence([
       ({'status': '400'}, '{"error":"invalid_request"}'),
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index edc5996..58f3e55 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -50,6 +50,7 @@
 from google.appengine.ext import testbed
 from google.appengine.runtime import apiproxy_errors
 from oauth2client.anyjson import simplejson
+from oauth2client.clientsecrets import _loadfile
 from oauth2client.appengine import AppAssertionCredentials
 from oauth2client.appengine import CredentialsModel
 from oauth2client.appengine import FlowProperty
@@ -72,6 +73,24 @@
   return os.path.join(DATA_DIR, filename)
 
 
+def load_and_cache(existing_file, fakename, cache_mock):
+  client_type, client_info = _loadfile(datafile(existing_file))
+  cache_mock.cache[fakename] = {client_type: client_info}
+
+
+class CacheMock(object):
+    def __init__(self):
+      self.cache = {}
+
+    def get(self, key, namespace=''):
+      # ignoring namespace for easier testing
+      return self.cache.get(key, None)
+
+    def set(self, key, value, namespace=''):
+      # ignoring namespace for easier testing
+      self.cache[key] = value
+
+
 class UserMock(object):
   """Mock the app engine user service"""
 
@@ -439,6 +458,14 @@
     http = self.decorator.http()
     self.assertEquals('foo_access_token', http.request.credentials.access_token)
 
+  def test_decorator_from_cached_client_secrets(self):
+    cache_mock = CacheMock()
+    load_and_cache('client_secrets.json', 'secret', cache_mock)
+    decorator = oauth2decorator_from_clientsecrets(
+      # filename, scope, message=None, cache=None
+      'secret', '', cache=cache_mock)
+    self.assertFalse(decorator._in_error)
+
   def test_decorator_from_client_secrets_not_logged_in_required(self):
     decorator = oauth2decorator_from_clientsecrets(
         datafile('client_secrets.json'),
diff --git a/tests/test_oauth2client_clientsecrets.py b/tests/test_oauth2client_clientsecrets.py
index 20cdf14..f69fb36 100644
--- a/tests/test_oauth2client_clientsecrets.py
+++ b/tests/test_oauth2client_clientsecrets.py
@@ -26,6 +26,11 @@
 from oauth2client import clientsecrets
 
 
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+VALID_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
+INVALID_FILE = os.path.join(DATA_DIR, 'unfilled_client_secrets.json')
+NONEXISTENT_FILE = os.path.join(__file__, '..', 'afilethatisntthere.json')
+
 class OAuth2CredentialsTests(unittest.TestCase):
 
   def setUp(self):
@@ -69,12 +74,72 @@
 
   def test_load_by_filename(self):
     try:
-      clientsecrets.loadfile(os.path.join(__file__, '..',
-                             'afilethatisntthere.json'))
+      clientsecrets._loadfile(NONEXISTENT_FILE)
       self.fail('should fail to load a missing client_secrets file.')
     except clientsecrets.InvalidClientSecretsError, e:
       self.assertTrue(str(e).startswith('File'))
 
 
+class CachedClientsecretsTests(unittest.TestCase):
+
+  class CacheMock(object):
+    def __init__(self):
+      self.cache = {}
+      self.last_get_ns = None
+      self.last_set_ns = None
+
+    def get(self, key, namespace=''):
+      # ignoring namespace for easier testing
+      self.last_get_ns = namespace
+      return self.cache.get(key, None)
+
+    def set(self, key, value, namespace=''):
+      # ignoring namespace for easier testing
+      self.last_set_ns = namespace
+      self.cache[key] = value
+
+  def setUp(self):
+    self.cache_mock = self.CacheMock()
+
+  def test_cache_miss(self):
+    client_type, client_info = clientsecrets.loadfile(
+      VALID_FILE, cache=self.cache_mock)
+    self.assertEquals('web', client_type)
+    self.assertEquals('foo_client_secret', client_info['client_secret'])
+
+    cached = self.cache_mock.cache[VALID_FILE]
+    self.assertEquals({client_type: client_info}, cached)
+
+    # make sure we're using non-empty namespace
+    ns = self.cache_mock.last_set_ns
+    self.assertTrue(bool(ns))
+    # make sure they're equal
+    self.assertEquals(ns, self.cache_mock.last_get_ns)
+
+  def test_cache_hit(self):
+    self.cache_mock.cache[NONEXISTENT_FILE] = { 'web': 'secret info' }
+
+    client_type, client_info = clientsecrets.loadfile(
+      NONEXISTENT_FILE, cache=self.cache_mock)
+    self.assertEquals('web', client_type)
+    self.assertEquals('secret info', client_info)
+    # make sure we didn't do any set() RPCs
+    self.assertEqual(None, self.cache_mock.last_set_ns)
+
+  def test_validation(self):
+    try:
+      clientsecrets.loadfile(INVALID_FILE, cache=self.cache_mock)
+      self.fail('Expected InvalidClientSecretsError to be raised '
+                'while loading %s' % INVALID_FILE)
+    except clientsecrets.InvalidClientSecretsError:
+      pass
+
+  def test_without_cache(self):
+    # this also ensures loadfile() is backward compatible
+    client_type, client_info = clientsecrets.loadfile(VALID_FILE)
+    self.assertEquals('web', client_type)
+    self.assertEquals('foo_client_secret', client_info['client_secret'])
+
+
 if __name__ == '__main__':
   unittest.main()