Repository: globus/globus-sdk-python
Pull request #186 (2017-04-13 17:15:57)

AuthAPIError tests
Updated some existing tests to use the AuthAPIError class, and added a few unit tests to test_exc.py


Patch:
diff --git a/docs/examples/native_app.rst b/docs/examples/native_app.rst
index 9f39b8ec..27b0b239 100644
--- a/docs/examples/native_app.rst
+++ b/docs/examples/native_app.rst
@@ -1,5 +1,3 @@
-.. _examples_native_app_login:
-
 Native App Login
 ----------------
 
diff --git a/docs/examples/three_legged_oauth.rst b/docs/examples/three_legged_oauth.rst
index ca36ef65..5a98e408 100644
--- a/docs/examples/three_legged_oauth.rst
+++ b/docs/examples/three_legged_oauth.rst
@@ -1,5 +1,3 @@
-.. _examples_three_legged_oauth_login:
-
 Three Legged OAuth with Flask
 -----------------------------
 
diff --git a/globus_sdk/auth/client_types/confidential_client.py b/globus_sdk/auth/client_types/confidential_client.py
index fa920c42..6126ada7 100644
--- a/globus_sdk/auth/client_types/confidential_client.py
+++ b/globus_sdk/auth/client_types/confidential_client.py
@@ -1,6 +1,4 @@
 import logging
-import six
-
 from globus_sdk.base import merge_params
 from globus_sdk.authorizers import BasicAuthorizer
 from globus_sdk.auth.oauth2_constants import DEFAULT_REQUESTED_SCOPES
@@ -74,9 +72,6 @@ class ConfidentialAppAuthClient(AuthClient):
         """
         self.logger.info('Fetching token(s) using client credentials')
         requested_scopes = requested_scopes or DEFAULT_REQUESTED_SCOPES
-        # convert scopes iterable to string immediately on load
-        if not isinstance(requested_scopes, six.string_types):
-            requested_scopes = " ".join(requested_scopes)
 
         return self.oauth2_token({
             'grant_type': 'client_credentials',
@@ -86,43 +81,12 @@ class ConfidentialAppAuthClient(AuthClient):
             self, redirect_uri, requested_scopes=None,
             state='_default', refresh_tokens=False):
         """
-        Starts or resumes an Authorization Code OAuth2 flow.
-
-        Under the hood, this is done by instantiating a
+        Starts an Authorization Code OAuth2 flow by instantiating a
         :class:`GlobusAuthorizationCodeFlowManager
         <globus_sdk.auth.GlobusAuthorizationCodeFlowManager>`
 
-        **Parameters**
-
-            ``redirect_uri`` (*string*)
-              The page that users should be directed to after authenticating at
-              the authorize URL. Required.
-
-            ``requested_scopes`` (*iterable* or *string*)
-              The scopes on the token(s) being requested, as a space-separated
-              string or an iterable of strings. Defaults to ``openid profile
-              email urn:globus:auth:scope:transfer.api.globus.org:all``
-
-            ``state`` (*string*)
-              This is a way of your application passing information back to
-              itself in the course of the OAuth flow. Because the user will
-              navigate away from your application to complete the flow, this
-              parameter lets you pass an arbitrary string from the starting
-              page to the ``redirect_uri``
-
-            ``refresh_tokens`` (*bool*)
-              When True, request refresh tokens in addition to access tokens
-
-        **Examples**
-
-        You can see an example of this flow :ref:`in the usage examples
-        <examples_three_legged_oauth_login>`
-
-        **External Documentation**
-
-        The Authorization Code Grant flow is described
-        `in the Globus Auth Specification \
-        <https://docs.globus.org/api/auth/developer-guide/#obtaining-authorization>`_
+        All of the parameters to this method are passed to that class's
+        initializer verbatim.
         """
         self.logger.info('Starting OAuth2 Authorization Code Grant Flow')
         self.current_oauth2_flow_manager = GlobusAuthorizationCodeFlowManager(
diff --git a/globus_sdk/auth/client_types/native_client.py b/globus_sdk/auth/client_types/native_client.py
index 20f55c58..818269ab 100644
--- a/globus_sdk/auth/client_types/native_client.py
+++ b/globus_sdk/auth/client_types/native_client.py
@@ -40,59 +40,14 @@ class NativeAppAuthClient(AuthClient):
             state='_default', verifier=None, refresh_tokens=False,
             prefill_named_grant=None):
         """
