1# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Noxfile for automating system tests.
16
17This file handles setting up environments needed by the system tests. This
18separates the tests from their environment configuration.
19
20See the `nox docs`_ for details on how this file works:
21
22.. _nox docs: http://nox.readthedocs.io/en/latest/
23"""
24
25import os
26import subprocess
27
28from nox.command import which
29import nox
30import py.path
31
32HERE = os.path.abspath(os.path.dirname(__file__))
33LIBRARY_DIR = os.path.abspath(os.path.dirname(HERE))
34DATA_DIR = os.path.join(HERE, "data")
35SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
36AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
37EXPLICIT_CREDENTIALS_ENV = "GOOGLE_APPLICATION_CREDENTIALS"
38EXPLICIT_PROJECT_ENV = "GOOGLE_CLOUD_PROJECT"
39EXPECT_PROJECT_ENV = "EXPECT_PROJECT_ID"
40
41SKIP_GAE_TEST_ENV = "SKIP_APP_ENGINE_SYSTEM_TEST"
42GAE_APP_URL_TMPL = "https://{}-dot-{}.appspot.com"
43GAE_TEST_APP_SERVICE = "google-auth-system-tests"
44
45# The download location for the Cloud SDK
46CLOUD_SDK_DIST_FILENAME = "google-cloud-sdk.tar.gz"
47CLOUD_SDK_DOWNLOAD_URL = "https://dl.google.com/dl/cloudsdk/release/{}".format(
48    CLOUD_SDK_DIST_FILENAME
49)
50
51# This environment variable is recognized by the Cloud SDK and overrides
52# the location of the SDK's configuration files (which is usually at
53# ${HOME}/.config).
54CLOUD_SDK_CONFIG_ENV = "CLOUDSDK_CONFIG"
55
56# If set, this is where the environment setup will install the Cloud SDK.
57# If unset, it will download the SDK to a temporary directory.
58CLOUD_SDK_ROOT = os.environ.get("CLOUD_SDK_ROOT")
59
60if CLOUD_SDK_ROOT is not None:
61    CLOUD_SDK_ROOT = py.path.local(CLOUD_SDK_ROOT)
62    CLOUD_SDK_ROOT.ensure(dir=True)  # Makes sure the directory exists.
63else:
64    CLOUD_SDK_ROOT = py.path.local.mkdtemp()
65
66# The full path the cloud sdk install directory
67CLOUD_SDK_INSTALL_DIR = CLOUD_SDK_ROOT.join("google-cloud-sdk")
68
69# The full path to the gcloud cli executable.
70GCLOUD = str(CLOUD_SDK_INSTALL_DIR.join("bin", "gcloud"))
71
72# gcloud requires Python 2 and doesn't work on 3, so we need to tell it
73# where to find 2 when we're running in a 3 environment.
74CLOUD_SDK_PYTHON_ENV = "CLOUDSDK_PYTHON"
75CLOUD_SDK_PYTHON = which("python2", None)
76
77# Cloud SDK helpers
78
79
80def install_cloud_sdk(session):
81    """Downloads and installs the Google Cloud SDK."""
82    # Configure environment variables needed by the SDK.
83    # This sets the config root to the tests' config root. This prevents
84    # our tests from clobbering a developer's configuration when running
85    # these tests locally.
86    session.env[CLOUD_SDK_CONFIG_ENV] = str(CLOUD_SDK_ROOT)
87    # This tells gcloud which Python interpreter to use (always use 2.7)
88    session.env[CLOUD_SDK_PYTHON_ENV] = CLOUD_SDK_PYTHON
89    # This set the $PATH for the subprocesses so they can find the gcloud
90    # executable.
91    session.env["PATH"] = (
92        str(CLOUD_SDK_INSTALL_DIR.join("bin")) + os.pathsep + os.environ["PATH"]
93    )
94
95    # If gcloud cli executable already exists, just update it.
96    if py.path.local(GCLOUD).exists():
97        session.run(GCLOUD, "components", "update", "-q")
98        return
99
100    tar_path = CLOUD_SDK_ROOT.join(CLOUD_SDK_DIST_FILENAME)
101
102    # Download the release.
103    session.run("wget", CLOUD_SDK_DOWNLOAD_URL, "-O", str(tar_path), silent=True)
104
105    # Extract the release.
106    session.run("tar", "xzf", str(tar_path), "-C", str(CLOUD_SDK_ROOT))
107    session.run(tar_path.remove)
108
109    # Run the install script.
110    session.run(
111        str(CLOUD_SDK_INSTALL_DIR.join("install.sh")),
112        "--usage-reporting",
113        "false",
114        "--path-update",
115        "false",
116        "--command-completion",
117        "false",
118        silent=True,
119    )
120
121
122def copy_credentials(credentials_path):
123    """Copies credentials into the SDK root as the application default
124    credentials."""
125    dest = CLOUD_SDK_ROOT.join("application_default_credentials.json")
126    if dest.exists():
127        dest.remove()
128    py.path.local(credentials_path).copy(dest)
129
130
131def configure_cloud_sdk(session, application_default_credentials, project=False):
132    """Installs and configures the Cloud SDK with the given application default
133    credentials.
134
135    If project is True, then a project will be set in the active config.
136    If it is false, this will ensure no project is set.
137    """
138    install_cloud_sdk(session)
139
140    # Setup the service account as the default user account. This is
141    # needed for the project ID detection to work. Note that this doesn't
142    # change the application default credentials file, which is user
143    # credentials instead of service account credentials sometimes.
144    session.run(
145        GCLOUD, "auth", "activate-service-account", "--key-file", SERVICE_ACCOUNT_FILE
146    )
147
148    if project:
149        session.run(GCLOUD, "config", "set", "project", "example-project")
150    else:
151        session.run(GCLOUD, "config", "unset", "project")
152
153    # Copy the credentials file to the config root. This is needed because
154    # unfortunately gcloud doesn't provide a clean way to tell it to use
155    # a particular set of credentials. However, this does verify that gcloud
156    # also considers the credentials valid by calling application-default
157    # print-access-token
158    session.run(copy_credentials, application_default_credentials)
159
160    # Calling this forces the Cloud SDK to read the credentials we just wrote
161    # and obtain a new access token with those credentials. This validates
162    # that our credentials matches the format expected by gcloud.
163    # Silent is set to True to prevent leaking secrets in test logs.
164    session.run(
165        GCLOUD, "auth", "application-default", "print-access-token", silent=True
166    )
167
168
169# Test sesssions
170
171TEST_DEPENDENCIES_ASYNC = ["aiohttp", "pytest-asyncio", "nest-asyncio"]
172TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock"]
173PYTHON_VERSIONS_ASYNC = ["3.7"]
174PYTHON_VERSIONS_SYNC = ["2.7", "3.7"]
175
176
177def default(session, *test_paths):
178    # replace 'session._runner.friendly_name' with
179    # session.name once nox has released a new version
180    # https://github.com/theacodes/nox/pull/386
181    sponge_log = f"--junitxml=system_{str(session._runner.friendly_name)}_sponge_log.xml"
182    session.run(
183        "pytest", sponge_log, *test_paths,
184    )
185
186
187@nox.session(python=PYTHON_VERSIONS_SYNC)
188def service_account_sync(session):
189    session.install(*TEST_DEPENDENCIES_SYNC)
190    session.install(LIBRARY_DIR)
191    default(
192        session,
193        "system_tests_sync/test_service_account.py",
194        *session.posargs,
195    )
196
197
198@nox.session(python=PYTHON_VERSIONS_SYNC)
199def default_explicit_service_account(session):
200    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
201    session.env[EXPECT_PROJECT_ENV] = "1"
202    session.install(*TEST_DEPENDENCIES_SYNC)
203    session.install(LIBRARY_DIR)
204    default(
205        session,
206        "system_tests_sync/test_default.py",
207        "system_tests_sync/test_id_token.py",
208        *session.posargs,
209    )
210
211
212@nox.session(python=PYTHON_VERSIONS_SYNC)
213def default_explicit_authorized_user(session):
214    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
215    session.install(*TEST_DEPENDENCIES_SYNC)
216    session.install(LIBRARY_DIR)
217    default(
218        session,
219        "system_tests_sync/test_default.py",
220        *session.posargs,
221    )
222
223
224@nox.session(python=PYTHON_VERSIONS_SYNC)
225def default_explicit_authorized_user_explicit_project(session):
226    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
227    session.env[EXPLICIT_PROJECT_ENV] = "example-project"
228    session.env[EXPECT_PROJECT_ENV] = "1"
229    session.install(*TEST_DEPENDENCIES_SYNC)
230    session.install(LIBRARY_DIR)
231    default(
232        session,
233        "system_tests_sync/test_default.py",
234        *session.posargs,
235    )
236
237
238@nox.session(python=PYTHON_VERSIONS_SYNC)
239def default_cloud_sdk_service_account(session):
240    configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
241    session.env[EXPECT_PROJECT_ENV] = "1"
242    session.install(*TEST_DEPENDENCIES_SYNC)
243    session.install(LIBRARY_DIR)
244    default(
245        session,
246        "system_tests_sync/test_default.py",
247        *session.posargs,
248    )
249
250
251@nox.session(python=PYTHON_VERSIONS_SYNC)
252def default_cloud_sdk_authorized_user(session):
253    configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
254    session.install(*TEST_DEPENDENCIES_SYNC)
255    session.install(LIBRARY_DIR)
256    default(
257        session,
258        "system_tests_sync/test_default.py",
259        *session.posargs,
260    )
261
262
263@nox.session(python=PYTHON_VERSIONS_SYNC)
264def default_cloud_sdk_authorized_user_configured_project(session):
265    configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
266    session.env[EXPECT_PROJECT_ENV] = "1"
267    session.install(*TEST_DEPENDENCIES_SYNC)
268    session.install(LIBRARY_DIR)
269    default(
270        session,
271        "system_tests_sync/test_default.py",
272        *session.posargs,
273    )
274
275
276@nox.session(python=PYTHON_VERSIONS_SYNC)
277def compute_engine(session):
278    session.install(*TEST_DEPENDENCIES_SYNC)
279    # unset Application Default Credentials so
280    # credentials are detected from environment
281    del session.virtualenv.env["GOOGLE_APPLICATION_CREDENTIALS"]
282    session.install(LIBRARY_DIR)
283    default(
284        session,
285        "system_tests_sync/test_compute_engine.py",
286        *session.posargs,
287    )
288
289
290@nox.session(python=["2.7"])
291def app_engine(session):
292    if SKIP_GAE_TEST_ENV in os.environ:
293        session.log("Skipping App Engine tests.")
294        return
295
296    session.install(LIBRARY_DIR)
297    # Unlike the default tests above, the App Engine system test require a
298    # 'real' gcloud sdk installation that is configured to deploy to an
299    # app engine project.
300    # Grab the project ID from the cloud sdk.
301    project_id = (
302        subprocess.check_output(
303            ["gcloud", "config", "list", "project", "--format", "value(core.project)"]
304        )
305        .decode("utf-8")
306        .strip()
307    )
308
309    if not project_id:
310        session.error(
311            "The Cloud SDK must be installed and configured to deploy to App " "Engine."
312        )
313
314    application_url = GAE_APP_URL_TMPL.format(GAE_TEST_APP_SERVICE, project_id)
315
316    # Vendor in the test application's dependencies
317    session.chdir(os.path.join(HERE, "system_tests_sync/app_engine_test_app"))
318    session.install(*TEST_DEPENDENCIES_SYNC)
319    session.run(
320        "pip", "install", "--target", "lib", "-r", "requirements.txt", silent=True
321    )
322
323    # Deploy the application.
324    session.run("gcloud", "app", "deploy", "-q", "app.yaml")
325
326    # Run the tests
327    session.env["TEST_APP_URL"] = application_url
328    session.chdir(HERE)
329    default(
330        session, "system_tests_sync/test_app_engine.py",
331    )
332
333
334@nox.session(python=PYTHON_VERSIONS_SYNC)
335def grpc(session):
336    session.install(LIBRARY_DIR)
337    session.install(*TEST_DEPENDENCIES_SYNC, "google-cloud-pubsub==1.7.0")
338    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
339    default(
340        session,
341        "system_tests_sync/test_grpc.py",
342        *session.posargs,
343    )
344
345
346@nox.session(python=PYTHON_VERSIONS_SYNC)
347def requests(session):
348    session.install(LIBRARY_DIR)
349    session.install(*TEST_DEPENDENCIES_SYNC)
350    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
351    default(
352        session,
353        "system_tests_sync/test_requests.py",
354        *session.posargs,
355    )
356
357
358@nox.session(python=PYTHON_VERSIONS_SYNC)
359def urllib3(session):
360    session.install(LIBRARY_DIR)
361    session.install(*TEST_DEPENDENCIES_SYNC)
362    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
363    default(
364        session,
365        "system_tests_sync/test_urllib3.py",
366        *session.posargs,
367    )
368
369
370@nox.session(python=PYTHON_VERSIONS_SYNC)
371def mtls_http(session):
372    session.install(LIBRARY_DIR)
373    session.install(*TEST_DEPENDENCIES_SYNC, "pyopenssl")
374    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
375    default(
376        session,
377        "system_tests_sync/test_mtls_http.py",
378        *session.posargs,
379    )
380
381
382@nox.session(python=PYTHON_VERSIONS_SYNC)
383def external_accounts(session):
384    session.install(
385        *TEST_DEPENDENCIES_SYNC,
386        LIBRARY_DIR,
387        "google-api-python-client",
388    )
389    default(
390        session,
391        "system_tests_sync/test_external_accounts.py",
392        *session.posargs,
393    )
394
395
396@nox.session(python=PYTHON_VERSIONS_SYNC)
397def downscoping(session):
398    session.install(
399        *TEST_DEPENDENCIES_SYNC,
400        LIBRARY_DIR,
401        "google-cloud-storage",
402    )
403    default(
404        session,
405        "system_tests_sync/test_downscoping.py",
406        *session.posargs,
407    )
408
409
410# ASYNC SYSTEM TESTS
411
412@nox.session(python=PYTHON_VERSIONS_ASYNC)
413def service_account_async(session):
414    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
415    session.install(LIBRARY_DIR)
416    default(
417        session,
418        "system_tests_async/test_service_account.py",
419        *session.posargs,
420    )
421
422
423@nox.session(python=PYTHON_VERSIONS_ASYNC)
424def default_explicit_service_account_async(session):
425    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
426    session.env[EXPECT_PROJECT_ENV] = "1"
427    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
428    session.install(LIBRARY_DIR)
429    default(
430        session,
431        "system_tests_async/test_default.py",
432        "system_tests_async/test_id_token.py",
433        *session.posargs,
434    )
435
436
437@nox.session(python=PYTHON_VERSIONS_ASYNC)
438def default_explicit_authorized_user_async(session):
439    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
440    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
441    session.install(LIBRARY_DIR)
442    default(
443        session,
444        "system_tests_async/test_default.py",
445        *session.posargs,
446    )
447
448
449@nox.session(python=PYTHON_VERSIONS_ASYNC)
450def default_explicit_authorized_user_explicit_project_async(session):
451    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
452    session.env[EXPLICIT_PROJECT_ENV] = "example-project"
453    session.env[EXPECT_PROJECT_ENV] = "1"
454    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
455    session.install(LIBRARY_DIR)
456    default(
457        session,
458        "system_tests_async/test_default.py",
459        *session.posargs,
460    )
461
462
463@nox.session(python=PYTHON_VERSIONS_ASYNC)
464def default_cloud_sdk_service_account_async(session):
465    configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
466    session.env[EXPECT_PROJECT_ENV] = "1"
467    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
468    session.install(LIBRARY_DIR)
469    default(
470        session,
471        "system_tests_async/test_default.py",
472        *session.posargs,
473    )
474
475
476@nox.session(python=PYTHON_VERSIONS_ASYNC)
477def default_cloud_sdk_authorized_user_async(session):
478    configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
479    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
480    session.install(LIBRARY_DIR)
481    default(
482        session,
483        "system_tests_async/test_default.py",
484        *session.posargs,
485    )
486
487
488@nox.session(python=PYTHON_VERSIONS_ASYNC)
489def default_cloud_sdk_authorized_user_configured_project_async(session):
490    configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
491    session.env[EXPECT_PROJECT_ENV] = "1"
492    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
493    session.install(LIBRARY_DIR)
494    default(
495        session,
496        "system_tests_async/test_default.py",
497        *session.posargs,
498    )
499