From 42e5d7e687251f19ba5fe5937b2756ad5beb1875 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Wed, 26 Oct 2016 15:31:21 -0700 Subject: [PATCH 1/3] Add App Engine system tests --- .travis.yml | 4 +- system_tests/app_engine/app/.gitignore | 1 + system_tests/app_engine/app/app.yaml | 12 +++ .../app_engine/app/appengine_config.py | 29 ++++++ system_tests/app_engine/app/main.py | 99 +++++++++++++++++++ system_tests/app_engine/app/requirements.txt | 3 + system_tests/app_engine/test_app_engine.py | 58 +++++++++++ tox.ini | 8 +- 8 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 system_tests/app_engine/app/.gitignore create mode 100644 system_tests/app_engine/app/app.yaml create mode 100644 system_tests/app_engine/app/appengine_config.py create mode 100644 system_tests/app_engine/app/main.py create mode 100644 system_tests/app_engine/app/requirements.txt create mode 100644 system_tests/app_engine/test_app_engine.py diff --git a/.travis.yml b/.travis.yml index 315f7c14c..aa39f9a4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,9 @@ matrix: - python: 3.5 env: TOXENV=cover - python: 3.5 - env: TOXENV=py35-system SYSTEM_TEST=1 + env: TOXENV=py35-system SYSTEM_TEST=1 SKIP_APP_ENGINE_SYSTEM_TEST=1 - python: 2.7 - env: TOXENV=py27-system SYSTEM_TEST=1 + env: TOXENV=py27-system SYSTEM_TEST=1 SKIP_APP_ENGINE_SYSTEM_TEST=1 cache: directories: - ${HOME}/.cache diff --git a/system_tests/app_engine/app/.gitignore b/system_tests/app_engine/app/.gitignore new file mode 100644 index 000000000..a65b41774 --- /dev/null +++ b/system_tests/app_engine/app/.gitignore @@ -0,0 +1 @@ +lib diff --git a/system_tests/app_engine/app/app.yaml b/system_tests/app_engine/app/app.yaml new file mode 100644 index 000000000..872efb37b --- /dev/null +++ b/system_tests/app_engine/app/app.yaml @@ -0,0 +1,12 @@ +api_version: 1 +service: google-auth-system-tests +runtime: python27 +threadsafe: true + +handlers: +- url: .* + script: main.app + +libraries: +- name: ssl + version: 2.7.11 diff --git a/system_tests/app_engine/app/appengine_config.py b/system_tests/app_engine/app/appengine_config.py new file mode 100644 index 000000000..da02e10d0 --- /dev/null +++ b/system_tests/app_engine/app/appengine_config.py @@ -0,0 +1,29 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.appengine.ext import vendor + +# Add any libraries installed in the "lib" folder. +vendor.add('lib') + + +# Patch os.path.expanduser. This should be fixed in GAE +# versions released after Nov 2016. +import os.path + + +def patched_expanduser(path): + return path + +os.path.expanduser = patched_expanduser diff --git a/system_tests/app_engine/app/main.py b/system_tests/app_engine/app/main.py new file mode 100644 index 000000000..0e07347a8 --- /dev/null +++ b/system_tests/app_engine/app/main.py @@ -0,0 +1,99 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import sys +from StringIO import StringIO +import traceback + +from google.appengine.api import app_identity +import google.auth +from google.auth import app_engine +import google.auth.transport.urllib3 +import urllib3.contrib.appengine +import webapp2 + + +http = urllib3.contrib.appengine.AppEngineManager() +http_request = google.auth.transport.urllib3.Request(http) + + +def test_credentials(): + credentials = app_engine.Credentials() + scoped_credentials = credentials.with_scopes([ + 'https://www.googleapis.com/auth/userinfo.email']) + + scoped_credentials.refresh(None) + + assert scoped_credentials.valid + assert scoped_credentials.token is not None + + +def test_default(): + credentials, project_id = google.auth.default() + + assert isinstance(credentials, app_engine.Credentials) + assert project_id == app_identity.get_application_id() + + +@contextlib.contextmanager +def capture(): + """Context manager that captures stderr and stdout.""" + oldout, olderr = sys.stdout, sys.stderr + try: + out = StringIO() + sys.stdout, sys.stderr = out, out + yield out + finally: + sys.stdout, sys.stderr = oldout, olderr + + +def run_tests(): + """Runs all tests. + + Returns: + Tuple[bool, str]: A tuple containing True if all tests pass, False + otherwise, and any captured output from the tests. + """ + status = False + output = '' + + with capture() as capsys: + try: + test_credentials() + test_default() + status = True + except Exception: + status = False + output = 'Stacktrace:\n{}\n'.format(traceback.format_exc()) + + output += 'Captured output:\n{}'.format(capsys.getvalue()) + return status, output + + +class MainHandler(webapp2.RequestHandler): + def get(self): + self.response.headers['content-type'] = 'text/plain' + + status, output = run_tests() + + if not status: + self.response.status = 500 + + self.response.write(output) + + +app = webapp2.WSGIApplication([ + ('/', MainHandler) +], debug=True) diff --git a/system_tests/app_engine/app/requirements.txt b/system_tests/app_engine/app/requirements.txt new file mode 100644 index 000000000..bd5c476ab --- /dev/null +++ b/system_tests/app_engine/app/requirements.txt @@ -0,0 +1,3 @@ +urllib3 +# Relative path to google-auth-python's source. +../../.. diff --git a/system_tests/app_engine/test_app_engine.py b/system_tests/app_engine/test_app_engine.py new file mode 100644 index 000000000..cfeb4a99b --- /dev/null +++ b/system_tests/app_engine/test_app_engine.py @@ -0,0 +1,58 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess + +from google.auth import _cloud_sdk +import pytest + + +HERE = os.path.dirname(__file__) +TEST_APP_DIR = os.path.join(HERE, 'app') +TEST_APP_SERVICE = 'google-auth-system-tests' + + +def vendor_app_dependencies(): + """Vendors in the test application's third-party dependencies.""" + subprocess.check_call( + ['pip', 'install', '-t', 'lib', '-r', 'requirements.txt']) + + +def deploy_app(): + """Deploys the test application using gcloud.""" + subprocess.check_call( + ['gcloud', 'app', 'deploy', '-q', 'app.yaml']) + + +@pytest.fixture +def app(monkeypatch): + monkeypatch.chdir(TEST_APP_DIR) + + vendor_app_dependencies() + deploy_app() + + application_id = _cloud_sdk.get_project_id() + application_url = 'https://{}-dot-{}.appspot.com'.format( + TEST_APP_SERVICE, application_id) + + yield application_url + + +@pytest.mark.skipif( + 'SKIP_APP_ENGINE_SYSTEM_TEST' in os.environ, + reason='Explicitly skipping App Engine system tests.') +def test_live_application(app, http_request): + response = http_request(method='GET', url=app) + assert response.status == 200, response.data.decode('utf-8') diff --git a/tox.ini b/tox.ini index 10f7d9946..6864ddee7 100644 --- a/tox.ini +++ b/tox.ini @@ -24,16 +24,20 @@ deps = [testenv:py35-system] basepython = python3.5 commands = - py.test system_tests + py.test --ignore system_tests/app_engine/app {posargs:system_tests} deps = {[testenv]deps} +passenv = + SKIP_APP_ENGINE_SYSTEM_TEST [testenv:py27-system] basepython = python2.7 commands = - py.test system_tests + py.test --ignore system_tests/app_engine/app {posargs:system_tests} deps = {[testenv]deps} +passenv = + SKIP_APP_ENGINE_SYSTEM_TEST [testenv:docgen] basepython = python3.5 From 1f4e69b236e4fc891d19c6a84b6ae39d4050711a Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Wed, 26 Oct 2016 15:38:05 -0700 Subject: [PATCH 2/3] Add token info check --- system_tests/app_engine/app/main.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/system_tests/app_engine/app/main.py b/system_tests/app_engine/app/main.py index 0e07347a8..d5957c010 100644 --- a/system_tests/app_engine/app/main.py +++ b/system_tests/app_engine/app/main.py @@ -13,32 +13,43 @@ # limitations under the License. import contextlib +import json import sys from StringIO import StringIO import traceback from google.appengine.api import app_identity import google.auth +from google.auth import _helpers from google.auth import app_engine import google.auth.transport.urllib3 import urllib3.contrib.appengine import webapp2 - -http = urllib3.contrib.appengine.AppEngineManager() -http_request = google.auth.transport.urllib3.Request(http) +TOKEN_INFO_URL = 'https://www.googleapis.com/oauth2/v3/tokeninfo' +EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email' +HTTP = urllib3.contrib.appengine.AppEngineManager() +HTTP_REQUEST = google.auth.transport.urllib3.Request(HTTP) def test_credentials(): credentials = app_engine.Credentials() - scoped_credentials = credentials.with_scopes([ - 'https://www.googleapis.com/auth/userinfo.email']) + scoped_credentials = credentials.with_scopes([EMAIL_SCOPE]) scoped_credentials.refresh(None) assert scoped_credentials.valid assert scoped_credentials.token is not None + # Get token info and verify scope + url = _helpers.update_query(TOKEN_INFO_URL, { + 'access_token': scoped_credentials.token + }) + response = HTTP_REQUEST(url=url, method='GET') + token_info = json.loads(response.data.decode('utf-8')) + + assert token_info['scope'] == EMAIL_SCOPE + def test_default(): credentials, project_id = google.auth.default() From 1aaf3845b688b11f5e78b15b9655c09a948bb7b3 Mon Sep 17 00:00:00 2001 From: Jon Wayne Parrott Date: Thu, 27 Oct 2016 12:36:22 -0700 Subject: [PATCH 3/3] Address review comments --- system_tests/app_engine/app/main.py | 45 +++++++++++++++------- system_tests/app_engine/test_app_engine.py | 6 +-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/system_tests/app_engine/app/main.py b/system_tests/app_engine/app/main.py index d5957c010..d19567d46 100644 --- a/system_tests/app_engine/app/main.py +++ b/system_tests/app_engine/app/main.py @@ -12,6 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""App Engine standard application that runs basic system tests for +google.auth.app_engine. + +This application has to run tests manually instead of using py.test because +py.test currently doesn't work on App Engine standard. +""" + import contextlib import json import sys @@ -43,7 +50,7 @@ def test_credentials(): # Get token info and verify scope url = _helpers.update_query(TOKEN_INFO_URL, { - 'access_token': scoped_credentials.token + 'access_token': scoped_credentials.token, }) response = HTTP_REQUEST(url=url, method='GET') token_info = json.loads(response.data.decode('utf-8')) @@ -70,6 +77,21 @@ def capture(): sys.stdout, sys.stderr = oldout, olderr +def run_test_func(func): + with capture() as capsys: + try: + func() + return True, '' + except Exception as exc: + output = ( + 'Test {} failed: {}\n\n' + 'Stacktrace:\n{}\n\n' + 'Captured output:\n{}').format( + func.func_name, exc, traceback.format_exc(), + capsys.getvalue()) + return False, output + + def run_tests(): """Runs all tests. @@ -77,19 +99,16 @@ def run_tests(): Tuple[bool, str]: A tuple containing True if all tests pass, False otherwise, and any captured output from the tests. """ - status = False + status = True output = '' - with capture() as capsys: - try: - test_credentials() - test_default() - status = True - except Exception: - status = False - output = 'Stacktrace:\n{}\n'.format(traceback.format_exc()) - - output += 'Captured output:\n{}'.format(capsys.getvalue()) + tests = (test_credentials, test_default) + + for test in tests: + test_status, test_output = run_test_func(test) + status = status and test_status + output += test_output + return status, output @@ -106,5 +125,5 @@ def get(self): app = webapp2.WSGIApplication([ - ('/', MainHandler) + ('/', MainHandler), ], debug=True) diff --git a/system_tests/app_engine/test_app_engine.py b/system_tests/app_engine/test_app_engine.py index cfeb4a99b..a6ccb718f 100644 --- a/system_tests/app_engine/test_app_engine.py +++ b/system_tests/app_engine/test_app_engine.py @@ -18,7 +18,7 @@ from google.auth import _cloud_sdk import pytest - +SKIP_TEST_ENV = 'SKIP_APP_ENGINE_SYSTEM_TEST' HERE = os.path.dirname(__file__) TEST_APP_DIR = os.path.join(HERE, 'app') TEST_APP_SERVICE = 'google-auth-system-tests' @@ -27,7 +27,7 @@ def vendor_app_dependencies(): """Vendors in the test application's third-party dependencies.""" subprocess.check_call( - ['pip', 'install', '-t', 'lib', '-r', 'requirements.txt']) + ['pip', 'install', '--target', 'lib', '-r', 'requirements.txt']) def deploy_app(): @@ -51,7 +51,7 @@ def app(monkeypatch): @pytest.mark.skipif( - 'SKIP_APP_ENGINE_SYSTEM_TEST' in os.environ, + SKIP_TEST_ENV in os.environ, reason='Explicitly skipping App Engine system tests.') def test_live_application(app, http_request): response = http_request(method='GET', url=app)