import io import sys from pathlib import Path from textwrap import dedent from unittest import mock import pytest import gitlab from gitlab import config, const custom_user_agent = "my-package/1.0.0" valid_config = """[global] default = one ssl_verify = true timeout = 2 [one] url = http://one.url private_token = ABCDEF [two] url = https://two.url private_token = GHIJKL ssl_verify = false timeout = 10 [three] url = https://three.url private_token = MNOPQR ssl_verify = /path/to/CA/bundle.crt per_page = 50 [four] url = https://four.url oauth_token = STUV """ ssl_verify_str_config = """[global] default = one ssl_verify = /etc/ssl/certs/ca-certificates.crt [one] url = http://one.url private_token = ABCDEF """ custom_user_agent_config = f"""[global] default = one user_agent = {custom_user_agent} [one] url = http://one.url private_token = ABCDEF """ no_default_config = """[global] [there] url = http://there.url private_token = ABCDEF """ invalid_data_config = """[global] [one] url = http://one.url [two] private_token = ABCDEF [three] meh = hem [four] url = http://four.url private_token = ABCDEF per_page = 200 [invalid-api-version] url = http://invalid-api-version.url private_token = ABCDEF api_version = 1 """ @pytest.fixture(autouse=True) def default_files(monkeypatch): """Overrides mocked default files from conftest.py as we have our own mocks here.""" monkeypatch.setattr(gitlab.config, "_DEFAULT_FILES", config._DEFAULT_FILES) def global_retry_transient_errors(value: bool) -> str: return f"""[global] default = one retry_transient_errors={value} [one] url = http://one.url private_token = ABCDEF""" def global_and_gitlab_retry_transient_errors( global_value: bool, gitlab_value: bool ) -> str: return f"""[global] default = one retry_transient_errors={global_value} [one] url = http://one.url private_token = ABCDEF retry_transient_errors={gitlab_value}""" def _mock_nonexistent_file(*args, **kwargs): raise OSError def _mock_existent_file(path, *args, **kwargs): return path def test_env_config_missing_file_raises(monkeypatch): monkeypatch.setenv("PYTHON_GITLAB_CFG", "/some/path") with pytest.raises(config.GitlabConfigMissingError): config._get_config_files() def test_env_config_not_defined_does_not_raise(monkeypatch): with monkeypatch.context() as m: m.setattr(config, "_DEFAULT_FILES", []) assert config._get_config_files() == [] def test_default_config(monkeypatch): with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_nonexistent_file) cp = config.GitlabConfigParser() assert cp.gitlab_id is None assert cp.http_username is None assert cp.http_password is None assert cp.job_token is None assert cp.oauth_token is None assert cp.private_token is None assert cp.api_version == "4" assert cp.order_by is None assert cp.pagination is None assert cp.per_page is None assert cp.retry_transient_errors is False assert cp.ssl_verify is True assert cp.timeout == 60 assert cp.url is None assert cp.user_agent == const.USER_AGENT @mock.patch("builtins.open") def test_invalid_id(m_open, monkeypatch): fd = io.StringIO(no_default_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) config.GitlabConfigParser("there") with pytest.raises(config.GitlabIDError): config.GitlabConfigParser() fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="not_there") @mock.patch("builtins.open") def test_invalid_data(m_open, monkeypatch): fd = io.StringIO(invalid_data_config) fd.close = mock.Mock(return_value=None, side_effect=lambda: fd.seek(0)) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) config.GitlabConfigParser("one") config.GitlabConfigParser("one") with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="two") with pytest.raises(config.GitlabDataError): config.GitlabConfigParser(gitlab_id="three") with pytest.raises(config.GitlabDataError) as e: config.GitlabConfigParser("four") assert str(e.value) == "Unsupported per_page number: 200" with pytest.raises(config.GitlabDataError) as e: config.GitlabConfigParser("invalid-api-version") assert str(e.value) == "Unsupported API version: 1" @mock.patch("builtins.open") def test_valid_data(m_open, monkeypatch): fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser() assert "one" == cp.gitlab_id assert "http://one.url" == cp.url assert "ABCDEF" == cp.private_token assert cp.oauth_token is None assert 2 == cp.timeout assert cp.ssl_verify is True assert cp.per_page is None fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser(gitlab_id="two") assert "two" == cp.gitlab_id assert "https://two.url" == cp.url assert "GHIJKL" == cp.private_token assert cp.oauth_token is None assert 10 == cp.timeout assert cp.ssl_verify is False fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser(gitlab_id="three") assert "three" == cp.gitlab_id assert "https://three.url" == cp.url assert "MNOPQR" == cp.private_token assert cp.oauth_token is None assert 2 == cp.timeout assert "/path/to/CA/bundle.crt" == cp.ssl_verify assert 50 == cp.per_page fd = io.StringIO(valid_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser(gitlab_id="four") assert "four" == cp.gitlab_id assert "https://four.url" == cp.url assert cp.private_token is None assert "STUV" == cp.oauth_token assert 2 == cp.timeout assert cp.ssl_verify is True @mock.patch("builtins.open") def test_ssl_verify_as_str(m_open, monkeypatch): fd = io.StringIO(ssl_verify_str_config) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser() assert cp.ssl_verify == "/etc/ssl/certs/ca-certificates.crt" @mock.patch("builtins.open") @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_data_from_helper(m_open, monkeypatch, tmp_path): helper = tmp_path / "helper.sh" helper.write_text( dedent( """\ #!/bin/sh echo "secret" """ ), encoding="utf-8", ) helper.chmod(0o755) fd = io.StringIO( dedent( f"""\ [global] default = helper [helper] url = https://helper.url oauth_token = helper: {helper} """ ) ) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser(gitlab_id="helper") assert "helper" == cp.gitlab_id assert "https://helper.url" == cp.url assert cp.private_token is None assert "secret" == cp.oauth_token @mock.patch("builtins.open") @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_from_helper_subprocess_error_raises_error(m_open, monkeypatch): # using false here to force a non-zero return code fd = io.StringIO( dedent( """\ [global] default = helper [helper] url = https://helper.url oauth_token = helper: false """ ) ) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) with pytest.raises(config.GitlabConfigHelperError) as e: config.GitlabConfigParser(gitlab_id="helper") assert "Failed to read oauth_token value from helper" in str(e.value) @mock.patch("builtins.open") @pytest.mark.parametrize( "config_string,expected_agent", [ (valid_config, gitlab.const.USER_AGENT), (custom_user_agent_config, custom_user_agent), ], ) def test_config_user_agent(m_open, monkeypatch, config_string, expected_agent): fd = io.StringIO(config_string) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser() assert cp.user_agent == expected_agent @mock.patch("builtins.open") @pytest.mark.parametrize( "config_string,expected", [ pytest.param(valid_config, False, id="default_value"), pytest.param( global_retry_transient_errors(True), True, id="global_config_true" ), pytest.param( global_retry_transient_errors(False), False, id="global_config_false" ), pytest.param( global_and_gitlab_retry_transient_errors(False, True), True, id="gitlab_overrides_global_true", ), pytest.param( global_and_gitlab_retry_transient_errors(True, False), False, id="gitlab_overrides_global_false", ), pytest.param( global_and_gitlab_retry_transient_errors(True, True), True, id="gitlab_equals_global_true", ), pytest.param( global_and_gitlab_retry_transient_errors(False, False), False, id="gitlab_equals_global_false", ), ], ) def test_config_retry_transient_errors_when_global_config_is_set( m_open, monkeypatch, config_string, expected ): fd = io.StringIO(config_string) fd.close = mock.Mock(return_value=None) m_open.return_value = fd with monkeypatch.context() as m: m.setattr(Path, "resolve", _mock_existent_file) cp = config.GitlabConfigParser() assert cp.retry_transient_errors == expected