import argparse import contextlib import io import os import tempfile from unittest import mock import pytest import gitlab.base from gitlab import cli from gitlab.exceptions import GitlabError from gitlab.mixins import CreateMixin, UpdateMixin from gitlab.types import RequiredOptional from gitlab.v4 import cli as v4_cli @pytest.mark.parametrize( "gitlab_resource,expected_class", [ ("class", "Class"), ("test-class", "TestClass"), ("test-longer-class", "TestLongerClass"), ("current-user-gpg-key", "CurrentUserGPGKey"), ("user-gpg-key", "UserGPGKey"), ("ldap-group", "LDAPGroup"), ], ) def test_gitlab_resource_to_cls(gitlab_resource, expected_class): def _namespace(): pass ExpectedClass = type(expected_class, (gitlab.base.RESTObject,), {}) _namespace.__dict__[expected_class] = ExpectedClass assert cli.gitlab_resource_to_cls(gitlab_resource, _namespace) == ExpectedClass @pytest.mark.parametrize( "class_name,expected_gitlab_resource", [ ("Class", "class"), ("TestClass", "test-class"), ("TestUPPERCASEClass", "test-uppercase-class"), ("UPPERCASETestClass", "uppercase-test-class"), ("CurrentUserGPGKey", "current-user-gpg-key"), ("UserGPGKey", "user-gpg-key"), ("LDAPGroup", "ldap-group"), ], ) def test_cls_to_gitlab_resource(class_name, expected_gitlab_resource): TestClass = type(class_name, (), {}) assert cli.cls_to_gitlab_resource(TestClass) == expected_gitlab_resource @pytest.mark.parametrize( "message,error,expected", [("foobar", None, "foobar\n"), ("foo", GitlabError("bar"), "foo (bar)\n")], ) def test_die(message, error, expected): fl = io.StringIO() with contextlib.redirect_stderr(fl): with pytest.raises(SystemExit) as test: cli.die(message, error) assert fl.getvalue() == expected assert test.value.code == 1 def test_parse_value(): ret = cli._parse_value("foobar") assert ret == "foobar" ret = cli._parse_value(True) assert ret is True ret = cli._parse_value(1) assert ret == 1 ret = cli._parse_value(None) assert ret is None fd, temp_path = tempfile.mkstemp() os.write(fd, b"content") os.close(fd) ret = cli._parse_value(f"@{temp_path}") assert ret == "content" os.unlink(temp_path) fl = io.StringIO() with contextlib.redirect_stderr(fl): with pytest.raises(SystemExit) as exc: cli._parse_value("@/thisfileprobablydoesntexist") assert fl.getvalue().startswith( "FileNotFoundError: [Errno 2] No such file or directory:" ) assert exc.value.code == 1 def test_base_parser(): parser = cli._get_base_parser() args = parser.parse_args(["-v", "-g", "gl_id", "-c", "foo.cfg", "-c", "bar.cfg"]) assert args.verbose assert args.gitlab == "gl_id" assert args.config_file == ["foo.cfg", "bar.cfg"] assert args.ssl_verify is None def test_no_ssl_verify(): parser = cli._get_base_parser() args = parser.parse_args(["--no-ssl-verify"]) assert args.ssl_verify is False def test_v4_parse_args(): parser = cli._get_parser() args = parser.parse_args(["project", "list"]) assert args.gitlab_resource == "project" assert args.resource_action == "list" def test_v4_parser(): parser = cli._get_parser() subparsers = next( action for action in parser._actions if isinstance(action, argparse._SubParsersAction) ) assert subparsers is not None assert "project" in subparsers.choices user_subparsers = next( action for action in subparsers.choices["project"]._actions if isinstance(action, argparse._SubParsersAction) ) assert user_subparsers is not None assert "list" in user_subparsers.choices assert "get" in user_subparsers.choices assert "delete" in user_subparsers.choices assert "update" in user_subparsers.choices assert "create" in user_subparsers.choices assert "archive" in user_subparsers.choices assert "unarchive" in user_subparsers.choices actions = user_subparsers.choices["create"]._option_string_actions assert not actions["--description"].required user_subparsers = next( action for action in subparsers.choices["group"]._actions if isinstance(action, argparse._SubParsersAction) ) actions = user_subparsers.choices["create"]._option_string_actions assert actions["--name"].required def test_extend_parser(): class ExceptionArgParser(argparse.ArgumentParser): def error(self, message): "Raise error instead of exiting on invalid arguments, to make testing easier" raise ValueError(message) class Fake(gitlab.base.RESTObject): _id_attr = None class FakeManager(CreateMixin, UpdateMixin, gitlab.base.RESTManager): _path = "/fake" _obj_cls = Fake _create_attrs = RequiredOptional( required=("create",), optional=("opt_create",), exclusive=("create_a", "create_b"), ) _update_attrs = RequiredOptional( required=("update",), optional=("opt_update",), exclusive=("update_a", "update_b"), ) parser = ExceptionArgParser() with mock.patch.dict( "gitlab.v4.objects.__dict__", {"FakeManager": FakeManager}, clear=True ): v4_cli.extend_parser(parser) assert parser.parse_args(["fake", "create", "--create", "1"]) assert parser.parse_args(["fake", "create", "--create", "1", "--opt-create", "1"]) assert parser.parse_args(["fake", "create", "--create", "1", "--create-a", "1"]) assert parser.parse_args(["fake", "create", "--create", "1", "--create-b", "1"]) with pytest.raises(ValueError): # missing required "create" parser.parse_args(["fake", "create", "--opt_create", "1"]) with pytest.raises(ValueError): # both exclusive options parser.parse_args( ["fake", "create", "--create", "1", "--create-a", "1", "--create-b", "1"] ) assert parser.parse_args(["fake", "update", "--update", "1"]) assert parser.parse_args(["fake", "update", "--update", "1", "--opt-update", "1"]) assert parser.parse_args(["fake", "update", "--update", "1", "--update-a", "1"]) assert parser.parse_args(["fake", "update", "--update", "1", "--update-b", "1"]) with pytest.raises(ValueError): # missing required "update" parser.parse_args(["fake", "update", "--opt_update", "1"]) with pytest.raises(ValueError): # both exclusive options parser.parse_args( ["fake", "update", "--update", "1", "--update-a", "1", "--update-b", "1"] ) def test_legacy_display_without_fields_warns(fake_object_no_id): printer = v4_cli.LegacyPrinter() with mock.patch("builtins.print") as mocked: printer.display(fake_object_no_id, obj=fake_object_no_id) assert "No default fields to show" in mocked.call_args.args[0] def test_legacy_display_with_long_repr_truncates(fake_object_long_repr): printer = v4_cli.LegacyPrinter() with mock.patch("builtins.print") as mocked: printer.display(fake_object_long_repr, obj=fake_object_long_repr) assert len(mocked.call_args.args[0]) < 80