1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
|
import argparse
import contextlib
import io
import os
import sys
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:
_id_attr = None
class FakeManager(gitlab.base.RESTManager, CreateMixin, UpdateMixin):
_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"]
)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="added in 3.8")
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]
@pytest.mark.skipif(sys.version_info < (3, 8), reason="added in 3.8")
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
|