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