Repository: TACC/agavepy

Issue:
Add new credentials format
Currently, the credentials for a user during a work session are stored, by default, in `~/.agave/current` and in case the user intends to work with multiple tenants there is the possibility to store another session in `~/.agave/backup`.

I want to propose we implement a better way to switch between contexts (agave tenants) by storing all sessions (credentials related to an oauth client) in the same file. The format could be as follows:

```
{                                                                               
    "current": {                                                                
        "access_token": "some-token",                      
        "apikey": "some-key",                               
        "apisecret": "some-secret",                            
        "baseurl": "https://api.sd2e.org/",                                     
        "created_at": 1531491260,                                               
        "devurl": "",                                                           
        "expires_at": "Fri Jul 13 13:14:20 CDT 2018",                           
        "expires_in": 14400,                                                    
        "refresh_token": "some-refresh-token",                    
        "tenantid": "sd2e",                                                     
        "username": "user"                                                    
    },                                                                          
    "tenants": {                                                                
        "3dem": {                                                               
            "access_token": "other-token",                                                 
            "apikey": "other-key",                                                       
            "apisecret": "other-secret",                                                    
            "baseurl": "https://api.3dem.org/",                                 
            "created_at": "123456",                                                   
            "devurl": "",                                                       
            "expires_at": "34234223",                                                   
            "expires_in": "14440",                                                   
            "refresh_token": "other-refresh-token",                                                
            "tenantid": "3dem",                                                 
            "username": "user"                                                      
        },                                                                      
        "sd2e": {
            "acces_token": "some-token",                      
            "apikey": "some-key",                               
            "apisecret": "some-secret",                            
            "baseurl": "https://api.sd2e.org/",                                     
            "created_at": 1531491260,                                               
            "devurl": "",                                                           
            "expires_at": "Fri Jul 13 13:14:20 CDT 2018",                           
            "expires_in": 14400,                                                    
            "refresh_token": "some-refresh-token",                    
            "tenantid": "sd2e",                                                     
            "username": "user"                                                                                                      
        }                                                                       
    }                                                                           
}
```

In this object, the key "current" holds the credentials for use in the current session. 
The key "tenants" holds the credentials for all sessions created by the user.

This change will be most visible when it propagates to the the command-line client `TACC-Cloud/agave-cli` in which each call to `tenants-init` will use an existing context if it exists.

If this is introduced at the next major release, we can include a message that informs users about the new format. Meanwhile, this and the existing format will be implemented to maintain backwards compatibility with all the tooling built around agave-cli.

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index 2735419..8ee50cd 100644
--- a/Makefile
+++ b/Makefile
@@ -26,6 +26,10 @@ clean:
 	rm -rf agavepy.egg-info build dist .cache
 	rm -rf schema openapi
 	rm -rf agavepy/__pycache__/