-        Starts a Native App OAuth2 flow.
-
-        This is done internally by instantiating a
+        Starts a Native App OAuth2 flow by instantiating a
         :class:`GlobusNativeAppFlowManager
         <globus_sdk.auth.GlobusNativeAppFlowManager>`
 
-        While the flow is in progress, the ``NativeAppAuthClient`` becomes
-        non thread-safe as temporary state is stored during the flow.
-
-        **Parameters**
-
-            ``requested_scopes`` (*iterable* or *string*)
-              The scopes on the token(s) being requested, as a space-separated
-              string or iterable of strings. Defaults to ``openid profile email
-              urn:globus:auth:scope:transfer.api.globus.org:all``
-
-            ``redirect_uri`` (*string*)
-              The page that users should be directed to after authenticating at
-              the authorize URL. Defaults to
-              'https://auth.globus.org/v2/web/auth-code', which displays the
-              resulting ``auth_code`` for users to copy-paste back into your
-              application (and thereby be passed back to the
-              ``GlobusNativeAppFlowManager``)
-
-            ``state`` (*string*)
-              Typically is not meaningful in the Native App Grant flow, but you
-              may have a specialized use case for it. The ``redirect_uri`` page
-              will have this included in a query parameter, so you can use it
-              to pass information to that page. It defaults to the string
-              '_default'
-
-            ``verifier`` (*string*)
-              A secret used for the Native App flow. It will by default be a
-              freshly generated random string, known only to this
-              ``GlobusNativeAppFlowManager`` instance
-
-            ``refresh_tokens`` (*bool*)
-              When True, request refresh tokens in addition to access tokens
-
-            ``prefill_named_grant`` (*string*)
-              Optionally prefill the named grant label on the consent page
-
-        **Examples**
-
-        You can see an example of this flow :ref:`in the usage examples
-        <examples_native_app_login>`
-
-        **External Documentation**
+        All of the parameters to this method are passed to that class's
+        initializer verbatim.
 
-        The Globus Auth specification for Native App grants details the
-        modifications to the Authorization Code grant flow as
-        `The PKCE Security Protocol \
-        <https://docs.globus.org/api/auth/developer-guide/#pkce>`_
+        #notthreadsafe
         """
         self.logger.info('Starting Native App Grant Flow')
         self.current_oauth2_flow_manager = GlobusNativeAppFlowManager(
diff --git a/globus_sdk/auth/oauth2_authorization_code.py b/globus_sdk/auth/oauth2_authorization_code.py
index 8a9b312b..df5141a0 100644
--- a/globus_sdk/auth/oauth2_authorization_code.py
+++ b/globus_sdk/auth/oauth2_authorization_code.py
@@ -1,5 +1,4 @@
 import logging
-import six
 from six.moves.urllib.parse import urlencode
 
 from globus_sdk.base import slash_join
@@ -35,9 +34,9 @@ class GlobusAuthorizationCodeFlowManager(GlobusOAuthFlowManager):
           The page that users should be directed to after authenticating at the
           authorize URL. Required.
 
-        ``requested_scopes`` (*iterable* or *string*)
+        ``requested_scopes`` (*string*)
           The scopes on the token(s) being requested, as a space-separated
-          string or an iterable of strings. Defaults to ``openid profile email
+          string. Defaults to ``openid profile email
           urn:globus:auth:scope:transfer.api.globus.org:all``
 
         ``state`` (*string*)
@@ -56,9 +55,6 @@ class GlobusAuthorizationCodeFlowManager(GlobusOAuthFlowManager):
                  refresh_tokens=False):
         # default to the default requested scopes
         self.requested_scopes = requested_scopes or DEFAULT_REQUESTED_SCOPES
-        # convert scopes iterable to string immediately on load
-        if not isinstance(self.requested_scopes, six.string_types):
-            self.requested_scopes = " ".join(self.requested_scopes)
 
         # store the remaining parameters directly, with no transformation
         self.client_id = auth_client.client_id
diff --git a/globus_sdk/auth/oauth2_constants.py b/globus_sdk/auth/oauth2_constants.py
index fb3fce16..e92902eb 100644
--- a/globus_sdk/auth/oauth2_constants.py
+++ b/globus_sdk/auth/oauth2_constants.py
@@ -2,5 +2,5 @@ __all__ = ['DEFAULT_REQUESTED_SCOPES']
 
 
 DEFAULT_REQUESTED_SCOPES = (
-    'openid', 'profile', 'email',
+    'openid profile email '
     'urn:globus:auth:scope:transfer.api.globus.org:all')
diff --git a/globus_sdk/auth/oauth2_native_app.py b/globus_sdk/auth/oauth2_native_app.py
index f42b38dc..af646d6d 100644
--- a/globus_sdk/auth/oauth2_native_app.py
+++ b/globus_sdk/auth/oauth2_native_app.py
@@ -3,7 +3,6 @@ import hashlib
 import base64
 import re
 import os
-import six
 from six.moves.urllib.parse import urlencode
 
 from globus_sdk.base import slash_join
@@ -73,9 +72,9 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager):
           used to extract default values for the flow, and also to make calls
           to the Auth service. This SHOULD be a ``NativeAppAuthClient``
 
-        ``requested_scopes`` (*iterable* or *string*)
+        ``requested_scopes`` (*string*)
           The scopes on the token(s) being requested, as a space-separated
-          string or iterable of strings. Defaults to ``openid profile email
+          string. Defaults to ``openid profile email
           urn:globus:auth:scope:transfer.api.globus.org:all``
 
         ``redirect_uri`` (*string*)
@@ -100,7 +99,7 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager):
         ``refresh_tokens`` (*bool*)
           When True, request refresh tokens in addition to access tokens
 
-        ``prefill_named_grant`` (*string*)
+          ``prefill_named_grant`` (*string*)
           Optionally prefill the named grant label on the consent page
     """
 
