From 8a809e7a4460fcea2d0ee23bcc3dfcd97980f312 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <55252373+jkyle109@users.noreply.github.com> Date: Wed, 24 Aug 2022 11:13:45 -0400 Subject: [PATCH] Unit tests for async credentials. (#631) * Unit tests for async credentials. * fix: Removed redundant tests. --- tests/test_credentials.py | 127 +++++++++++++++++++++++++++++++++++--- tests/testutils.py | 54 ++++++++++++++++ 2 files changed, 174 insertions(+), 7 deletions(-) diff --git a/tests/test_credentials.py b/tests/test_credentials.py index cceb6b6f9..733f45dea 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -22,7 +22,9 @@ from google.auth import crypt from google.auth import exceptions from google.oauth2 import credentials as gcredentials +from google.oauth2 import _credentials_async as gcredentials_async from google.oauth2 import service_account +from google.oauth2 import _service_account_async as service_account_async import pytest from firebase_admin import credentials @@ -33,16 +35,15 @@ def check_scopes(g_credential): assert isinstance(g_credential, google.auth.credentials.ReadOnlyScoped) assert sorted(credentials._scopes) == sorted(g_credential.scopes) +invalid_certs = { + 'NonExistingFile': ('non_existing.json', IOError), + 'RefreskToken': ('refresh_token.json', ValueError), + 'MalformedPrivateKey': ('malformed_key.json', ValueError), + 'MissingClientId': ('no_client_email_service_account.json', ValueError), +} class TestCertificate: - invalid_certs = { - 'NonExistingFile': ('non_existing.json', IOError), - 'RefreskToken': ('refresh_token.json', ValueError), - 'MalformedPrivateKey': ('malformed_key.json', ValueError), - 'MissingClientId': ('no_client_email_service_account.json', ValueError), - } - def test_init_from_file(self): credential = credentials.Certificate( testutils.resource_filename('service_account.json')) @@ -86,6 +87,45 @@ def _verify_credential(self, credential): assert isinstance(access_token.expiry, datetime.datetime) +class TestCertificateAsync: + + @pytest.mark.asyncio + async def test_init_from_file(self): + credential = credentials.Certificate( + testutils.resource_filename('service_account.json')) + await self._verify_credential(credential) + + @pytest.mark.asyncio + async def test_init_from_path_like(self): + path = pathlib.Path(testutils.resource_filename('service_account.json')) + credential = credentials.Certificate(path) + await self._verify_credential(credential) + + + @pytest.mark.asyncio + async def test_init_from_dict(self): + parsed_json = json.loads(testutils.resource('service_account.json')) + credential = credentials.Certificate(parsed_json) + await self._verify_credential(credential) + + @pytest.mark.asyncio + async def _verify_credential(self, credential): + assert credential.project_id == 'mock-project-id' + assert credential.service_account_email == 'mock-email@mock-project.iam.gserviceaccount.com' + assert isinstance(credential.signer, crypt.Signer) + + g_credential_async = credential.get_credential_async() + assert isinstance(g_credential_async, service_account_async.Credentials) + assert g_credential_async.token is None + check_scopes(g_credential_async) + + mock_response = {'access_token': 'mock_access_token', 'expires_in': 3600} + credentials._request_async = testutils.MockAsyncRequest(200, json.dumps(mock_response)) + access_token_async = await credential.get_access_token_async() + assert access_token_async.access_token == 'mock_access_token' + assert isinstance(access_token_async.expiry, datetime.datetime) + + @pytest.fixture def app_default(request): var_name = 'GOOGLE_APPLICATION_CREDENTIALS' @@ -129,6 +169,38 @@ def test_nonexisting_path(self, app_default): creds.get_credential() # This now throws. +class TestApplicationDefaultAsync: + + @pytest.mark.asyncio + @pytest.mark.parametrize('app_default', [testutils.resource_filename('service_account.json')], + indirect=True) + async def test_init(self, app_default): + del app_default + credential = credentials.ApplicationDefault() + assert credential.project_id == 'mock-project-id' + + g_credential_async = credential.get_credential_async() + assert isinstance(g_credential_async, google.auth.credentials.Credentials) + assert g_credential_async.token is None + check_scopes(g_credential_async) + + mock_response = {'access_token': 'mock_access_token', 'expires_in': 3600} + credentials._request_async = testutils.MockAsyncRequest(200, json.dumps(mock_response)) + access_token_async = await credential.get_access_token_async() + assert access_token_async.access_token == 'mock_access_token' + assert isinstance(access_token_async.expiry, datetime.datetime) + + @pytest.mark.parametrize('app_default', [testutils.resource_filename('non_existing.json')], + indirect=True) + def test_nonexisting_path(self, app_default): + del app_default + # This does not yet throw because the credentials are lazily loaded. + creds = credentials.ApplicationDefault() + + with pytest.raises(exceptions.DefaultCredentialsError): + creds.get_credential_async() # This now throws. + + class TestRefreshToken: def test_init_from_file(self): @@ -191,3 +263,44 @@ def _verify_credential(self, credential): access_token = credential.get_access_token() assert access_token.access_token == 'mock_access_token' assert isinstance(access_token.expiry, datetime.datetime) + + +class TestRefreshTokenAsync: + + @pytest.mark.asyncio + async def test_init_from_file(self): + credential = credentials.RefreshToken( + testutils.resource_filename('refresh_token.json')) + await self._verify_credential(credential) + + @pytest.mark.asyncio + async def test_init_from_path_like(self): + path = pathlib.Path(testutils.resource_filename('refresh_token.json')) + credential = credentials.RefreshToken(path) + await self._verify_credential(credential) + + @pytest.mark.asyncio + async def test_init_from_dict(self): + parsed_json = json.loads(testutils.resource('refresh_token.json')) + credential = credentials.RefreshToken(parsed_json) + await self._verify_credential(credential) + + @pytest.mark.asyncio + async def _verify_credential(self, credential): + assert credential.client_id == 'mock.apps.googleusercontent.com' + assert credential.client_secret == 'mock-secret' + assert credential.refresh_token == 'mock-refresh-token' + + g_credential_async = credential.get_credential_async() + assert isinstance(g_credential_async, gcredentials_async.Credentials) + assert g_credential_async.token is None + check_scopes(g_credential_async) + + mock_response = { + 'access_token': 'mock_access_token', + 'expires_in': 3600 + } + credentials._request_async = testutils.MockAsyncRequest(200, json.dumps(mock_response)) + access_token_async = await credential.get_access_token_async() + assert access_token_async.access_token == 'mock_access_token' + assert isinstance(access_token_async.expiry, datetime.datetime) diff --git a/tests/testutils.py b/tests/testutils.py index 92755107c..6ab69dda4 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -20,6 +20,7 @@ from google.auth import credentials from google.auth import transport +from google.auth.transport import _aiohttp_requests as aiohttp_requests from requests import adapters from requests import models @@ -111,6 +112,59 @@ def __call__(self, *args, **kwargs): # pylint: disable=arguments-differ raise self.error +class MockAsyncResponse(aiohttp_requests._CombinedResponse): + def __init__(self, status, response): + super(MockAsyncResponse, self).__init__(response) + self._status = status + self._response = response + self._raw_content = response + + @property + def status(self): + return self._status + + @property + def headers(self): + return {} + + @property + def data(self): + return self._response.encode() + + async def content(self): + return self._response.encode() + +class MockAsyncRequest(aiohttp_requests.Request): + """A mock async HTTP requests implementation. + + This can be used whenever an async HTTP interaction needs to be mocked + for testing purposes. For example HTTP calls to fetch public key + certificates, and HTTP calls to retrieve access tokens can be + mocked using this class. + """ + + def __init__(self, status, response): + super(MockAsyncRequest, self).__init__() + self.response = MockAsyncResponse(status, response) + self.log = [] + + async def __call__(self, *args, **kwargs): # pylint: disable=arguments-differ + self.log.append((args, kwargs)) + return self.response + + +class MockFailedAsyncRequest(aiohttp_requests.Request): + """A mock HTTP request that fails by raising an exception.""" + + def __init__(self, error): + super(MockFailedAsyncRequest, self).__init__() + self.error = error + self.log = [] + + async def __call__(self, *args, **kwargs): # pylint: disable=arguments-differ + self.log.append((args, kwargs)) + raise self.error + # Temporarily disable the lint rule. For more information see: # https://github.com/googleapis/google-auth-library-python/pull/561 # pylint: disable=abstract-method