blob: 142cf226bbcd2d65fe0ff3da7336f91af1dd198c [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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 __future__ import annotations
import subprocess
from unittest import mock
import pytest
from airflow_breeze.utils.github import (
env_without_github_tokens,
retrieve_github_token,
run_gh_command,
)
from airflow_breeze.utils.shared_options import set_dry_run
def _completed_process(returncode: int, stdout: str = "") -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(args=["gh"], returncode=returncode, stdout=stdout, stderr="")
def test_env_without_github_tokens_removes_ambient_token_vars(monkeypatch):
monkeypatch.setenv("GH_TOKEN", "gh-token")
monkeypatch.setenv("GITHUB_TOKEN", "github-token")
monkeypatch.setenv("OTHER_VAR", "kept")
cleaned_env = env_without_github_tokens()
assert "GH_TOKEN" not in cleaned_env
assert "GITHUB_TOKEN" not in cleaned_env
assert cleaned_env["OTHER_VAR"] == "kept"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_prefers_clean_gh_auth_token(mock_run, monkeypatch):
monkeypatch.setenv("GH_TOKEN", "env-gh-token")
monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
mock_run.return_value = _completed_process(returncode=0, stdout="stored-gh-token\n")
assert retrieve_github_token() == "stored-gh-token"
mock_run.assert_called_once()
call_env = mock_run.call_args.kwargs["env"]
assert "GH_TOKEN" not in call_env
assert "GITHUB_TOKEN" not in call_env
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_falls_back_to_env_token(mock_run, monkeypatch):
monkeypatch.setenv("GH_TOKEN", "env-gh-token")
monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
mock_run.return_value = _completed_process(returncode=1)
assert retrieve_github_token() == "env-gh-token"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_falls_back_to_env_token_when_gh_is_missing(mock_run, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
mock_run.side_effect = FileNotFoundError
assert retrieve_github_token() == "env-github-token"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_falls_back_to_env_token_when_gh_returns_whitespace(mock_run, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
mock_run.return_value = _completed_process(returncode=0, stdout=" \n")
assert retrieve_github_token() == "env-github-token"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_keeps_explicit_token(mock_run, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "env-token")
assert retrieve_github_token("explicit-token") == "explicit-token"
mock_run.assert_not_called()
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_retrieve_github_token_does_not_treat_env_token_argument_as_explicit(mock_run, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "env-token")
mock_run.return_value = _completed_process(returncode=0, stdout="stored-gh-token\n")
assert retrieve_github_token("env-token") == "stored-gh-token"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_run_gh_command_retries_with_original_env_after_clean_env_failure(mock_run, monkeypatch):
monkeypatch.setenv("GH_TOKEN", "env-gh-token")
monkeypatch.setenv("GITHUB_TOKEN", "env-github-token")
mock_run.side_effect = [
_completed_process(returncode=1),
_completed_process(returncode=0),
]
result = run_gh_command(["gh", "workflow", "run", "docs.yml"], capture_output=True)
assert result.returncode == 0
assert mock_run.call_count == 2
first_env = mock_run.call_args_list[0].kwargs["env"]
second_env = mock_run.call_args_list[1].kwargs["env"]
assert "GH_TOKEN" not in first_env
assert "GITHUB_TOKEN" not in first_env
assert second_env["GH_TOKEN"] == "env-gh-token"
assert second_env["GITHUB_TOKEN"] == "env-github-token"
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_run_gh_command_does_not_retry_after_clean_env_success(mock_run, monkeypatch):
monkeypatch.setenv("GITHUB_TOKEN", "env-token")
mock_run.return_value = _completed_process(returncode=0)
result = run_gh_command(["gh", "api", "repos/apache/airflow"], capture_output=True)
assert result.returncode == 0
mock_run.assert_called_once()
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_run_gh_command_raises_when_check_true_and_no_env_token_to_retry(mock_run, monkeypatch):
monkeypatch.delenv("GH_TOKEN", raising=False)
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
mock_run.return_value = _completed_process(returncode=1)
with pytest.raises(subprocess.CalledProcessError) as ctx:
run_gh_command(["gh", "api", "repos/apache/airflow"], capture_output=True, check=True)
assert ctx.value.returncode == 1
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_run_gh_command_raises_when_gh_is_missing(mock_run):
mock_run.side_effect = FileNotFoundError
with pytest.raises(FileNotFoundError):
run_gh_command(["gh", "api", "repos/apache/airflow"], capture_output=True)
@mock.patch("airflow_breeze.utils.github.subprocess.run")
def test_run_gh_command_skips_subprocess_in_dry_run(mock_run):
set_dry_run(True)
try:
result = run_gh_command(["gh", "workflow", "run", "docs.yml"], capture_output=True)
finally:
set_dry_run(False)
assert result.returncode == 0
mock_run.assert_not_called()