@@ -120,9 +119,6 @@ class GlobusNativeAppFlowManager(GlobusOAuthFlowManager):
 
         # default to the default requested scopes
         self.requested_scopes = requested_scopes or DEFAULT_REQUESTED_SCOPES
-        # convert scopes iterable to string immediately on load
-        if not isinstance(self.requested_scopes, six.string_types):
-            self.requested_scopes = " ".join(self.requested_scopes)
 
         # default to `/v2/web/auth-code` on whatever environment we're looking
         # at -- most typically it will be `https://auth.globus.org/`


Test patch:
diff --git a/tests/integration/test_auth_client_flow.py b/tests/integration/test_auth_client_flow.py
index 4ca019d8..5dd1fb44 100644
--- a/tests/integration/test_auth_client_flow.py
+++ b/tests/integration/test_auth_client_flow.py
@@ -4,7 +4,7 @@ import globus_sdk
 from globus_sdk.auth.oauth2_constants import DEFAULT_REQUESTED_SCOPES
 from globus_sdk.auth.oauth2_native_app import make_native_app_challenge
 from tests.framework import CapturedIOTestCase, get_client_data
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class AuthClientIntegrationTests(CapturedIOTestCase):
@@ -27,8 +27,7 @@ class AuthClientIntegrationTests(CapturedIOTestCase):
                          "client_id=" + ac.client_id,
                          "redirect_uri=" +
                          quote_plus(ac.base_url + "v2/web/auth-code"),
-                         "scope=" + quote_plus(
-                             " ".join(DEFAULT_REQUESTED_SCOPES)),
+                         "scope=" + quote_plus(DEFAULT_REQUESTED_SCOPES),
                          "state=" + "_default",
                          "response_type=" + "code",
                          "code_challenge=" +
@@ -77,8 +76,7 @@ class AuthClientIntegrationTests(CapturedIOTestCase):
         expected_vals = [ac.base_url + "v2/oauth2/authorize?",
                          "client_id=" + ac.client_id,
                          "redirect_uri=" + "uri",
-                         "scope=" + quote_plus(
-                             " ".join(DEFAULT_REQUESTED_SCOPES)),
+                         "scope=" + quote_plus(DEFAULT_REQUESTED_SCOPES),
                          "state=" + "_default",
                          "response_type=" + "code",
                          "access_type=" + "online"]
