Repository: GoogleCloudPlatform/cloud-sql-python-connector
Pull request #589 (2023-01-16 15:17:41)

chore: move `_auth_init` out of Instance class
The `_auth_init` method to initialize credentials does not need to be an `Instance` class method. It does not rely on any class attributes and thus can me moved to the`utils.py` file as a utility method.


Patch:
diff --git a/google/cloud/sql/connector/instance.py b/google/cloud/sql/connector/instance.py
index 4d516f6..f07d9c1 100644
--- a/google/cloud/sql/connector/instance.py
+++ b/google/cloud/sql/connector/instance.py
@@ -22,7 +22,7 @@ from google.cloud.sql.connector.refresh_utils import (
     _seconds_until_refresh,
     _is_valid,
 )
-from google.cloud.sql.connector.utils import write_to_file
+from google.cloud.sql.connector.utils import write_to_file, _auth_init
 from google.cloud.sql.connector.version import __version__ as version
 from google.cloud.sql.connector.exceptions import (
     TLSVersionError,
@@ -36,11 +36,9 @@ import asyncio
 import aiohttp
 import datetime
 from enum import Enum
-import google.auth
-from google.auth.credentials import Credentials, with_scopes_if_required
+from google.auth.credentials import Credentials
 from cryptography.x509 import load_pem_x509_certificate
 from cryptography.hazmat.backends import default_backend
-import google.auth.transport.requests
 import ssl
 from tempfile import TemporaryDirectory
 from typing import (
@@ -249,9 +247,7 @@ class Instance:
                 "Arg credentials must be type 'google.auth.credentials.Credentials' "
                 "or None (to use Application Default Credentials)"
             )
-
-        self._auth_init(credentials)
-
+        self._credentials = _auth_init(credentials)
         self._refresh_rate_limiter = AsyncRateLimiter(
             max_capacity=2, rate=1 / 30, loop=self._loop
         )
@@ -259,25 +255,6 @@ class Instance:
         self._current = self._schedule_refresh(0)
         self._next = self._current
 
-    def _auth_init(self, credentials: Optional[Credentials]) -> None:
-        """Creates and assigns a Google Python API service object for
-        Google Cloud SQL Admin API.
-
-        :type credentials: google.auth.credentials.Credentials
-        :param credentials
-            Credentials object used to authenticate connections to Cloud SQL server.
-            If not specified, Application Default Credentials are used.
-        """
-        scopes = ["https://www.googleapis.com/auth/sqlservice.admin"]
-        # if Credentials object is passed in, use for authentication
-        if isinstance(credentials, Credentials):
-            credentials = with_scopes_if_required(credentials, scopes=scopes)
-        # otherwise use application default credentials
-        else:
-            credentials, project = google.auth.default(scopes=scopes)
-
-        self._credentials = credentials
-
     def force_refresh(self) -> None:
         """
         Forces a new refresh attempt immediately to be used for future connection attempts.
diff --git a/google/cloud/sql/connector/utils.py b/google/cloud/sql/connector/utils.py
index 21bebd6..637737d 100755
--- a/google/cloud/sql/connector/utils.py
+++ b/google/cloud/sql/connector/utils.py
@@ -17,8 +17,10 @@ limitations under the License.
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization
 from cryptography.hazmat.primitives.asymmetric import rsa
+from google.auth import default
+from google.auth.credentials import Credentials, with_scopes_if_required
 
-from typing import Tuple
+from typing import Tuple, Optional
 
 
 async def generate_keys() -> Tuple[bytes, str]:
@@ -102,3 +104,23 @@ def format_database_user(database_version: str, user: str) -> str:
         return user.split("@")[0]
 
     return user
+
+
+def _auth_init(credentials: Optional[Credentials]) -> Credentials:
+    """Creates google.auth credentials object with scopes required to make
+    calls to the Cloud SQL Admin APIs.
+
+    :type credentials: google.auth.credentials.Credentials
+    :param credentials
+        Credentials object used to authenticate connections to Cloud SQL server.
+        If not specified, Application Default Credentials are used.
+    """
+    scopes = ["https://www.googleapis.com/auth/sqlservice.admin"]
+    # if Credentials object is passed in, use for authentication
+    if isinstance(credentials, Credentials):
+        credentials = with_scopes_if_required(credentials, scopes=scopes)
+    # otherwise use application default credentials
+    else:
+        credentials, _ = default(scopes=scopes)
+
+    return credentials


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9811855..e1c4831 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -154,7 +154,7 @@ async def instance(
     keys = event_loop.create_task(generate_keys())
     _, client_key = await keys
 
-    with patch("google.auth.default") as mock_auth:
+    with patch("google.cloud.sql.connector.utils.default") as mock_auth:
         mock_auth.return_value = fake_credentials, None
         # mock Cloud SQL Admin API calls
         with aioresponses() as mocked:
@@ -188,7 +188,7 @@ async def connector(fake_credentials: Credentials) -> AsyncGenerator[Connector,
     project, region, instance_name = instance_connection_name.split(":")
     # initialize connector
     connector = Connector()
-    with patch("google.auth.default") as mock_auth:
+    with patch("google.cloud.sql.connector.utils.default") as mock_auth:
         mock_auth.return_value = fake_credentials, None
         # mock Cloud SQL Admin API calls
         mock_instance = FakeCSQLInstance(project, region, instance_name)
diff --git a/tests/unit/test_instance.py b/tests/unit/test_instance.py
index c843a42..0cc5da3 100644
--- a/tests/unit/test_instance.py
+++ b/tests/unit/test_instance.py
@@ -56,7 +56,7 @@ async def test_Instance_init(
     keys = asyncio.wrap_future(
         asyncio.run_coroutine_threadsafe(generate_keys(), event_loop), loop=event_loop
     )
-    with patch("google.auth.default") as mock_auth:
+    with patch("google.cloud.sql.connector.utils.default") as mock_auth:
         mock_auth.return_value = fake_credentials, None
         instance = Instance(connect_string, "pymysql", keys, event_loop)
     project_result = instance._project
@@ -206,42 +206,6 @@ async def test_force_refresh_cancels_pending_refresh(
     assert isinstance(await instance._current, InstanceMetadata)
 
 
-@pytest.mark.asyncio
-async def test_auth_init_with_credentials_object(
-    instance: Instance, fake_credentials: Credentials
-) -> None:
-    """
-    Test that Instance's _auth_init initializes _credentials
-    when passed a google.auth.credentials.Credentials object.
-    """
-    setattr(instance, "_credentials", None)
-    with patch(
-        "google.cloud.sql.connector.instance.with_scopes_if_required"
-    ) as mock_auth:
-        mock_auth.return_value = fake_credentials
-        instance._auth_init(credentials=fake_credentials)
-        assert isinstance(instance._credentials, Credentials)
-        mock_auth.assert_called_once()
-    await instance.close()
-
-
-@pytest.mark.asyncio
-async def test_auth_init_with_default_credentials(
-    instance: Instance, fake_credentials: Credentials
-) -> None:
-    """
-    Test that Instance's _auth_init initializes _credentials
-    with application default credentials when credentials are not specified.
-    """
-    setattr(instance, "_credentials", None)
-    with patch("google.auth.default") as mock_auth:
-        mock_auth.return_value = fake_credentials, None
-        instance._auth_init(credentials=None)
-        assert isinstance(instance._credentials, Credentials)
-        mock_auth.assert_called_once()
-    await instance.close()
-
-
 @pytest.mark.asyncio
 async def test_Instance_close(instance: Instance) -> None:
     """
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 0b9b72a..b6de639 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -15,6 +15,8 @@ limitations under the License.
 """
 
 from google.cloud.sql.connector import utils
+from google.auth.credentials import Credentials
+from mock import patch
 
 import pytest  # noqa F401 Needed to run the tests
 
@@ -75,3 +77,29 @@ def test_format_database_user_mysql() -> None:
     user2 = utils.format_database_user("MYSQL_8_0", "test")
     assert user == "test"
     assert user2 == "test"
+
+
+def test_auth_init_with_credentials_object(fake_credentials: Credentials) -> None:
+    """
+    Test that _auth_init initializes credentials with scopes
+    when passed a google.auth.credentials.Credentials object.
+    """
+    with patch("google.cloud.sql.connector.utils.with_scopes_if_required") as mock_auth:
+        mock_auth.return_value = fake_credentials
+        credentials = utils._auth_init(credentials=fake_credentials)
+        assert isinstance(credentials, Credentials)
+        mock_auth.assert_called_once()
+
+
+def test_auth_init_with_default_credentials(
+    fake_credentials: Credentials,
+) -> None:
+    """
+    Test that _auth_init initializes _credentials
+    with application default credentials when credentials are not specified.
+    """
+    with patch("google.cloud.sql.connector.utils.default") as mock_auth:
+        mock_auth.return_value = fake_credentials, None
+        credentials = utils._auth_init(credentials=None)
+        assert isinstance(credentials, Credentials)
+        mock_auth.assert_called_once()