From 677d0e9bc2559af1cc589a26ee12f8fcd384272e Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Mon, 14 May 2018 09:37:02 -0700 Subject: [PATCH 1/2] TST: Add unit tests for pandas_gbq.auth.get_credentials(). Tests (actual auth mocked out): * Using private key with contents. * Using private key with path. * Using default credentials. * Using cached user credentials. --- pandas_gbq/auth.py | 16 +++--- tests/data/dummy_key.json | 5 ++ tests/unit/test_auth.py | 100 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 tests/data/dummy_key.json create mode 100644 tests/unit/test_auth.py diff --git a/pandas_gbq/auth.py b/pandas_gbq/auth.py index f401243d..fcf9bc8f 100644 --- a/pandas_gbq/auth.py +++ b/pandas_gbq/auth.py @@ -39,8 +39,10 @@ def get_service_account_credentials(private_key): import google.auth.transport.requests from google.oauth2.service_account import Credentials + is_path = os.path.isfile(private_key) + try: - if os.path.isfile(private_key): + if is_path: with open(private_key) as f: json_key = json.loads(f.read()) else: @@ -64,11 +66,13 @@ def get_service_account_credentials(private_key): return credentials, json_key.get('project_id') except (KeyError, ValueError, TypeError, AttributeError): raise pandas_gbq.exceptions.InvalidPrivateKeyFormat( - "Private key is missing or invalid. It should be service " - "account private key JSON (file path or string contents) " - "with at least two keys: 'client_email' and 'private_key'. " - "Can be obtained from: https://console.developers.google." - "com/permissions/serviceaccounts") + 'Detected private_key as {}. '.format( + 'path' if is_path else 'contents') + + 'Private key is missing or invalid. It should be service ' + 'account private key JSON (file path or string contents) ' + 'with at least two keys: "client_email" and "private_key". ' + 'Can be obtained from: https://console.developers.google.' + 'com/permissions/serviceaccounts') def get_application_default_credentials(project_id=None): diff --git a/tests/data/dummy_key.json b/tests/data/dummy_key.json new file mode 100644 index 00000000..25e17b78 --- /dev/null +++ b/tests/data/dummy_key.json @@ -0,0 +1,5 @@ + + { + "private_key": "some_key", + "client_email": "service-account@example.com" + } \ No newline at end of file diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py new file mode 100644 index 00000000..c80e6a9a --- /dev/null +++ b/tests/unit/test_auth.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +import json +import os.path + +try: + import mock +except ImportError: # pragma: NO COVER + from unittest import mock + +from pandas_gbq import auth + + +def test_get_credentials_private_key_contents(monkeypatch): + from google.oauth2 import service_account + + def from_service_account_info(key_info): + mock_credentials = mock.create_autospec(service_account.Credentials) + mock_credentials.with_scopes.return_value = mock_credentials + mock_credentials.refresh.return_value = mock_credentials + return mock_credentials + + monkeypatch.setattr( + service_account.Credentials, + 'from_service_account_info', + from_service_account_info) + private_key = json.dumps({ + 'private_key': 'some_key', + 'client_email': 'service-account@example.com', + 'project_id': 'private-key-project' + }) + credentials, project = auth.get_credentials(private_key=private_key) + + assert credentials is not None + assert project == 'private-key-project' + + +def test_get_credentials_private_key_path(monkeypatch): + from google.oauth2 import service_account + + def from_service_account_info(key_info): + mock_credentials = mock.create_autospec(service_account.Credentials) + mock_credentials.with_scopes.return_value = mock_credentials + mock_credentials.refresh.return_value = mock_credentials + return mock_credentials + + monkeypatch.setattr( + service_account.Credentials, + 'from_service_account_info', + from_service_account_info) + private_key = os.path.join( + os.path.dirname(__file__), '..', 'data', 'dummy_key.json') + credentials, project = auth.get_credentials(private_key=private_key) + + assert credentials is not None + assert project is None + + +def test_get_credentials_default_credentials(monkeypatch): + import google.auth + import google.auth.credentials + import google.cloud.bigquery + + def mock_default_credentials(scopes=None, request=None): + return ( + mock.create_autospec(google.auth.credentials.Credentials), + 'default-project', + ) + + monkeypatch.setattr(google.auth, 'default', mock_default_credentials) + mock_client = mock.create_autospec(google.cloud.bigquery.Client) + monkeypatch.setattr(google.cloud.bigquery, 'Client', mock_client) + + credentials, project = auth.get_credentials() + assert project == 'default-project' + assert credentials is not None + + +def test_get_credentials_load_user_no_default(monkeypatch): + import google.auth + import google.auth.credentials + + def mock_default_credentials(scopes=None, request=None): + return (None, None) + + monkeypatch.setattr(google.auth, 'default', mock_default_credentials) + mock_user_credentials = mock.create_autospec( + google.auth.credentials.Credentials) + + def mock_load_credentials(project_id=None, credentials_path=None): + return mock_user_credentials + + monkeypatch.setattr( + auth, + 'load_user_account_credentials', + mock_load_credentials) + + credentials, project = auth.get_credentials() + assert project is None + assert credentials is mock_user_credentials From 24d0f264273acb38376830305e3a26979c5d185e Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 25 May 2018 17:06:15 -0700 Subject: [PATCH 2/2] TST: Use classmethod decorator for mocked methods. See: https://stackoverflow.com/a/29235090/101923 --- tests/unit/test_auth.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index c80e6a9a..6da3f35d 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -14,8 +14,9 @@ def test_get_credentials_private_key_contents(monkeypatch): from google.oauth2 import service_account - def from_service_account_info(key_info): - mock_credentials = mock.create_autospec(service_account.Credentials) + @classmethod + def from_service_account_info(cls, key_info): + mock_credentials = mock.create_autospec(cls) mock_credentials.with_scopes.return_value = mock_credentials mock_credentials.refresh.return_value = mock_credentials return mock_credentials @@ -38,8 +39,9 @@ def from_service_account_info(key_info): def test_get_credentials_private_key_path(monkeypatch): from google.oauth2 import service_account - def from_service_account_info(key_info): - mock_credentials = mock.create_autospec(service_account.Credentials) + @classmethod + def from_service_account_info(cls, key_info): + mock_credentials = mock.create_autospec(cls) mock_credentials.with_scopes.return_value = mock_credentials mock_credentials.refresh.return_value = mock_credentials return mock_credentials