@@ -113,7 +111,7 @@ class AuthClientIntegrationTests(CapturedIOTestCase):
         flow_manager = globus_sdk.auth.GlobusNativeAppFlowManager(ac)
         ac.current_oauth2_flow_manager = flow_manager
 
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             ac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
@@ -129,7 +127,7 @@ class AuthClientIntegrationTests(CapturedIOTestCase):
             ac, "uri")
         ac.current_oauth2_flow_manager = flow_manager
 
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             ac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
diff --git a/tests/integration/test_confidential_client_flow.py b/tests/integration/test_confidential_client_flow.py
index d713c741..07d690e1 100644
--- a/tests/integration/test_confidential_client_flow.py
+++ b/tests/integration/test_confidential_client_flow.py
@@ -4,7 +4,7 @@ import globus_sdk
 from globus_sdk.auth.oauth2_constants import DEFAULT_REQUESTED_SCOPES
 from tests.framework import CapturedIOTestCase, get_client_data
 from globus_sdk.auth import GlobusAuthorizationCodeFlowManager
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class ConfidentialAppAuthClientIntegrationTests(CapturedIOTestCase):
@@ -31,8 +31,7 @@ class ConfidentialAppAuthClientIntegrationTests(CapturedIOTestCase):
         flow = self.cac.oauth2_start_flow("uri")
         self.assertIsInstance(flow, GlobusAuthorizationCodeFlowManager)
         self.assertEqual(flow.redirect_uri, "uri")
-        self.assertEqual(flow.requested_scopes,
-                         " ".join(DEFAULT_REQUESTED_SCOPES))
+        self.assertEqual(flow.requested_scopes, DEFAULT_REQUESTED_SCOPES)
         self.assertEqual(flow.state, "_default")
         self.assertFalse(flow.refresh_tokens)
 
@@ -41,15 +40,14 @@ class ConfidentialAppAuthClientIntegrationTests(CapturedIOTestCase):
         expected_vals = [self.cac.base_url + "v2/oauth2/authorize?",
                          "client_id=" + self.cac.client_id,
                          "redirect_uri=" + "uri",
-                         "scope=" + quote_plus(
-                             " ".join(DEFAULT_REQUESTED_SCOPES)),
+                         "scope=" + quote_plus(DEFAULT_REQUESTED_SCOPES),
                          "state=" + "_default",
                          "access_type=" + "online"]
         for val in expected_vals:
             self.assertIn(val, url_res)
 
         # confirm client can try exchanging code for tokens via flow
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.cac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
@@ -83,7 +81,7 @@ class ConfidentialAppAuthClientIntegrationTests(CapturedIOTestCase):
             self.assertIn(val, url_res)
 
         # confirm client can try exchanging code for tokens via flow
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.cac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
diff --git a/tests/integration/test_native_client_flow.py b/tests/integration/test_native_client_flow.py
index 6d7d42a2..3f97c236 100644
--- a/tests/integration/test_native_client_flow.py
+++ b/tests/integration/test_native_client_flow.py
@@ -5,7 +5,7 @@ from globus_sdk.auth.oauth2_constants import DEFAULT_REQUESTED_SCOPES
 from globus_sdk.auth.oauth2_native_app import make_native_app_challenge
 from tests.framework import CapturedIOTestCase, get_client_data
 from globus_sdk.auth import GlobusNativeAppFlowManager
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class NativeAppAuthClientIntegrationTests(CapturedIOTestCase):
@@ -31,8 +31,7 @@ class NativeAppAuthClientIntegrationTests(CapturedIOTestCase):
         self.assertIsInstance(flow, GlobusNativeAppFlowManager)
         self.assertEqual(flow.redirect_uri,
                          self.nac.base_url + "v2/web/auth-code")
-        self.assertEqual(flow.requested_scopes,
-                         " ".join(DEFAULT_REQUESTED_SCOPES))
+        self.assertEqual(flow.requested_scopes, DEFAULT_REQUESTED_SCOPES)
         self.assertEqual(flow.state, "_default")
         self.assertFalse(flow.refresh_tokens)
 
@@ -42,8 +41,7 @@ class NativeAppAuthClientIntegrationTests(CapturedIOTestCase):
                          "client_id=" + self.nac.client_id,
                          "redirect_uri=" +
                          quote_plus(self.nac.base_url + "v2/web/auth-code"),
