From ec37ec231f41f9147d116cdee02cc07128ae827f Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Wed, 31 Jul 2024 12:54:07 +0200 Subject: [PATCH] add/adapt integration tests for github app auth --- .github/workflows/tests.yaml | 5 ++- README.md | 10 ++++- repo_policy_compliance/github_client.py | 26 ++++++----- src-docs/github_client.py.md | 14 +++--- tests/app/integration/conftest.py | 60 +++++++++++++++++++++---- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 43ef75ff..b5583fe4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,7 +18,10 @@ jobs: - name: Run tests (unit + integration) id: run-tests env: - GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} + AUTH_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} + AUTH_GITHUB_APP_ID : ${{ secrets.TEST_GITHUB_APP_ID }} + AUTH_GITHUB_APP_INSTALLATION_ID : ${{ secrets.TEST_GITHUB_APP_INSTALLATION_ID }} + AUTH_GITHUB_APP_PRIVATE_KEY : ${{ secrets.TEST_GITHUB_APP_PRIVATE_KEY }} run: | # Ensure that stdout appears as normal and redirect to file and exit depends on exit code of first command STDOUT_LOG=$(mktemp --suffix=stdout.log) diff --git a/README.md b/README.md index de47bacd..5078c34c 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,14 @@ failing check to be used for testing purposes. There are two types of test: the application test and the charm test. ### Application tests -To run the application tests, the `GITHUB_TOKEN` environment variable must be set. This +To run the application tests, the `AUTH_GITHUB_TOKEN` environment variable must be set. This should be a token of a user with full repo permissions for the test repository. +You can also pass in `AUTH_APP_ID`, `AUTH_INSTALLATION_ID`, and `AUTH_PRIVATE_KEY` +to test the authentication using Github App Auth. In that case, the tests will randomly select +either the token or the app auth to run the tests. Note that the Github app should be installed +in the test repository organisation/user namespace, with access granted to the test repository +and in the user namespace that owns the Github token with access to the forked repository. + The command `tox -e test` can be used to run all tests, which are primarily integration tests. You can also select the repository against which to run the tests by setting the `--repository` flag. The tests will fork the repository and create PRs against it. @@ -66,6 +72,8 @@ bot to test things like comments from a user with no write permissions or above. GitHub actions should have access to the GitHub token via a secret called `PERSONAL_GITHUB_TOKEN`. It is recommended to use either a fine-grained PAT or a token that is short-lived, e.g. 7 days. When it expires, a new token must be set. +For the GitHub App Auth, the `TEST_GITHUB_APP_ID`, `TEST_GIHUB_APP_INSTALLATION_ID`, and `TEST_GITHUB_APPP_RIVATE_KEY` +should be set as secrets. ### Charm tests diff --git a/repo_policy_compliance/github_client.py b/repo_policy_compliance/github_client.py index 98214379..8634127b 100644 --- a/repo_policy_compliance/github_client.py +++ b/repo_policy_compliance/github_client.py @@ -27,19 +27,19 @@ # Bandit thinks this constant is the real Github token GITHUB_TOKEN_ENV_NAME = "GITHUB_TOKEN" # nosec -GITHUB_APP_ID_NAME = "GITHUB_APP_ID" -GITHUB_APP_INSTALLATION_ID_NAME = "GITHUB_APP_INSTALLATION_ID" -GITHUB_APP_PRIVATE_KEY_NAME = "GITHUB_APP_PRIVATE_KEY" +GITHUB_APP_ID_ENV_NAME = "GITHUB_APP_ID" +GITHUB_APP_INSTALLATION_ID_ENV_NAME = "GITHUB_APP_INSTALLATION_ID" +GITHUB_APP_PRIVATE_KEY_ENV_NAME = "GITHUB_APP_PRIVATE_KEY" MISSING_GITHUB_CONFIG_ERR_MSG = ( - f"Either the {GITHUB_TOKEN_ENV_NAME} or not all of {GITHUB_APP_ID_NAME}," - f" {GITHUB_APP_INSTALLATION_ID_NAME}, {GITHUB_APP_PRIVATE_KEY_NAME} " + f"Either the {GITHUB_TOKEN_ENV_NAME} or not all of {GITHUB_APP_ID_ENV_NAME}," + f" {GITHUB_APP_INSTALLATION_ID_ENV_NAME}, {GITHUB_APP_PRIVATE_KEY_ENV_NAME} " f"environment variables were not provided or empty, " "it is needed for interactions with GitHub, " ) NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG = ( - f"Not all of {GITHUB_APP_ID_NAME}, {GITHUB_APP_INSTALLATION_ID_NAME}," - f" {GITHUB_APP_PRIVATE_KEY_NAME} environment variables were provided, " + f"Not all of {GITHUB_APP_ID_ENV_NAME}, {GITHUB_APP_INSTALLATION_ID_ENV_NAME}," + f" {GITHUB_APP_PRIVATE_KEY_ENV_NAME} environment variables were provided, " ) # the following is no hardcoded password PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG = ( # nosec @@ -77,12 +77,14 @@ def _get_auth() -> Auth: A GitHub auth object that is configured with a token from the environment. """ github_token = os.getenv(GITHUB_TOKEN_ENV_NAME) or os.getenv(f"FLASK_{GITHUB_TOKEN_ENV_NAME}") - github_app_id = os.getenv(GITHUB_APP_ID_NAME) or os.getenv(f"FLASK_{GITHUB_APP_ID_NAME}") - github_app_installation_id_str = os.getenv(GITHUB_APP_INSTALLATION_ID_NAME) or os.getenv( - f"FLASK_{GITHUB_APP_INSTALLATION_ID_NAME}" + github_app_id = os.getenv(GITHUB_APP_ID_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_ID_ENV_NAME}" ) - github_app_private_key = os.getenv(GITHUB_APP_PRIVATE_KEY_NAME) or os.getenv( - f"FLASK_{GITHUB_APP_PRIVATE_KEY_NAME}" + github_app_installation_id_str = os.getenv(GITHUB_APP_INSTALLATION_ID_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_INSTALLATION_ID_ENV_NAME}" + ) + github_app_private_key = os.getenv(GITHUB_APP_PRIVATE_KEY_ENV_NAME) or os.getenv( + f"FLASK_{GITHUB_APP_PRIVATE_KEY_ENV_NAME}" ) _ensure_either_github_token_or_app_config( diff --git a/src-docs/github_client.py.md b/src-docs/github_client.py.md index 28957794..38066eb5 100644 --- a/src-docs/github_client.py.md +++ b/src-docs/github_client.py.md @@ -8,9 +8,9 @@ Module for GitHub client. **Global Variables** --------------- - **GITHUB_TOKEN_ENV_NAME** -- **GITHUB_APP_ID_NAME** -- **GITHUB_APP_INSTALLATION_ID_NAME** -- **GITHUB_APP_PRIVATE_KEY_NAME** +- **GITHUB_APP_ID_ENV_NAME** +- **GITHUB_APP_INSTALLATION_ID_ENV_NAME** +- **GITHUB_APP_PRIVATE_KEY_ENV_NAME** - **MISSING_GITHUB_CONFIG_ERR_MSG** - **NOT_ALL_GITHUB_APP_CONFIG_ERR_MSG** - **PROVIDED_GITHUB_TOKEN_AND_APP_CONFIG_ERR_MSG** @@ -41,7 +41,7 @@ Get a GitHub client. --- - + ## function `inject` @@ -65,7 +65,7 @@ Injects a GitHub client as the first argument to a function. --- - + ## function `get_collaborators` @@ -95,7 +95,7 @@ Get collaborators with a given affiliation and permission. --- - + ## function `get_branch` @@ -125,7 +125,7 @@ Get the branch for the check. --- - + ## function `get_collaborator_permission` diff --git a/tests/app/integration/conftest.py b/tests/app/integration/conftest.py index e3a7396f..31ff0ad0 100644 --- a/tests/app/integration/conftest.py +++ b/tests/app/integration/conftest.py @@ -2,8 +2,9 @@ # See LICENSE file for licensing details. """Fixtures for integration tests.""" - +import logging import os +import random from typing import Iterator, cast import pytest @@ -16,13 +17,54 @@ from github.Repository import Repository import repo_policy_compliance -from repo_policy_compliance.github_client import GITHUB_TOKEN_ENV_NAME, get_collaborators -from repo_policy_compliance.github_client import inject as inject_github_client +from repo_policy_compliance.github_client import ( + GITHUB_APP_ID_ENV_NAME, + GITHUB_APP_INSTALLATION_ID_ENV_NAME, + GITHUB_APP_PRIVATE_KEY_ENV_NAME, + GITHUB_TOKEN_ENV_NAME, + get_collaborators, +) from ...conftest import REPOSITORY_ARGUMENT_NAME from . import branch_protection from .types_ import BranchWithProtection, RequestedCollaborator +logger = logging.getLogger(__name__) + +TEST_GITHUB_APP_ID_ENV_NAME = f"AUTH_{GITHUB_APP_ID_ENV_NAME}" +TEST_GITHUB_APP_INSTALLATION_ID_ENV_NAME = f"AUTH_{GITHUB_APP_INSTALLATION_ID_ENV_NAME}" +TEST_GITHUB_APP_PRIVATE_KEY_ENV_NAME = f"AUTH_{GITHUB_APP_PRIVATE_KEY_ENV_NAME}" +TEST_GITHUB_TOKEN_ENV_NAME = f"AUTH_{GITHUB_TOKEN_ENV_NAME}" + + +@pytest.fixture(scope="session", name="randomize_github_auth_method", autouse=True) +def fixture_randomize_github_auth_method(monkeypatch: pytest.MonkeyPatch) -> None: + """Randomize the GitHub authentication method. + + Either use Github Token auth or Github App auth if the latter is set. + This is achieved by monkeypatching the environment variables. + """ + app_id = os.getenv(TEST_GITHUB_APP_ID_ENV_NAME) + app_install_id = os.getenv(TEST_GITHUB_APP_INSTALLATION_ID_ENV_NAME) + app_private_key = os.getenv(TEST_GITHUB_APP_PRIVATE_KEY_ENV_NAME) + github_token = os.getenv(TEST_GITHUB_TOKEN_ENV_NAME) + + # random is not used for security purposes + if random.random() < 0.5 and app_id and app_install_id and app_private_key: # nosec + monkeypatch.delenv(GITHUB_TOKEN_ENV_NAME, raising=False) + monkeypatch.setenv(GITHUB_APP_ID_ENV_NAME, app_id) + monkeypatch.setenv(GITHUB_APP_INSTALLATION_ID_ENV_NAME, app_install_id) + monkeypatch.setenv(GITHUB_APP_PRIVATE_KEY_ENV_NAME, app_private_key) + logger.info("Using GitHub App authentication for this test.") + else: + assert ( + github_token + ), f"GitHub token must be set in the environment variable {TEST_GITHUB_TOKEN_ENV_NAME}" + monkeypatch.delenv(GITHUB_APP_ID_ENV_NAME, raising=False) + monkeypatch.delenv(GITHUB_APP_INSTALLATION_ID_ENV_NAME, raising=False) + monkeypatch.delenv(GITHUB_APP_PRIVATE_KEY_ENV_NAME, raising=False) + monkeypatch.setenv(GITHUB_TOKEN_ENV_NAME, github_token) + @pytest.fixture(scope="session", name="github_repository_name") def fixture_github_repository_name(pytestconfig: pytest.Config) -> str: @@ -31,10 +73,12 @@ def fixture_github_repository_name(pytestconfig: pytest.Config) -> str: @pytest.fixture(scope="session", name="github_token") -def fixutre_github_token() -> str: +def fixture_github_token() -> str: """Get the GitHub token from the environment.""" - github_token = os.getenv(GITHUB_TOKEN_ENV_NAME) - assert github_token, f"GitHub must be set in the environment variable {GITHUB_TOKEN_ENV_NAME}" + github_token = os.getenv(TEST_GITHUB_TOKEN_ENV_NAME) + assert ( + github_token + ), f"GitHub must be set in the environment variable {TEST_GITHUB_TOKEN_ENV_NAME}" return github_token @@ -64,9 +108,9 @@ def fixture_ci_github_repository( @pytest.fixture(scope="session", name="github_repository") -def fixture_github_repository(github_repository_name: str) -> Repository: +def fixture_github_repository(github_repository_name: str, github_token: str) -> Repository: """Returns client to the Github repository.""" - github_client = inject_github_client(lambda client: client)() + github_client = Github(auth=Token(github_token)) return github_client.get_repo(github_repository_name)