+	rm -rf agavepy/tenants/*.pyc agavepy/tenants/__pycache__/
+	rm -rf agavepy/clients/*.pyc agavepy/clients/__pycache__/
+	rm -rf agavepy/tokens/*.pyc agavepy/tokens/__pycache__/
+	rm -rf agavepy/utils/*.pyc agavepy/utils/__pycache__/
 	rm -rf agavepy/tests/__pycache__/
 	rm -rf agavepy/*.pyc
 	rm -rf tests/__pycache__/
diff --git a/README.rst b/README.rst
index 162fb75..f7b0e64 100644
--- a/README.rst
+++ b/README.rst
@@ -96,7 +96,7 @@ Agave tenant.
     tacc.prod            TACC                                     https://api.tacc.utexas.edu/
     vdjserver.org        VDJ Server                               https://vdj-agave-api.tacc.utexas.edu/
     
-    Please specify the url of a tenant to interact with: https://api.araport.org/
+    Please specify the ID of a tenant to interact with: araport.org
     >>> ag.api_server
     'https://api.araport.org/'
 
@@ -107,7 +107,14 @@ If you already now what tenant you want to work with, you can instantiate
 .. code-block:: pycon
 
    >>> from agavepy.agave import Agave
-   >>> ag = Agave(api_server='https://api.tacc.cloud')
+   >>> ag = Agave(api_server="https://api.tacc.cloud")
+
+or 
+
+.. code-block:: pycon
+
+    >>> from agavepy.agave import Agave
+    >>> ag = Agave(tenant_id="tacc.prod")
 
 Once the object is instantiated, interact with it according to the API 
 documentation and your specific usage needs. 
@@ -181,6 +188,22 @@ Keep in mind that you will need to create an oauth client first!
 
 
 
+Saving your credentials
+^^^^^^^^^^^^^^^^^^^^^^^
+
+To save your process (api key, api secret, access token, refresh token, tenant
+information) you can use the method ``Agave.save_configs()``
+
+.. code-block:: pycon
+
+    >>> ag.save_configs()
+
+By default, ``Agave.save_configs`` will store credentials in ``~/.agave``. 
+It will save all session in ``~/.agave/config.json`` and, for
+backwards-compatibility with other agave tooling, it will save the current
+session in ``~/.agave/current``.
+
+
 The refresh token
 ^^^^^^^^^^^^^^^^^
 
diff --git a/agavepy/agave.py b/agavepy/agave.py
index d8c91af..46e76fe 100644
--- a/agavepy/agave.py
+++ b/agavepy/agave.py
@@ -19,6 +19,10 @@ import jinja2
 import dateutil.parser
 import requests
 
+from .tenants import tenant_list
+from .clients import client_create
+from .tokens import token_create
+from .utils import save_config
 
 import sys
 sys.path.insert(0, os.path.dirname(__file__))
@@ -27,10 +31,6 @@ from .swaggerpy.client import SwaggerClient
 from .swaggerpy.http_client import SynchronousHttpClient
 from .swaggerpy.processors import SwaggerProcessor
 
-from .clients import client_create
-from .tenants import tenant_list
-from .tokens import token_create
-
 
 HERE = os.path.dirname(os.path.abspath(__file__))
 
@@ -224,26 +224,44 @@ class Agave(object):
     ]
 
     def __init__(self, **kwargs):
+        
         for param, mandatory, attr, default in self.PARAMS:
             try:
-                value = (kwargs[param] if mandatory
-                         else kwargs.get(param, default))
+                value = (kwargs[param] if mandatory 
+                        else kwargs.get(param, default))
             except KeyError:
-                # Request user to set the tenant url (api_server).
-                if param == "api_server":
-                    self.list_tenants(tenantsurl="https://api.tacc.utexas.edu/tenants")
-                    value = input(
-                        "\nPlease specify the url of a tenant to interact with: ")
-                    
-                    # If present, remove the last '/' from the url.
-                    if value[-1] == "/":
-                        value = value[:-1]
-
-                else:
-                    raise AgaveError("parameter \"{}\" is mandatory".format(param))
+                pass
 
             setattr(self, attr, value)
 
+        # The following sectionsets tenant ID (tenant_id) and tenant url 
+        # (api_server).
+        # Neither tenant ID nor tenant url are set.
+        if self.tenant_id is None and self.api_server is None:
+            tenants = self.list_tenants(tenantsurl="https://api.tacc.utexas.edu/tenants")
+            value = input("\nPlease specify the ID for the tenant you wish to interact with: ")
+            self.tenant_id  = tenants[value]["id"]
+            tenant_url = tenants[value]["url"]
+            if tenant_url[-1] == '/':
+                tenant_url = tenant_url[:-1]
+            self.api_server = tenant_url
+        # Tenant ID was not set.
+        elif self.tenant_id is None and self.api_server is not None:
+            tenants = tenant_list(tenantsurl="https://api.tacc.utexas.edu/tenants")
+
+            for _, tenant in tenants.items():
+                if self.api_server in tenant["url"]:
+                    self.tenant_id = tenant["id"]
+        # Tenant url was not set.
+        elif self.api_server is None and self.tenant_id is not None:
+            tenants = tenant_list(tenantsurl="https://api.tacc.utexas.edu/tenants")
+
+            tenant_url = tenants[self.tenant_id]["url"]
+            if tenant_url[-1] == '/':
+                tenant_url = tenant_url[:-1]
+            self.api_server = tenant_url
+    
+
         if self.resources is None:
             self.resources = load_resource(self.api_server)
         self.host = urllib.parse.urlsplit(self.api_server).netloc
@@ -518,6 +536,41 @@ class Agave(object):
         return list(set(base))
 
 
+    def save_configs(self, cache_dir=None):
+        """ Save configs
+
+        Save configuration to AGAVE_CACHE_DIR. This will update 
+        AGAVE_CACHE_DIR/current and AGAVE_CACHE_DIR/config.json.
+        
+        PARAMETERS
+        ----------
+        cache_dir: string (default: None)
+            If no cache_dir is passed it will default to ~/.agave.
+        """
+        current_context = {
+            "tenantid": self.tenant_id,
+            "baseurl": self.api_server,
+            "devurl": "",
+            "apisecret": self.api_secret,
+            "apikey": self.api_key,
+            "username": self.username,
+            "access_token": self.token,
+            "refresh_token": self.refresh_token,
+            "created_at": self.created_at,
+            "expires_in": self.expires_in,
+            "expires_at": self.expires_at,
+        }
+
+        # Convert None values to empty strings.
+        for k, v in current_context.items():
+            if v is None or isinstance(v, Resource):
+                current_context[k] = ""
+
+        if cache_dir is None:
+            cache_dir = os.path.expanduser("~/.agave")
+        save_config(cache_dir, current_context)
+
+
     def list_tenants(self, tenantsurl="https://api.tacc.utexas.edu/tenants"):
         """ List Agave tenants
 
@@ -526,7 +579,14 @@ class Agave(object):
         tenantsurl: string (default: "https://api.tacc.utexas.edu/tenants")
             Endpoint with Agave tenant information.
         """
-        tenant_list(tenantsurl)
+        tenants = tenant_list(tenantsurl)
+        print("{0:<20} {1:<40} {2:<50}".format("ID", "NAME", "URL"))
+        for _, tenant in tenants.items():
+            print("{0:<20} {1:<40} {2:<50}".format(
+                    tenant["id"], tenant["name"], tenant["url"]))
+
+        return tenants
+
 
     def clients_create(self, client_name, description):
         """ Create an Agave Oauth client
diff --git a/agavepy/clients/__init__.py b/agavepy/clients/__init__.py
new file mode 100644
index 0000000..d3c181e
--- /dev/null
+++ b/agavepy/clients/__init__.py
@@ -0,0 +1,1 @@
+from .clients import client_create
diff --git a/agavepy/clients.py b/agavepy/clients/clients.py
similarity index 97%
rename from agavepy/clients.py
rename to agavepy/clients/clients.py
index fb3220a..ea81239 100644
--- a/agavepy/clients.py
+++ b/agavepy/clients/clients.py
@@ -10,7 +10,7 @@ import json
 import requests
 import sys
 from os import path
-from utils import handle_bad_response_status_code
+from ..utils import handle_bad_response_status_code
 
 
 class AgaveClientError(Exception):
diff --git a/agavepy/tenants/__init__.py b/agavepy/tenants/__init__.py
new file mode 100644
index 0000000..d53de83
--- /dev/null
+++ b/agavepy/tenants/__init__.py
@@ -0,0 +1,1 @@
+from .tenants import tenant_list
diff --git a/agavepy/tenants.py b/agavepy/tenants/tenants.py
similarity index 73%
rename from agavepy/tenants.py
rename to agavepy/tenants/tenants.py
index 85a0222..196e20d 100644
--- a/agavepy/tenants.py
+++ b/agavepy/tenants/tenants.py
@@ -6,7 +6,7 @@ Methods to interact with Agave tenants.
 from __future__ import print_function
 import requests
 import sys
-from utils import handle_bad_response_status_code
+from ..utils import handle_bad_response_status_code
 
 
 def get_tenants(url):
@@ -52,9 +52,18 @@ def tenant_list(tenantsurl="https://api.tacc.utexas.edu/tenants"):
     # Get a json of all AGave tenants.
     tenants = get_tenants(tenantsurl)
 
-    # Print results.
-    print("{0:<20} {1:<40} {2:<50}".format("CODE", "NAME", "URL"))
+    # Create dictionary of tenants.
+    tenants_info = dict()
+
+    # Parse results.
     for tenant in tenants.get("result", []):
-        print("{0:<20} {1:<40} {2:<50}".format(
-            tenant.get("code", ""), tenant.get("name", ""), 
-            tenant.get("baseUrl", "")))
+        # code is the tenant id, name is the full name, baseUrl is the url.
+        tenant_info = {
+            "id": tenant.get("code", ""), 
+            "name": tenant.get("name", ""), 
+            "url": tenant.get("baseUrl", ""),
+        }
+        tenants_info[tenant_info["id"]] = tenant_info
+
+
+    return tenants_info
diff --git a/agavepy/tokens/__init__.py b/agavepy/tokens/__init__.py
new file mode 100644
index 0000000..9602360
--- /dev/null
+++ b/agavepy/tokens/__init__.py
@@ -0,0 +1,1 @@
+from .tokens import token_create
diff --git a/agavepy/tokens.py b/agavepy/tokens/tokens.py
similarity index 97%
rename from agavepy/tokens.py
rename to agavepy/tokens/tokens.py
index 0a0b59f..50a5f6e 100644
--- a/agavepy/tokens.py
+++ b/agavepy/tokens/tokens.py
@@ -10,7 +10,7 @@ import requests
 import sys
 import time
 from os import path
-from utils import handle_bad_response_status_code
+from ..utils import handle_bad_response_status_code
 
 
 class AgaveTokenError(Exception):
diff --git a/agavepy/utils/__init__.py b/agavepy/utils/__init__.py
index fb93af6..0aff1bb 100644
--- a/agavepy/utils/__init__.py
+++ b/agavepy/utils/__init__.py
@@ -1,1 +1,2 @@
+from .cachedir_helpers import save_config
 from .response_handlers import handle_bad_response_status_code
diff --git a/agavepy/utils/cachedir_helpers.py b/agavepy/utils/cachedir_helpers.py
new file mode 100644
index 0000000..2f0af07
--- /dev/null
+++ b/agavepy/utils/cachedir_helpers.py
@@ -0,0 +1,105 @@
+"""
+    cachedir_helpers.py
+"""
+from __future__ import print_function
+import json
+import requests
+import sys
+import os
+
+
+def make_cache_dir(cache_dir):
+    """ Create cache dir
+
+    Create agave cache dir in in the specified location, if it doesn't already
+    exist.
+
+    PARAMETERS
+    ----------
+    cache_dir: str
+        Location to make cache dir.
+    """
+    if not os.path.exists(cache_dir):
+        os.makedirs(cache_dir)
+
+
+def save_config(cache_dir, current_context):
+    """ Initiate an Agave Tenant
+
+    Create or switch the current context to a specified Agave tenant.
+    The current context along with all previous used are stored in a
+    local database (arguments.agavedb).
+
+    The ~/.agave/config.json file will have the following format:
+        * "current" will specify the configuration to be used for the current 
+          session. The contents of this section should match those of 
+          ~/.agave/current.
+        * "tenants" will have one or more keys, and each key will have a json 
+          object related to it. Each key will correspond to a tenant id.
+
+    For example:
+    {
+        "current": {
+            "access_token": "some-token",
+            ...
+            "username": "user"
+        },
+        "tenants": {
+            "3dem": {
+                "access_token": "other-token",
+                ...
+                "username": "user"
+            },
+            "sd2e": {
+                "acces_token": "some-token",
+                ...
+                "usernamer":user"
+            }
+        }
+    }
+
+    PARAMETERS
+    ----------
+    """
+    # Get location to store configuration.
+    make_cache_dir(cache_dir)
+    current_file = "{}/current".format(cache_dir)
+    config_file = "{}/config.json".format(cache_dir)
+    config_files = [current_file, config_file]
+
+
+    # Read in configuration from cache dir if it exist, else create one.
+    if os.path.isfile(config_file):
+        with open(config_file, "r") as f:
+            agave_context = json.load(f)
+    else:
+        agave_context = dict()
+
+
+    # Set up ~/.agave/config.json
+
+    # We are saving configurations for the first time so we have to set 
+    # "current" and add it to "tenants".
+    if "tenants" not in agave_context:
+        # No existing tenants, so we just add the current context.
+        agave_context["current"] = current_context
+        
+        # Create an empty dictionary for "tenants" key.
+        agave_context["tenants"] = dict()
+        # Save current tenant context.
+        agave_context["tenants"][current_context["tenantid"]] = agave_context["current"]
+    # "tenants" already exist so we just have to put the current context
+    # back in.
+    else:
+        # Save current tenant context.
+        agave_context["tenants"][agave_context["current"]["tenantid"]] = agave_context["current"]
+
+        # Save current_context as such.
+        agave_context["current"] =  current_context
+
+
+    # Save data to cache dir files.
+    with open(config_file, "w") as f:
+        json.dump(agave_context, f, sort_keys=True, indent=4)
+    with open(current_file, "w") as f:
+        json.dump(agave_context["current"], f, sort_keys=True)
diff --git a/agavepy/utils/response_handlers.py b/agavepy/utils/response_handlers.py
index e98a8dc..859f0f0 100644
--- a/agavepy/utils/response_handlers.py
+++ b/agavepy/utils/response_handlers.py
@@ -21,7 +21,7 @@ def handle_bad_response_status_code(r):
     """
     if not r.ok:
         error_msg = "Bad {0} request to {1}, status code {2}\n".format(
-            r.request.method, r.url, r.status_code),                        
+            r.request.method, r.url, r.status_code)
         error_msg += "{}\n".format(r.request.body)
         error_msg += "{}\n".format(r.json())
 


Test patch:
diff --git a/tests/clients_test.py b/tests/clients_test.py
index be061b1..6d0d3a5 100644
--- a/tests/clients_test.py
+++ b/tests/clients_test.py
@@ -90,7 +90,7 @@ class TestMockServer(MockServer):
 
 
     @patch("agavepy.agave.input")
-    @patch("clients.getpass.getpass")
+    @patch("agavepy.clients.clients.getpass.getpass")
     def test_client_create(self, mock_input, mock_pass):
         """ Test client create op
 
diff --git a/tests/get_access_token_test.py b/tests/get_access_token_test.py
index db72b2d..0624de6 100644
--- a/tests/get_access_token_test.py
+++ b/tests/get_access_token_test.py
@@ -92,7 +92,7 @@ class TestMockServer(MockServer):
         MockServer.serve.__func__(cls, MockServerTokenEndpoints)
 
 
-    @patch("tokens.getpass.getpass")
+    @patch("agavepy.tokens.tokens.getpass.getpass")
     def test_get_access_token(self, mock_pass):
         """ Test client create op
 
diff --git a/tests/initialize_agave_test.py b/tests/initialize_agave_test.py
new file mode 100644
index 0000000..796703c
--- /dev/null
+++ b/tests/initialize_agave_test.py
@@ -0,0 +1,46 @@
+"""
+    initialize_agave_test.py
+
+Test initialization of Agave object.    
+"""
+import pytest
+import json
+import os
+import sys
+from response_templates import response_template_to_json
+
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+from agavepy.agave import Agave
+
+
+# The agave API will provide a response with the following format upon a
+# successfull attempt at listing tenants.
+sample_tenants_list_response = response_template_to_json("tenants-list.json")
+
+
+class TestAgaveInitialization:
+    """ Test client-related agave api endpoints 
+
+    Tests client creation (HTTP POST request), removal (HTTP DELETE request), 
+    and listing (HTTP GET request).
+    """
+    
+    @patch("agavepy.tenants.tenants.get_tenants")
+    @patch("agavepy.agave.input")
+    def test_Agave_init(self, mock_get_tenants, mock_input):
+        """ Test Agave initialization
+        """
+        # Patch list_tenants function and input.
+        mock_input.return_value = sample_tenants_list_response
+        mock_get_tenants.return_value = "sd2e"
+
+        # Instantiate Agave object making reference to local mock server.
+        ag = Agave()
+
+        assert ag.tenant_id == "sd2e"
+        assert ag.api_server == "https://api.sd2e.org"
diff --git a/tests/sample_responses/sample-config.json b/tests/sample_responses/sample-config.json
new file mode 100644
index 0000000..cfb1ae4
--- /dev/null
+++ b/tests/sample_responses/sample-config.json
@@ -0,0 +1,43 @@
+{
+  "current": {
+    "access_token": "",
+    "apikey": "",
+    "apisecret": "",
+    "baseurl": "https://api.sd2e.org",
+    "created_at": "",
+    "devurl": "",
+    "expires_at": "",
+    "expires_in": "",
+    "refresh_token": "",
+    "tenantid": "sd2e",
+    "username": ""
+  },
+  "tenants": {
+    "irec": {
+      "access_token": "",
+      "apikey": "",
+      "apisecret": "",
+      "baseurl": "https://irec.tenants.prod.tacc.cloud",
+      "created_at": "",
+      "devurl": "",
+      "expires_at": "",
+      "expires_in": "",
+      "refresh_token": "",
+      "tenantid": "irec",
+      "username": ""
+    },
+    "sd2e": {
+      "access_token": "",
+      "apikey": "",
+      "apisecret": "",
+      "baseurl": "https://api.sd2e.org",
+      "created_at": "",
+      "devurl": "",
+      "expires_at": "",
+      "expires_in": "",
+      "refresh_token": "",
+      "tenantid": "sd2e",
+      "username": ""
+    }
+  }
+}
diff --git a/tests/sample_responses/tenants-list.json b/tests/sample_responses/tenants-list.json
new file mode 100644
index 0000000..11125d7
--- /dev/null
+++ b/tests/sample_responses/tenants-list.json
@@ -0,0 +1,260 @@
+{
+  "status": "success",
+  "message": "",
+  "result": [
+    {
+      "id": "6601411570898817-b0b0b0bb0b-0001-016",
+      "name": "3dem Tenant",
+      "baseUrl": "https://api.3dem.org/",
+      "code": "3dem",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/3dem"
+        },
+        "publickey": {
+          "href": "https://api.3dem.org/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "4201449684007961-b0b0b0bb0b-0001-016",
+      "name": "Agave Public Tenant",
+      "baseUrl": "https://public.agaveapi.co/",
+      "code": "agave.prod",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/agave.prod"
+        },
+        "publickey": {
+          "href": "https://public.agaveapi.co/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898826-b0b0b0bb0b-0001-016",
+      "name": "Araport",
+      "baseUrl": "https://api.araport.org/",
+      "code": "araport.org",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/araport.org"
+        },
+        "publickey": {
+          "href": "https://api.araport.org/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "3201449684007961-b0b0b0bb0b-0001-016",
+      "name": "DesignSafe",
+      "baseUrl": "https://agave.designsafe-ci.org/",
+      "code": "designsafe",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/designsafe"
+        },
+        "publickey": {
+          "href": "https://agave.designsafe-ci.org/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898781-b0b0b0bb0b-0001-016",
+      "name": "CyVerse Science APIs",
+      "baseUrl": "https://agave.iplantc.org/",
+      "code": "iplantc.org",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/iplantc.org"
+        },
+        "publickey": {
+          "href": "https://agave.iplantc.org/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898837-b0b0b0bb0b-0001-016",
+      "name": "iReceptor",
+      "baseUrl": "https://irec.tenants.prod.tacc.cloud/",
+      "code": "irec",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@sfu.ca",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/irec"
+        },
+        "publickey": {
+          "href": "https://irec.tenants.prod.tacc.cloud/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898818-b0b0b0bb0b-0001-016",
+      "name": "Portals Tenant",
+      "baseUrl": "https://portals-api.tacc.utexas.edu/",
+      "code": "portals",
+      "contact": [
+        {
+          "name": "Portals Admin",
+          "email": "admin@example.com",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/portals"
+        },
+        "publickey": {
+          "href": "https://portals-api.tacc.utexas.edu/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898817-b0b0b0bb0b-0001-016",
+      "name": "SD2E Tenant",
+      "baseUrl": "https://api.sd2e.org/",
+      "code": "sd2e",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/sd2e"
+        },
+        "publickey": {
+          "href": "https://api.sd2e.org/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "5603147879475572250-242ac113-0001-016",
+      "name": "Science Gateways Community Institute",
+      "baseUrl": "https://sgci.tacc.cloud/",
+      "code": "sgci",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@sciencegateways.org",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/sgci"
+        },
+        "publickey": {
+          "href": "https://sgci.tacc.cloud/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001423978050162-b0b0b0bb0b-0001-016",
+      "name": "TACC",
+      "baseUrl": "https://api.tacc.utexas.edu/",
+      "code": "tacc.prod",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/tacc.prod"
+        },
+        "publickey": {
+          "href": "https://api.tacc.utexas.edu/apim/v2/publickey"
+        }
+      }
+    },
+    {
+      "id": "0001411570898793-b0b0b0bb0b-0001-016",
+      "name": "VDJ Server",
+      "baseUrl": "https://vdj-agave-api.tacc.utexas.edu/",
+      "code": "vdjserver.org",
+      "contact": [
+        {
+          "name": "user user",
+          "email": "user@tacc.utexas.edu",
+          "url": "",
+          "type": "admin",
+          "primary": true
+        }
+      ],
+      "_links": {
+        "self": {
+          "href": "https://docker.example.com/tenants/v2/vdjserver.org"
+        },
+        "publickey": {
+          "href": "https://vdj-agave-api.tacc.utexas.edu/apim/v2/publickey"
+        }
+      }
+    }
+  ],
+  "version": "2.2.22-r7deb380"
+}
diff --git a/tests/save_configs_test.py b/tests/save_configs_test.py
new file mode 100644
index 0000000..bb95c65
--- /dev/null
+++ b/tests/save_configs_test.py
@@ -0,0 +1,66 @@
+"""
+    save_configs_test.py
+
+Test save configuration function.    
+"""
+import pytest
+import json
+import os
+import shutil
+import sys
+import tempfile
+from response_templates import response_template_to_json
+
+try:
+    from unittest.mock import patch
+except ImportError:
+    from mock import patch
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+from agavepy.agave import Agave
+
+
+# The agave API will provide a response with the following format upon a
+# successfull attempt at listing tenants.
+sample_tenants_list_response = response_template_to_json("tenants-list.json")
+
+# Sample configuration file using tenants irec and sd2e (sd2e is in current).
+sample_config = response_template_to_json("sample-config.json")
+
+
+class TestSaveConfigs:
+    """ Test client-related agave api endpoints 
+
+    Tests client creation (HTTP POST request), removal (HTTP DELETE request), 
+    and listing (HTTP GET request).
+    """
+    
+    @patch("agavepy.tenants.tenants.get_tenants")
+    def test_save_configs(self, mock_get_tenants):
+        """ Test Agave initialization
+        """
+        # Patch list_tenants function and input.
+        mock_get_tenants.return_value = sample_tenants_list_response
+
+
+        # Instantiate Agave objects and save configurations to cache dir.
+        try:
+            # create a tmp dir and use it as a cache dir.
+            cache_dir = tempfile.mkdtemp()
+
+            a = Agave(tenant_id="sd2e")
+            a.save_configs(cache_dir)
+            b = Agave(tenant_id="irec")
+            b.save_configs(cache_dir)
+            c = Agave(tenant_id="sd2e")
+            c.save_configs(cache_dir)
+
+            with open("{}/config.json".format(cache_dir), "r") as f:
+                config = json.load(f)
+
+            print(config)
+            print()
+            print(sample_config)
+            assert config == sample_config 
+        finally:
+            shutil.rmtree(cache_dir)