-                         "scope=" + quote_plus(
-                             " ".join(DEFAULT_REQUESTED_SCOPES)),
+                         "scope=" + quote_plus(DEFAULT_REQUESTED_SCOPES),
                          "state=" + "_default",
                          "code_challenge=" + quote_plus(flow.challenge),
                          "access_type=" + "online"]
@@ -51,7 +49,7 @@ class NativeAppAuthClientIntegrationTests(CapturedIOTestCase):
             self.assertIn(val, url_res)
 
         # confirm client can try exchanging code for tokens via flow
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.nac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
@@ -88,7 +86,7 @@ class NativeAppAuthClientIntegrationTests(CapturedIOTestCase):
             self.assertIn(val, url_res)
 
         # confirm client can try exchanging code for tokens via flow
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.nac.oauth2_exchange_code_for_tokens("invalid_code")
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")
diff --git a/tests/unit/test_auth_client.py b/tests/unit/test_auth_client.py
index 1d6d7887..a8c271f6 100644
--- a/tests/unit/test_auth_client.py
+++ b/tests/unit/test_auth_client.py
@@ -7,7 +7,7 @@ import globus_sdk
 from tests.framework import (CapturedIOTestCase,
                              get_client_data, get_user_data,
                              SDKTESTER1A_NATIVE1_AUTH_RT)
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class AuthClientTests(CapturedIOTestCase):
@@ -122,13 +122,13 @@ class AuthClientTests(CapturedIOTestCase):
         Confirms bad and unauthorized requests to get_identities throw errors
         """
         # bad request
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.ac.get_identities(usernames=["a", "b"], ids=["0", "1"])
         self.assertEqual(apiErr.exception.http_status, 400)
         self.assertEqual(apiErr.exception.code, "INVALID_PARAMETERS")
 
         # no auth
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             globus_sdk.AuthClient().get_identities()
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "UNAUTHORIZED")
@@ -190,7 +190,7 @@ class AuthClientTests(CapturedIOTestCase):
         self.assertEqual(ref_res["token_type"], "Bearer")
 
         # without valid client id
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             ref_res = self.ac.oauth2_refresh_token(SDKTESTER1A_NATIVE1_AUTH_RT)
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "Error")  # json is malformed?
@@ -205,7 +205,7 @@ class AuthClientTests(CapturedIOTestCase):
                                               additional_params=param)
         self.assertFalse(rev_res["active"])
 
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.ac.get_identities()
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "UNAUTHORIZED")
@@ -244,7 +244,7 @@ class AuthClientTests(CapturedIOTestCase):
             get_user_data()["sdktester1a"]["id"])
 
         # unauthorized client
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             globus_sdk.AuthClient().oauth2_userinfo()
         self.assertEqual(apiErr.exception.http_status, 403)
         self.assertEqual(apiErr.exception.code, "FORBIDDEN")
diff --git a/tests/unit/test_confidential_client.py b/tests/unit/test_confidential_client.py
index a6b1059d..ba68a391 100644
--- a/tests/unit/test_confidential_client.py
+++ b/tests/unit/test_confidential_client.py
@@ -1,11 +1,6 @@
-try:
-    import mock
-except ImportError:
-    from unittest import mock
-
 import globus_sdk
 from tests.framework import CapturedIOTestCase, get_client_data
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class ConfidentialAppAuthClientTests(CapturedIOTestCase):
@@ -33,14 +28,7 @@ class ConfidentialAppAuthClientTests(CapturedIOTestCase):
         Confirm tokens allow use of userinfo for the client
         Returns access_token for testing
         """
-        with mock.patch.object(self.cac, 'oauth2_token',
-                               side_effect=self.cac.oauth2_token) as m:
-            token_res = self.cac.oauth2_client_credentials_tokens(
-                requested_scopes="openid profile")
-            m.assert_called_once_with(
-                {"grant_type": "client_credentials",
-                 "scope": "openid profile"})
-
+        token_res = self.cac.oauth2_client_credentials_tokens()
         # validate results
         self.assertIn("access_token", token_res)
         self.assertIn("expires_in", token_res)
@@ -84,7 +72,7 @@ class ConfidentialAppAuthClientTests(CapturedIOTestCase):
             self.assertIn("resource_server", token_info)
 
         # confirm non resource servers are unauthorized
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.cac.oauth2_client_credentials_tokens(access_token)
         self.assertEqual(apiErr.exception.http_status, 400)
         self.assertEqual(apiErr.exception.code, "UNKNOWN_SCOPE_ERROR")
@@ -92,7 +80,7 @@ class ConfidentialAppAuthClientTests(CapturedIOTestCase):
 
         # confirm invalid tokens are unauthorized
         invalid_token = tokens["auth.globus.org"]["access_token"]
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.cac.oauth2_client_credentials_tokens(invalid_token)
         self.assertEqual(apiErr.exception.http_status, 400)
         self.assertEqual(apiErr.exception.code, "UNKNOWN_SCOPE_ERROR")
diff --git a/tests/unit/test_exc.py b/tests/unit/test_exc.py
index 638259db..a2f3cc73 100644
--- a/tests/unit/test_exc.py
+++ b/tests/unit/test_exc.py
@@ -2,14 +2,14 @@ import requests
 import json
 import six
 
-from globus_sdk.exc import (GlobusAPIError, TransferAPIError,
+from globus_sdk.exc import (GlobusAPIError, TransferAPIError, AuthAPIError,
                             GlobusOptionalDependencyError,
                             NetworkError, GlobusTimeoutError,
                             GlobusConnectionError, convert_request_exception)
 from tests.framework import CapturedIOTestCase
 
 
-# super class for Globus and Transfer APIError tests with shared setup
+# super class for Globus, Auth, and Transfer APIError tests with shared setup
 class APIErrorTests(CapturedIOTestCase):
 
     __test__ = False  # prevents base class from trying to run tests
@@ -151,6 +151,68 @@ class TransferAPIErrorTests(APIErrorTests):
         self.assertEqual(apiErr.exception._get_args(), expected)
 
 
+class AuthAPIErrorTests(APIErrorTests):
+
+    __test__ = True  # marks sub-class as having tests
+
+    def setUp(self):
+        """
+        Creates auth-like json responses in addition to the APIError
+        setUp responses for testing
+        """
+        super(AuthAPIErrorTests, self).setUp()
+
+        self.simple_auth_data = {"error": "simple auth error message"}
+        self.simple_auth_response = requests.Response()
+        self.simple_auth_response._content = six.b(
+            json.dumps(self.simple_auth_data))
+        self.simple_auth_response.headers["Content-Type"] = "application/json"
+        self.simple_auth_response.status_code = "404"
+
+        self.nested_auth_data = {"errors": [
+            {"detail": "nested auth error message", "code": "Auth Error"}]}
+        self.nested_auth_response = requests.Response()
+        self.nested_auth_response._content = six.b(
+            json.dumps(self.nested_auth_data))
+        self.nested_auth_response.headers["Content-Type"] = "application/json"
+        self.nested_auth_response.status_code = "404"
+
+    def test_get_args(self):
+        """
+        Gets args from all four response types, confirms expected results
+        implicitly tests _load_from_json
+        """
+        # simple auth
+        with self.assertRaises(AuthAPIError) as apiErr:
+            raise AuthAPIError(self.simple_auth_response)
+        expected = ("404", "Error", "simple auth error message")
+        self.assertEqual(apiErr.exception._get_args(), expected)
+
+        # nested auth
+        with self.assertRaises(AuthAPIError) as apiErr:
+            raise AuthAPIError(self.nested_auth_response)
+        expected = ("404", "Auth Error", "nested auth error message")
+        self.assertEqual(apiErr.exception._get_args(), expected)
+
+        # json in wrong format
+        with self.assertRaises(AuthAPIError) as apiErr:
+            raise AuthAPIError(self.json_response)
+        expected = ("400", "Json Error", "json error message")
+        self.assertEqual(apiErr.exception._get_args(), expected)
+
+        # text
+        with self.assertRaises(AuthAPIError) as apiErr:
+            raise AuthAPIError(self.text_response)
+        expected = ("401", "Error", "error message")
+        self.assertEqual(apiErr.exception._get_args(), expected)
+
+        # malformed json
+        with self.assertRaises(AuthAPIError) as apiErr:
+            raise AuthAPIError(self.malformed_response)
+        expected = ("403", "Error", "{")
+        self.assertEqual(apiErr.exception._get_args(), expected)
+
+
 class NetworkErrorTests(CapturedIOTestCase):
 
     def setUp(self):
diff --git a/tests/unit/test_native_client.py b/tests/unit/test_native_client.py
index 52ad7f92..67f6dddb 100644
--- a/tests/unit/test_native_client.py
+++ b/tests/unit/test_native_client.py
@@ -2,7 +2,7 @@ import globus_sdk
 from tests.framework import (CapturedIOTestCase,
                              get_client_data, get_user_data,
                              SDKTESTER1A_NATIVE1_AUTH_RT)
-from globus_sdk.exc import GlobusAPIError
+from globus_sdk.exc import AuthAPIError
 
 
 class NativeAppAuthClientTests(CapturedIOTestCase):
@@ -29,7 +29,7 @@ class NativeAppAuthClientTests(CapturedIOTestCase):
         """
         Confirms native apps aren't authorized to get_identities on their own
         """
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             self.nac.get_identities(ids=get_user_data()["sdktester1a"]["id"])
         self.assertEqual(apiErr.exception.http_status, 401)
         self.assertEqual(apiErr.exception.code, "UNAUTHORIZED")
@@ -73,7 +73,7 @@ class NativeAppAuthClientTests(CapturedIOTestCase):
         self.assertFalse(rev_res["active"])
 
         # confirm token is no longer usable
-        with self.assertRaises(GlobusAPIError) as apiErr:
+        with self.assertRaises(AuthAPIError) as apiErr:
             ac = globus_sdk.AuthClient(
                 authorizer=globus_sdk.AccessTokenAuthorizer(access_token),
                 client_id=get_client_data()["native_app_client1"]["id"])
diff --git a/tests/unit/test_oauth2_authorization_code.py b/tests/unit/test_oauth2_authorization_code.py
index 5c634b91..5c531719 100644
--- a/tests/unit/test_oauth2_authorization_code.py
+++ b/tests/unit/test_oauth2_authorization_code.py
@@ -18,16 +18,10 @@ class GlobusAuthorizationCodeFlowManagerTests(CapturedIOTestCase):
         self.ac = mock.Mock()
         self.ac.client_id = "client_id"
         self.ac.base_url = "base_url/"
-        self.flow_manager = globus_sdk.auth.GlobusAuthorizationCodeFlowManager(
+        self.flow_manager = globus_sdk.auth.GlobusNativeAppFlowManager(
             self.ac, requested_scopes="scopes", redirect_uri="uri",
             state="state")
 
-    def test_init_handles_iterable_scopes(self):
-        flow_manager = globus_sdk.auth.GlobusAuthorizationCodeFlowManager(
-            self.ac, requested_scopes=["scope1", "scope2"], redirect_uri="uri",
-            state="state")
-        self.assertEquals(flow_manager.requested_scopes, "scope1 scope2")
-
     def test_get_authorize_url(self):
         """
         Creates an authorize url, confirms results match object values
@@ -61,7 +55,9 @@ class GlobusAuthorizationCodeFlowManagerTests(CapturedIOTestCase):
         auth_code = "code"
         self.flow_manager.exchange_code_for_tokens(auth_code)
 
-        expected = {"grant_type": "authorization_code",
+        expected = {"client_id": self.ac.client_id,
+                    "grant_type": "authorization_code",
                     "code": auth_code.encode("utf-8"),
+                    "code_verifier": self.flow_manager.verifier,
                     "redirect_uri": self.flow_manager.redirect_uri}
         self.ac.oauth2_token.assert_called_with(expected)
diff --git a/tests/unit/test_oauth2_native_app.py b/tests/unit/test_oauth2_native_app.py
index 0782c1ae..fb060c90 100644
--- a/tests/unit/test_oauth2_native_app.py
+++ b/tests/unit/test_oauth2_native_app.py
@@ -26,13 +26,6 @@ class GlobusNativeAppFlowManagerTests(CapturedIOTestCase):
             self.ac, requested_scopes="scopes", redirect_uri="uri",
             state="state")
 
-    def test_init_handles_iterable_scopes(self):
-        flow_manager = globus_sdk.auth.GlobusNativeAppFlowManager(
-            self.ac, requested_scopes=set(("scope1", "scope2")),
-            redirect_uri="uri", state="state")
-        self.assertIn(flow_manager.requested_scopes, ("scope1 scope2",
-                                                      "scope2 scope1"))
-
     def test_make_native_app_challenge(self):
         """
         Makes native app challenge with and without verifier,