summaryrefslogtreecommitdiff
path: root/tools/functional
diff options
context:
space:
mode:
authorNejc Habjan <nejc.habjan@siemens.com>2020-09-05 00:15:04 +0200
committerNejc Habjan <nejc.habjan@siemens.com>2020-09-15 22:05:48 +0200
commit79489c775141c4ddd1f7aecae90dae8061d541fe (patch)
tree7c8f45e232049e0cad13af57e05b7ccbd8c6bb82 /tools/functional
parent0d89a6e61bd4ae244c1545463272ef830d72dda9 (diff)
downloadgitlab-79489c775141c4ddd1f7aecae90dae8061d541fe.tar.gz
test(env): replace custom scripts with pytest and docker-compose
Diffstat (limited to 'tools/functional')
-rw-r--r--tools/functional/api/test_gitlab.py8
-rw-r--r--tools/functional/cli/conftest.py4
-rw-r--r--tools/functional/conftest.py99
-rwxr-xr-xtools/functional/ee-test.py159
-rw-r--r--tools/functional/fixtures/avatar.pngbin0 -> 592 bytes
-rw-r--r--tools/functional/fixtures/docker-compose.yml31
-rw-r--r--tools/functional/fixtures/set_token.rb9
-rw-r--r--tools/functional/python_test_v4.py1141
8 files changed, 1445 insertions, 6 deletions
diff --git a/tools/functional/api/test_gitlab.py b/tools/functional/api/test_gitlab.py
new file mode 100644
index 0000000..5cf3418
--- /dev/null
+++ b/tools/functional/api/test_gitlab.py
@@ -0,0 +1,8 @@
+"""
+Temporary module to run legacy tests as a single pytest test case
+as they're all plain asserts at module level.
+"""
+
+
+def test_api_v4(gl):
+ from tools.functional import python_test_v4
diff --git a/tools/functional/cli/conftest.py b/tools/functional/cli/conftest.py
index 13c3096..ba94dcb 100644
--- a/tools/functional/cli/conftest.py
+++ b/tools/functional/cli/conftest.py
@@ -2,7 +2,7 @@ import pytest
@pytest.fixture
-def gitlab_cli(script_runner, CONFIG):
+def gitlab_cli(script_runner, gitlab_config):
"""Wrapper fixture to help make test cases less verbose."""
def _gitlab_cli(subcommands):
@@ -10,7 +10,7 @@ def gitlab_cli(script_runner, CONFIG):
Return a script_runner.run method that takes a default gitlab
command, and subcommands passed as arguments inside test cases.
"""
- command = ["gitlab", "--config-file", CONFIG]
+ command = ["gitlab", "--config-file", gitlab_config]
for subcommand in subcommands:
# ensure we get strings (e.g from IDs)
diff --git a/tools/functional/conftest.py b/tools/functional/conftest.py
index e60fa39..ec0d08b 100644
--- a/tools/functional/conftest.py
+++ b/tools/functional/conftest.py
@@ -1,6 +1,8 @@
+import time
import tempfile
from pathlib import Path
from random import randint
+from subprocess import check_output
import pytest
@@ -8,6 +10,7 @@ import gitlab
TEMP_DIR = tempfile.gettempdir()
+TEST_DIR = Path(__file__).resolve().parent
def random_id():
@@ -20,15 +23,103 @@ def random_id():
return randint(9, 9999)
+def reset_gitlab(gl):
+ # previously tools/reset_gitlab.py
+ for project in gl.projects.list():
+ project.delete()
+ for group in gl.groups.list():
+ group.delete()
+ for variable in gl.variables.list():
+ variable.delete()
+ for user in gl.users.list():
+ if user.username != "root":
+ user.delete()
+
+
+def set_token(container):
+ set_token_rb = TEST_DIR / "fixtures" / "set_token.rb"
+
+ with open(set_token_rb, "r") as f:
+ set_token_command = f.read().strip()
+
+ rails_command = [
+ "docker",
+ "exec",
+ container,
+ "gitlab-rails",
+ "runner",
+ set_token_command,
+ ]
+ output = check_output(rails_command).decode().strip()
+
+ return output
+
+
@pytest.fixture(scope="session")
-def CONFIG():
- return Path(TEMP_DIR) / "python-gitlab.cfg"
+def docker_compose_file():
+ return TEST_DIR / "fixtures" / "docker-compose.yml"
@pytest.fixture(scope="session")
-def gl(CONFIG):
+def check_is_alive(request):
+ """
+ Return a healthcheck function fixture for the GitLab container spinup.
+ """
+ start = time.time()
+
+ # Temporary manager to disable capsys in a session-scoped fixture
+ # so people know it takes a while for GitLab to spin up
+ # https://github.com/pytest-dev/pytest/issues/2704
+ capmanager = request.config.pluginmanager.getplugin("capturemanager")
+
+ def _check(container):
+ delay = int(time.time() - start)
+
+ with capmanager.global_and_fixture_disabled():
+ print(f"Waiting for GitLab to reconfigure.. (~{delay}s)")
+
+ logs = ["docker", "logs", container]
+ output = check_output(logs).decode()
+
+ return "gitlab Reconfigured!" in output
+
+ return _check
+
+
+@pytest.fixture(scope="session")
+def gitlab_config(check_is_alive, docker_ip, docker_services):
+ config_file = Path(TEMP_DIR) / "python-gitlab.cfg"
+ port = docker_services.port_for("gitlab", 80)
+
+ docker_services.wait_until_responsive(
+ timeout=180, pause=5, check=lambda: check_is_alive("gitlab-test")
+ )
+
+ token = set_token("gitlab-test")
+
+ config = f"""[global]
+default = local
+timeout = 60
+
+[local]
+url = http://{docker_ip}:{port}
+private_token = {token}
+api_version = 4"""
+
+ with open(config_file, "w") as f:
+ f.write(config)
+
+ return config_file
+
+
+@pytest.fixture(scope="session")
+def gl(gitlab_config):
"""Helper instance to make fixtures and asserts directly via the API."""
- return gitlab.Gitlab.from_config("local", [CONFIG])
+
+ instance = gitlab.Gitlab.from_config("local", [gitlab_config])
+ reset_gitlab(instance)
+
+ return instance
@pytest.fixture(scope="module")
diff --git a/tools/functional/ee-test.py b/tools/functional/ee-test.py
new file mode 100755
index 0000000..3f75655
--- /dev/null
+++ b/tools/functional/ee-test.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+
+import gitlab
+
+
+P1 = "root/project1"
+P2 = "root/project2"
+MR_P1 = 1
+I_P1 = 1
+I_P2 = 1
+EPIC_ISSUES = [4, 5]
+G1 = "group1"
+LDAP_CN = "app1"
+LDAP_PROVIDER = "ldapmain"
+
+
+def start_log(message):
+ print("Testing %s... " % message, end="")
+
+
+def end_log():
+ print("OK")
+
+
+gl = gitlab.Gitlab.from_config("ee")
+project1 = gl.projects.get(P1)
+project2 = gl.projects.get(P2)
+issue_p1 = project1.issues.get(I_P1)
+issue_p2 = project2.issues.get(I_P2)
+group1 = gl.groups.get(G1)
+mr = project1.mergerequests.get(1)
+
+start_log("MR approvals")
+approval = project1.approvals.get()
+v = approval.reset_approvals_on_push
+approval.reset_approvals_on_push = not v
+approval.save()
+approval = project1.approvals.get()
+assert v != approval.reset_approvals_on_push
+project1.approvals.set_approvers(1, [1], [])
+approval = project1.approvals.get()
+assert approval.approvers[0]["user"]["id"] == 1
+
+approval = mr.approvals.get()
+approval.approvals_required = 2
+approval.save()
+approval = mr.approvals.get()
+assert approval.approvals_required == 2
+approval.approvals_required = 3
+approval.save()
+approval = mr.approvals.get()
+assert approval.approvals_required == 3
+mr.approvals.set_approvers(1, [1], [])
+approval = mr.approvals.get()
+assert approval.approvers[0]["user"]["id"] == 1
+
+ars = project1.approvalrules.list(all=True)
+assert len(ars) == 0
+project1.approvalrules.create(
+ {"name": "approval-rule", "approvals_required": 1, "group_ids": [group1.id]}
+)
+ars = project1.approvalrules.list(all=True)
+assert len(ars) == 1
+assert ars[0].approvals_required == 2
+ars[0].save()
+ars = project1.approvalrules.list(all=True)
+assert len(ars) == 1
+assert ars[0].approvals_required == 2
+ars[0].delete()
+ars = project1.approvalrules.list(all=True)
+assert len(ars) == 0
+end_log()
+
+start_log("geo nodes")
+# very basic tests because we only have 1 node...
+nodes = gl.geonodes.list()
+status = gl.geonodes.status()
+end_log()
+
+start_log("issue links")
+# bit of cleanup just in case
+for link in issue_p1.links.list():
+ issue_p1.links.delete(link.issue_link_id)
+
+src, dst = issue_p1.links.create({"target_project_id": P2, "target_issue_iid": I_P2})
+links = issue_p1.links.list()
+link_id = links[0].issue_link_id
+issue_p1.links.delete(link_id)
+end_log()
+
+start_log("LDAP links")
+# bit of cleanup just in case
+if hasattr(group1, "ldap_group_links"):
+ for link in group1.ldap_group_links:
+ group1.delete_ldap_group_link(link["cn"], link["provider"])
+assert gl.ldapgroups.list()
+group1.add_ldap_group_link(LDAP_CN, 30, LDAP_PROVIDER)
+group1.ldap_sync()
+group1.delete_ldap_group_link(LDAP_CN)
+end_log()
+
+start_log("boards")
+# bit of cleanup just in case
+for board in project1.boards.list():
+ if board.name == "testboard":
+ board.delete()
+board = project1.boards.create({"name": "testboard"})
+board = project1.boards.get(board.id)
+project1.boards.delete(board.id)
+
+for board in group1.boards.list():
+ if board.name == "testboard":
+ board.delete()
+board = group1.boards.create({"name": "testboard"})
+board = group1.boards.get(board.id)
+group1.boards.delete(board.id)
+end_log()
+
+start_log("push rules")
+pr = project1.pushrules.get()
+if pr:
+ pr.delete()
+pr = project1.pushrules.create({"deny_delete_tag": True})
+pr.deny_delete_tag = False
+pr.save()
+pr = project1.pushrules.get()
+assert pr is not None
+assert pr.deny_delete_tag == False
+pr.delete()
+end_log()
+
+start_log("license")
+l = gl.get_license()
+assert "user_limit" in l
+try:
+ gl.set_license("dummykey")
+except Exception as e:
+ assert "The license key is invalid." in e.error_message
+end_log()
+
+start_log("epics")
+epic = group1.epics.create({"title": "Test epic"})
+epic.title = "Fixed title"
+epic.labels = ["label1", "label2"]
+epic.save()
+epic = group1.epics.get(epic.iid)
+assert epic.title == "Fixed title"
+assert len(group1.epics.list())
+
+# issues
+assert not epic.issues.list()
+for i in EPIC_ISSUES:
+ epic.issues.create({"issue_id": i})
+assert len(EPIC_ISSUES) == len(epic.issues.list())
+for ei in epic.issues.list():
+ ei.delete()
+
+epic.delete()
+end_log()
diff --git a/tools/functional/fixtures/avatar.png b/tools/functional/fixtures/avatar.png
new file mode 100644
index 0000000..a3a767c
--- /dev/null
+++ b/tools/functional/fixtures/avatar.png
Binary files differ
diff --git a/tools/functional/fixtures/docker-compose.yml b/tools/functional/fixtures/docker-compose.yml
new file mode 100644
index 0000000..5dd9d90
--- /dev/null
+++ b/tools/functional/fixtures/docker-compose.yml
@@ -0,0 +1,31 @@
+version: '3'
+services:
+ gitlab:
+ image: 'gitlab/gitlab-ce:latest'
+ container_name: 'gitlab-test'
+ hostname: 'gitlab.test'
+ privileged: true # Just in case https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/1350
+ environment:
+ GITLAB_OMNIBUS_CONFIG: |
+ external_url 'http://gitlab.test'
+ gitlab_rails['initial_root_password'] = '5iveL!fe'
+ gitlab_rails['initial_shared_runners_registration_token'] = 'sTPNtWLEuSrHzoHP8oCU'
+ registry['enable'] = false
+ nginx['redirect_http_to_https'] = false
+ nginx['listen_port'] = 80
+ nginx['listen_https'] = false
+ pages_external_url 'http://pages.gitlab.lxd'
+ gitlab_pages['enable'] = true
+ gitlab_pages['inplace_chroot'] = true
+ prometheus['enable'] = false
+ alertmanager['enable'] = false
+ node_exporter['enable'] = false
+ redis_exporter['enable'] = false
+ postgres_exporter['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ gitlab_exporter['enable'] = false
+ grafana['enable'] = false
+ letsencrypt['enable'] = false
+ ports:
+ - '8080:80'
+ - '2222:22'
diff --git a/tools/functional/fixtures/set_token.rb b/tools/functional/fixtures/set_token.rb
new file mode 100644
index 0000000..735dcd5
--- /dev/null
+++ b/tools/functional/fixtures/set_token.rb
@@ -0,0 +1,9 @@
+# https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#programmatically-creating-a-personal-access-token
+
+user = User.find_by_username('root')
+
+token = user.personal_access_tokens.create(scopes: [:api, :sudo], name: 'default');
+token.set_token('python-gitlab-token');
+token.save!
+
+puts token.token
diff --git a/tools/functional/python_test_v4.py b/tools/functional/python_test_v4.py
new file mode 100644
index 0000000..6c90fe6
--- /dev/null
+++ b/tools/functional/python_test_v4.py
@@ -0,0 +1,1141 @@
+import base64
+import tempfile
+import time
+from pathlib import Path
+
+import requests
+
+import gitlab
+
+LOGIN = "root"
+PASSWORD = "5iveL!fe"
+
+SSH_KEY = (
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih"
+ "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n"
+ "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l"
+ "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI"
+ "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh"
+ "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar"
+)
+DEPLOY_KEY = (
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG"
+ "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI"
+ "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6"
+ "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu"
+ "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv"
+ "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc"
+ "vn bar@foo"
+)
+
+GPG_KEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFn5mzYBCADH6SDVPAp1zh/hxmTi0QplkOfExBACpuY6OhzNdIg+8/528b3g
+Y5YFR6T/HLv/PmeHskUj21end1C0PNG2T9dTx+2Vlh9ISsSG1kyF9T5fvMR3bE0x
+Dl6S489CXZrjPTS9SHk1kF+7dwjUxLJyxF9hPiSihFefDFu3NeOtG/u8vbC1mewQ
+ZyAYue+mqtqcCIFFoBz7wHKMWjIVSJSyTkXExu4OzpVvy3l2EikbvavI3qNz84b+
+Mgkv/kiBlNoCy3CVuPk99RYKZ3lX1vVtqQ0OgNGQvb4DjcpyjmbKyibuZwhDjIOh
+au6d1OyEbayTntd+dQ4j9EMSnEvm/0MJ4eXPABEBAAG0G0dpdGxhYlRlc3QxIDxm
+YWtlQGZha2UudGxkPokBNwQTAQgAIQUCWfmbNgIbAwULCQgHAgYVCAkKCwIEFgID
+AQIeAQIXgAAKCRBgxELHf8f3hF3yB/wNJlWPKY65UsB4Lo0hs1OxdxCDqXogSi0u
+6crDEIiyOte62pNZKzWy8TJcGZvznRTZ7t8hXgKFLz3PRMcl+vAiRC6quIDUj+2V
+eYfwaItd1lUfzvdCaC7Venf4TQ74f5vvNg/zoGwE6eRoSbjlLv9nqsxeA0rUBUQL
+LYikWhVMP3TrlfgfduYvh6mfgh57BDLJ9kJVpyfxxx9YLKZbaas9sPa6LgBtR555
+JziUxHmbEv8XCsUU8uoFeP1pImbNBplqE3wzJwzOMSmmch7iZzrAwfN7N2j3Wj0H
+B5kQddJ9dmB4BbU0IXGhWczvdpxboI2wdY8a1JypxOdePoph/43iuQENBFn5mzYB
+CADnTPY0Zf3d9zLjBNgIb3yDl94uOcKCq0twNmyjMhHzGqw+UMe9BScy34GL94Al
+xFRQoaL+7P8hGsnsNku29A/VDZivcI+uxTx4WQ7OLcn7V0bnHV4d76iky2ufbUt/
+GofthjDs1SonePO2N09sS4V4uK0d5N4BfCzzXgvg8etCLxNmC9BGt7AaKUUzKBO4
+2QvNNaC2C/8XEnOgNWYvR36ylAXAmo0sGFXUsBCTiq1fugS9pwtaS2JmaVpZZ3YT
+pMZlS0+SjC5BZYFqSmKCsA58oBRzCxQz57nR4h5VEflgD+Hy0HdW0UHETwz83E6/
+U0LL6YyvhwFr6KPq5GxinSvfABEBAAGJAR8EGAEIAAkFAln5mzYCGwwACgkQYMRC
+x3/H94SJgwgAlKQb10/xcL/epdDkR7vbiei7huGLBpRDb/L5fM8B5W77Qi8Xmuqj
+cCu1j99ZCA5hs/vwVn8j8iLSBGMC5gxcuaar/wtmiaEvT9fO/h6q4opG7NcuiJ8H
+wRj8ccJmRssNqDD913PLz7T40Ts62blhrEAlJozGVG/q7T3RAZcskOUHKeHfc2RI
+YzGsC/I9d7k6uxAv1L9Nm5F2HaAQDzhkdd16nKkGaPGR35cT1JLInkfl5cdm7ldN
+nxs4TLO3kZjUTgWKdhpgRNF5hwaz51ZjpebaRf/ZqRuNyX4lIRolDxzOn/+O1o8L
+qG2ZdhHHmSK2LaQLFiSprUkikStNU9BqSQ==
+=5OGa
+-----END PGP PUBLIC KEY BLOCK-----"""
+AVATAR_PATH = Path(__file__).resolve().parent / "fixtures" / "avatar.png"
+TEMP_DIR = Path(tempfile.gettempdir())
+
+# token authentication from config file
+gl = gitlab.Gitlab.from_config(config_files=[TEMP_DIR / "python-gitlab.cfg"])
+gl.auth()
+assert isinstance(gl.user, gitlab.v4.objects.CurrentUser)
+
+# markdown
+html = gl.markdown("foo")
+assert "foo" in html
+
+success, errors = gl.lint("Invalid")
+assert success is False
+assert errors
+
+# sidekiq
+out = gl.sidekiq.queue_metrics()
+assert isinstance(out, dict)
+assert "pages" in out["queues"]
+out = gl.sidekiq.process_metrics()
+assert isinstance(out, dict)
+assert "hostname" in out["processes"][0]
+out = gl.sidekiq.job_stats()
+assert isinstance(out, dict)
+assert "processed" in out["jobs"]
+out = gl.sidekiq.compound_metrics()
+assert isinstance(out, dict)
+assert "jobs" in out
+assert "processes" in out
+assert "queues" in out
+
+# settings
+settings = gl.settings.get()
+settings.default_projects_limit = 42
+settings.save()
+settings = gl.settings.get()
+assert settings.default_projects_limit == 42
+
+# users
+new_user = gl.users.create(
+ {
+ "email": "foo@bar.com",
+ "username": "foo",
+ "name": "foo",
+ "password": "foo_password",
+ "avatar": open(AVATAR_PATH, "rb"),
+ }
+)
+avatar_url = new_user.avatar_url.replace("gitlab.test", "localhost:8080")
+uploaded_avatar = requests.get(avatar_url).content
+assert uploaded_avatar == open(AVATAR_PATH, "rb").read()
+users_list = gl.users.list()
+for user in users_list:
+ if user.username == "foo":
+ break
+assert new_user.username == user.username
+assert new_user.email == user.email
+
+new_user.block()
+new_user.unblock()
+
+# user projects list
+assert len(new_user.projects.list()) == 0
+
+# events list
+new_user.events.list()
+
+foobar_user = gl.users.create(
+ {
+ "email": "foobar@example.com",
+ "username": "foobar",
+ "name": "Foo Bar",
+ "password": "foobar_password",
+ }
+)
+
+assert gl.users.list(search="foobar")[0].id == foobar_user.id
+expected = [new_user, foobar_user]
+actual = list(gl.users.list(search="foo"))
+assert len(expected) == len(actual)
+assert len(gl.users.list(search="asdf")) == 0
+foobar_user.bio = "This is the user bio"
+foobar_user.save()
+
+# GPG keys
+gkey = new_user.gpgkeys.create({"key": GPG_KEY})
+assert len(new_user.gpgkeys.list()) == 1
+# Seems broken on the gitlab side
+# gkey = new_user.gpgkeys.get(gkey.id)
+gkey.delete()
+assert len(new_user.gpgkeys.list()) == 0
+
+# SSH keys
+key = new_user.keys.create({"title": "testkey", "key": SSH_KEY})
+assert len(new_user.keys.list()) == 1
+key.delete()
+assert len(new_user.keys.list()) == 0
+
+# emails
+email = new_user.emails.create({"email": "foo2@bar.com"})
+assert len(new_user.emails.list()) == 1
+email.delete()
+assert len(new_user.emails.list()) == 0
+
+# custom attributes
+attrs = new_user.customattributes.list()
+assert len(attrs) == 0
+attr = new_user.customattributes.set("key", "value1")
+assert len(gl.users.list(custom_attributes={"key": "value1"})) == 1
+assert attr.key == "key"
+assert attr.value == "value1"
+assert len(new_user.customattributes.list()) == 1
+attr = new_user.customattributes.set("key", "value2")
+attr = new_user.customattributes.get("key")
+assert attr.value == "value2"
+assert len(new_user.customattributes.list()) == 1
+attr.delete()
+assert len(new_user.customattributes.list()) == 0
+
+# impersonation tokens
+user_token = new_user.impersonationtokens.create(
+ {"name": "token1", "scopes": ["api", "read_user"]}
+)
+l = new_user.impersonationtokens.list(state="active")
+assert len(l) == 1
+user_token.delete()
+l = new_user.impersonationtokens.list(state="active")
+assert len(l) == 0
+l = new_user.impersonationtokens.list(state="inactive")
+assert len(l) == 1
+
+new_user.delete()
+foobar_user.delete()
+assert len(gl.users.list()) == 3 + len(
+ [u for u in gl.users.list() if u.username == "ghost"]
+)
+
+# current user mail
+mail = gl.user.emails.create({"email": "current@user.com"})
+assert len(gl.user.emails.list()) == 1
+mail.delete()
+assert len(gl.user.emails.list()) == 0
+
+# current user GPG keys
+gkey = gl.user.gpgkeys.create({"key": GPG_KEY})
+assert len(gl.user.gpgkeys.list()) == 1
+# Seems broken on the gitlab side
+gkey = gl.user.gpgkeys.get(gkey.id)
+gkey.delete()
+assert len(gl.user.gpgkeys.list()) == 0
+
+# current user key
+key = gl.user.keys.create({"title": "testkey", "key": SSH_KEY})
+assert len(gl.user.keys.list()) == 1
+key.delete()
+assert len(gl.user.keys.list()) == 0
+
+# templates
+assert gl.dockerfiles.list()
+dockerfile = gl.dockerfiles.get("Node")
+assert dockerfile.content is not None
+
+assert gl.gitignores.list()
+gitignore = gl.gitignores.get("Node")
+assert gitignore.content is not None
+
+assert gl.gitlabciymls.list()
+gitlabciyml = gl.gitlabciymls.get("Nodejs")
+assert gitlabciyml.content is not None
+
+assert gl.licenses.list()
+license = gl.licenses.get(
+ "bsd-2-clause", project="mytestproject", fullname="mytestfullname"
+)
+assert "mytestfullname" in license.content
+
+# groups
+user1 = gl.users.create(
+ {
+ "email": "user1@test.com",
+ "username": "user1",
+ "name": "user1",
+ "password": "user1_pass",
+ }
+)
+user2 = gl.users.create(
+ {
+ "email": "user2@test.com",
+ "username": "user2",
+ "name": "user2",
+ "password": "user2_pass",
+ }
+)
+group1 = gl.groups.create({"name": "group1", "path": "group1"})
+group2 = gl.groups.create({"name": "group2", "path": "group2"})
+
+p_id = gl.groups.list(search="group2")[0].id
+group3 = gl.groups.create({"name": "group3", "path": "group3", "parent_id": p_id})
+group4 = gl.groups.create({"name": "group4", "path": "group4"})
+
+assert len(gl.groups.list()) == 4
+assert len(gl.groups.list(search="oup1")) == 1
+assert group3.parent_id == p_id
+assert group2.subgroups.list()[0].id == group3.id
+
+group1.members.create({"access_level": gitlab.const.OWNER_ACCESS, "user_id": user1.id})
+group1.members.create({"access_level": gitlab.const.GUEST_ACCESS, "user_id": user2.id})
+
+group2.members.create({"access_level": gitlab.const.OWNER_ACCESS, "user_id": user2.id})
+
+group4.share(group1.id, gitlab.const.DEVELOPER_ACCESS)
+group4.share(group2.id, gitlab.const.MAINTAINER_ACCESS)
+# Reload group4 to have updated shared_with_groups
+group4 = gl.groups.get(group4.id)
+assert len(group4.shared_with_groups) == 2
+group4.unshare(group1.id)
+# Reload group4 to have updated shared_with_groups
+group4 = gl.groups.get(group4.id)
+assert len(group4.shared_with_groups) == 1
+
+# User memberships (admin only)
+memberships1 = user1.memberships.list()
+assert len(memberships1) == 1
+
+memberships2 = user2.memberships.list()
+assert len(memberships2) == 2
+
+membership = memberships1[0]
+assert membership.source_type == "Namespace"
+assert membership.access_level == gitlab.const.OWNER_ACCESS
+
+project_memberships = user1.memberships.list(type="Project")
+assert len(project_memberships) == 0
+
+group_memberships = user1.memberships.list(type="Namespace")
+assert len(group_memberships) == 1
+
+try:
+ membership = user1.memberships.list(type="Invalid")
+except gitlab.GitlabListError as e:
+ error_message = e.error_message
+assert error_message == "type does not have a valid value"
+
+try:
+ user1.memberships.list(sudo=user1.name)
+except gitlab.GitlabListError as e:
+ error_message = e.error_message
+assert error_message == "403 Forbidden"
+
+# Administrator belongs to the groups
+assert len(group1.members.list()) == 3
+assert len(group2.members.list()) == 2
+
+group1.members.delete(user1.id)
+assert len(group1.members.list()) == 2
+assert len(group1.members.all())
+member = group1.members.get(user2.id)
+member.access_level = gitlab.const.OWNER_ACCESS
+member.save()
+member = group1.members.get(user2.id)
+assert member.access_level == gitlab.const.OWNER_ACCESS
+
+group2.members.delete(gl.user.id)
+
+# group custom attributes
+attrs = group2.customattributes.list()
+assert len(attrs) == 0
+attr = group2.customattributes.set("key", "value1")
+assert len(gl.groups.list(custom_attributes={"key": "value1"})) == 1
+assert attr.key == "key"
+assert attr.value == "value1"
+assert len(group2.customattributes.list()) == 1
+attr = group2.customattributes.set("key", "value2")
+attr = group2.customattributes.get("key")
+assert attr.value == "value2"
+assert len(group2.customattributes.list()) == 1
+attr.delete()
+assert len(group2.customattributes.list()) == 0
+
+# group notification settings
+settings = group2.notificationsettings.get()
+settings.level = "disabled"
+settings.save()
+settings = group2.notificationsettings.get()
+assert settings.level == "disabled"
+
+# group badges
+badge_image = "http://example.com"
+badge_link = "http://example/img.svg"
+badge = group2.badges.create({"link_url": badge_link, "image_url": badge_image})
+assert len(group2.badges.list()) == 1
+badge.image_url = "http://another.example.com"
+badge.save()
+badge = group2.badges.get(badge.id)
+assert badge.image_url == "http://another.example.com"
+badge.delete()
+assert len(group2.badges.list()) == 0
+
+# group milestones
+gm1 = group1.milestones.create({"title": "groupmilestone1"})
+assert len(group1.milestones.list()) == 1
+gm1.due_date = "2020-01-01T00:00:00Z"
+gm1.save()
+gm1.state_event = "close"
+gm1.save()
+gm1 = group1.milestones.get(gm1.id)
+assert gm1.state == "closed"
+assert len(gm1.issues()) == 0
+assert len(gm1.merge_requests()) == 0
+
+
+# group labels
+# group1.labels.create({"name": "foo", "description": "bar", "color": "#112233"})
+# g_l = group1.labels.get("foo")
+# assert g_l.description == "bar"
+# g_l.description = "baz"
+# g_l.save()
+# g_l = group1.labels.get("foo")
+# assert g_l.description == "baz"
+# assert len(group1.labels.list()) == 1
+# g_l.delete()
+# assert len(group1.labels.list()) == 0
+
+
+# group import/export
+export = group1.exports.create()
+assert export.message == "202 Accepted"
+
+# We cannot check for export_status with group export API
+time.sleep(10)
+
+import_archive = TEMP_DIR / "gitlab-group-export.tgz"
+import_path = "imported_group"
+import_name = "Imported Group"
+
+with open(import_archive, "wb") as f:
+ export.download(streamed=True, action=f.write)
+
+with open(import_archive, "rb") as f:
+ output = gl.groups.import_group(f, import_path, import_name)
+assert output["message"] == "202 Accepted"
+
+# We cannot check for returned ID with group import API
+time.sleep(10)
+group_import = gl.groups.get(import_path)
+
+assert group_import.path == import_path
+assert group_import.name == import_name
+
+
+# hooks
+hook = gl.hooks.create({"url": "http://whatever.com"})
+assert len(gl.hooks.list()) == 1
+hook.delete()
+assert len(gl.hooks.list()) == 0
+
+# projects
+admin_project = gl.projects.create({"name": "admin_project"})
+gr1_project = gl.projects.create({"name": "gr1_project", "namespace_id": group1.id})
+gr2_project = gl.projects.create({"name": "gr2_project", "namespace_id": group2.id})
+sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user1.name)
+
+assert len(gl.projects.list(owned=True)) == 3
+assert len(gl.projects.list(search="admin")) == 1
+assert len(gl.projects.list(as_list=False)) == 4
+
+# test pagination
+l1 = gl.projects.list(per_page=1, page=1)
+l2 = gl.projects.list(per_page=1, page=2)
+assert len(l1) == 1
+assert len(l2) == 1
+assert l1[0].id != l2[0].id
+
+# group custom attributes
+attrs = admin_project.customattributes.list()
+assert len(attrs) == 0
+attr = admin_project.customattributes.set("key", "value1")
+assert len(gl.projects.list(custom_attributes={"key": "value1"})) == 1
+assert attr.key == "key"
+assert attr.value == "value1"
+assert len(admin_project.customattributes.list()) == 1
+attr = admin_project.customattributes.set("key", "value2")
+attr = admin_project.customattributes.get("key")
+assert attr.value == "value2"
+assert len(admin_project.customattributes.list()) == 1
+attr.delete()
+assert len(admin_project.customattributes.list()) == 0
+
+# project pages domains
+domain = admin_project.pagesdomains.create({"domain": "foo.domain.com"})
+assert len(admin_project.pagesdomains.list()) == 1
+assert len(gl.pagesdomains.list()) == 1
+domain = admin_project.pagesdomains.get("foo.domain.com")
+assert domain.domain == "foo.domain.com"
+domain.delete()
+assert len(admin_project.pagesdomains.list()) == 0
+
+# project content (files)
+admin_project.files.create(
+ {
+ "file_path": "README",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "Initial commit",
+ }
+)
+readme = admin_project.files.get(file_path="README", ref="master")
+readme.content = base64.b64encode(b"Improved README").decode()
+time.sleep(2)
+readme.save(branch="master", commit_message="new commit")
+readme.delete(commit_message="Removing README", branch="master")
+
+admin_project.files.create(
+ {
+ "file_path": "README.rst",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "New commit",
+ }
+)
+readme = admin_project.files.get(file_path="README.rst", ref="master")
+# The first decode() is the ProjectFile method, the second one is the bytes
+# object method
+assert readme.decode().decode() == "Initial content"
+
+blame = admin_project.files.blame(file_path="README.rst", ref="master")
+
+data = {
+ "branch": "master",
+ "commit_message": "blah blah blah",
+ "actions": [{"action": "create", "file_path": "blah", "content": "blah"}],
+}
+admin_project.commits.create(data)
+assert "@@" in admin_project.commits.list()[0].diff()[0]["diff"]
+
+# commit status
+commit = admin_project.commits.list()[0]
+# size = len(commit.statuses.list())
+# status = commit.statuses.create({"state": "success", "sha": commit.id})
+# assert len(commit.statuses.list()) == size + 1
+
+# assert commit.refs()
+# assert commit.merge_requests()
+
+# commit signature (for unsigned commits)
+# TODO: reasonable tests for signed commits?
+try:
+ signature = commit.signature()
+except gitlab.GitlabGetError as e:
+ error_message = e.error_message
+assert error_message == "404 Signature Not Found"
+
+# commit comment
+commit.comments.create({"note": "This is a commit comment"})
+# assert len(commit.comments.list()) == 1
+
+# commit discussion
+count = len(commit.discussions.list())
+discussion = commit.discussions.create({"body": "Discussion body"})
+# assert len(commit.discussions.list()) == (count + 1)
+d_note = discussion.notes.create({"body": "first note"})
+d_note_from_get = discussion.notes.get(d_note.id)
+d_note_from_get.body = "updated body"
+d_note_from_get.save()
+discussion = commit.discussions.get(discussion.id)
+# assert discussion.attributes["notes"][-1]["body"] == "updated body"
+d_note_from_get.delete()
+discussion = commit.discussions.get(discussion.id)
+# assert len(discussion.attributes["notes"]) == 1
+
+# Revert commit
+revert_commit = commit.revert(branch="master")
+
+expected_message = 'Revert "{}"\n\nThis reverts commit {}'.format(
+ commit.message, commit.id
+)
+assert revert_commit["message"] == expected_message
+
+try:
+ commit.revert(branch="master")
+ # Only here to really ensure expected error without a full test framework
+ raise AssertionError("Two revert attempts should raise GitlabRevertError")
+except gitlab.GitlabRevertError:
+ pass
+
+# housekeeping
+admin_project.housekeeping()
+
+# repository
+tree = admin_project.repository_tree()
+assert len(tree) != 0
+assert tree[0]["name"] == "README.rst"
+blob_id = tree[0]["id"]
+blob = admin_project.repository_raw_blob(blob_id)
+assert blob.decode() == "Initial content"
+archive1 = admin_project.repository_archive()
+archive2 = admin_project.repository_archive("master")
+assert archive1 == archive2
+snapshot = admin_project.snapshot()
+
+# project file uploads
+filename = "test.txt"
+file_contents = "testing contents"
+uploaded_file = admin_project.upload(filename, file_contents)
+assert uploaded_file["alt"] == filename
+assert uploaded_file["url"].startswith("/uploads/")
+assert uploaded_file["url"].endswith("/" + filename)
+assert uploaded_file["markdown"] == "[{}]({})".format(
+ uploaded_file["alt"], uploaded_file["url"]
+)
+
+# environments
+admin_project.environments.create(
+ {"name": "env1", "external_url": "http://fake.env/whatever"}
+)
+envs = admin_project.environments.list()
+assert len(envs) == 1
+env = envs[0]
+env.external_url = "http://new.env/whatever"
+env.save()
+env = admin_project.environments.list()[0]
+assert env.external_url == "http://new.env/whatever"
+env.stop()
+env.delete()
+assert len(admin_project.environments.list()) == 0
+
+# Project clusters
+admin_project.clusters.create(
+ {
+ "name": "cluster1",
+ "platform_kubernetes_attributes": {
+ "api_url": "http://url",
+ "token": "tokenval",
+ },
+ }
+)
+clusters = admin_project.clusters.list()
+assert len(clusters) == 1
+cluster = clusters[0]
+cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"}
+cluster.save()
+cluster = admin_project.clusters.list()[0]
+assert cluster.platform_kubernetes["api_url"] == "http://newurl"
+cluster.delete()
+assert len(admin_project.clusters.list()) == 0
+
+# Group clusters
+group1.clusters.create(
+ {
+ "name": "cluster1",
+ "platform_kubernetes_attributes": {
+ "api_url": "http://url",
+ "token": "tokenval",
+ },
+ }
+)
+clusters = group1.clusters.list()
+assert len(clusters) == 1
+cluster = clusters[0]
+cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"}
+cluster.save()
+cluster = group1.clusters.list()[0]
+assert cluster.platform_kubernetes["api_url"] == "http://newurl"
+cluster.delete()
+assert len(group1.clusters.list()) == 0
+
+# project events
+admin_project.events.list()
+
+# forks
+fork = admin_project.forks.create({"namespace": user1.username})
+p = gl.projects.get(fork.id)
+assert p.forked_from_project["id"] == admin_project.id
+
+forks = admin_project.forks.list()
+assert fork.id in map(lambda p: p.id, forks)
+
+# project hooks
+hook = admin_project.hooks.create({"url": "http://hook.url"})
+assert len(admin_project.hooks.list()) == 1
+hook.note_events = True
+hook.save()
+hook = admin_project.hooks.get(hook.id)
+assert hook.note_events is True
+hook.delete()
+
+# deploy keys
+deploy_key = admin_project.keys.create({"title": "foo@bar", "key": DEPLOY_KEY})
+project_keys = list(admin_project.keys.list())
+assert len(project_keys) == 1
+
+sudo_project.keys.enable(deploy_key.id)
+assert len(sudo_project.keys.list()) == 1
+sudo_project.keys.delete(deploy_key.id)
+assert len(sudo_project.keys.list()) == 0
+
+# deploy tokens
+deploy_token = admin_project.deploytokens.create(
+ {
+ "name": "foo",
+ "username": "bar",
+ "expires_at": "2022-01-01",
+ "scopes": ["read_registry"],
+ }
+)
+assert len(admin_project.deploytokens.list()) == 1
+assert gl.deploytokens.list() == admin_project.deploytokens.list()
+
+assert admin_project.deploytokens.list()[0].name == "foo"
+assert admin_project.deploytokens.list()[0].expires_at == "2022-01-01T00:00:00.000Z"
+assert admin_project.deploytokens.list()[0].scopes == ["read_registry"]
+# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/211963 is fixed
+# assert admin_project.deploytokens.list()[0].username == "bar"
+deploy_token.delete()
+assert len(admin_project.deploytokens.list()) == 0
+# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/212523 is fixed
+# assert len(gl.deploytokens.list()) == 0
+
+
+deploy_token_group = gl.groups.create(
+ {"name": "deploy_token_group", "path": "deploy_token_group"}
+)
+
+# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/211878 is fixed
+# deploy_token = group_deploy_token.deploytokens.create(
+# {
+# "name": "foo",
+# "scopes": ["read_registry"],
+# }
+# )
+
+# Remove once https://gitlab.com/gitlab-org/gitlab/-/issues/211878 is fixed
+deploy_token = deploy_token_group.deploytokens.create(
+ {
+ "name": "foo",
+ "username": "",
+ "expires_at": "",
+ "scopes": ["read_repository"],
+ }
+)
+
+assert len(deploy_token_group.deploytokens.list()) == 1
+# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/212523 is fixed
+# assert gl.deploytokens.list() == deploy_token_group.deploytokens.list()
+deploy_token.delete()
+assert len(deploy_token_group.deploytokens.list()) == 0
+# Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/212523 is fixed
+# assert len(gl.deploytokens.list()) == 0
+
+deploy_token_group.delete()
+
+# labels
+# label1 = admin_project.labels.create({"name": "label1", "color": "#778899"})
+# label1 = admin_project.labels.list()[0]
+# assert len(admin_project.labels.list()) == 1
+# label1.new_name = "label1updated"
+# label1.save()
+# assert label1.name == "label1updated"
+# label1.subscribe()
+# assert label1.subscribed == True
+# label1.unsubscribe()
+# assert label1.subscribed == False
+# label1.delete()
+
+# milestones
+m1 = admin_project.milestones.create({"title": "milestone1"})
+assert len(admin_project.milestones.list()) == 1
+m1.due_date = "2020-01-01T00:00:00Z"
+m1.save()
+m1.state_event = "close"
+m1.save()
+m1 = admin_project.milestones.get(m1.id)
+assert m1.state == "closed"
+assert len(m1.issues()) == 0
+assert len(m1.merge_requests()) == 0
+
+# issues
+issue1 = admin_project.issues.create({"title": "my issue 1", "milestone_id": m1.id})
+issue2 = admin_project.issues.create({"title": "my issue 2"})
+issue3 = admin_project.issues.create({"title": "my issue 3"})
+assert len(admin_project.issues.list()) == 3
+issue3.state_event = "close"
+issue3.save()
+assert len(admin_project.issues.list(state="closed")) == 1
+assert len(admin_project.issues.list(state="opened")) == 2
+assert len(admin_project.issues.list(milestone="milestone1")) == 1
+assert m1.issues().next().title == "my issue 1"
+size = len(issue1.notes.list())
+note = issue1.notes.create({"body": "This is an issue note"})
+assert len(issue1.notes.list()) == size + 1
+emoji = note.awardemojis.create({"name": "tractor"})
+assert len(note.awardemojis.list()) == 1
+emoji.delete()
+assert len(note.awardemojis.list()) == 0
+note.delete()
+assert len(issue1.notes.list()) == size
+assert isinstance(issue1.user_agent_detail(), dict)
+
+assert issue1.user_agent_detail()["user_agent"]
+assert issue1.participants()
+assert type(issue1.closed_by()) == list
+assert type(issue1.related_merge_requests()) == list
+
+# issue labels
+label2 = admin_project.labels.create({"name": "label2", "color": "#aabbcc"})
+issue1.labels = ["label2"]
+issue1.save()
+
+assert issue1 in admin_project.issues.list(labels=["label2"])
+assert issue1 in admin_project.issues.list(labels="label2")
+assert issue1 in admin_project.issues.list(labels="Any")
+assert issue1 not in admin_project.issues.list(labels="None")
+
+# issue events
+events = issue1.resourcelabelevents.list()
+assert events
+event = issue1.resourcelabelevents.get(events[0].id)
+assert event
+
+# issue milestones
+milestones = issue1.resourcemilestoneevents.list()
+assert milestones
+milestone = issue1.resourcemilestoneevents.get(milestones[0].id)
+assert milestone
+
+size = len(issue1.discussions.list())
+discussion = issue1.discussions.create({"body": "Discussion body"})
+assert len(issue1.discussions.list()) == size + 1
+d_note = discussion.notes.create({"body": "first note"})
+d_note_from_get = discussion.notes.get(d_note.id)
+d_note_from_get.body = "updated body"
+d_note_from_get.save()
+discussion = issue1.discussions.get(discussion.id)
+assert discussion.attributes["notes"][-1]["body"] == "updated body"
+d_note_from_get.delete()
+discussion = issue1.discussions.get(discussion.id)
+assert len(discussion.attributes["notes"]) == 1
+
+# tags
+tag1 = admin_project.tags.create({"tag_name": "v1.0", "ref": "master"})
+assert len(admin_project.tags.list()) == 1
+tag1.set_release_description("Description 1")
+tag1.set_release_description("Description 2")
+assert tag1.release["description"] == "Description 2"
+tag1.delete()
+
+# project snippet
+admin_project.snippets_enabled = True
+admin_project.save()
+snippet = admin_project.snippets.create(
+ {
+ "title": "snip1",
+ "file_name": "foo.py",
+ "content": "initial content",
+ "visibility": gitlab.v4.objects.VISIBILITY_PRIVATE,
+ }
+)
+
+assert snippet.user_agent_detail()["user_agent"]
+
+size = len(snippet.discussions.list())
+discussion = snippet.discussions.create({"body": "Discussion body"})
+assert len(snippet.discussions.list()) == size + 1
+d_note = discussion.notes.create({"body": "first note"})
+d_note_from_get = discussion.notes.get(d_note.id)
+d_note_from_get.body = "updated body"
+d_note_from_get.save()
+discussion = snippet.discussions.get(discussion.id)
+assert discussion.attributes["notes"][-1]["body"] == "updated body"
+d_note_from_get.delete()
+discussion = snippet.discussions.get(discussion.id)
+assert len(discussion.attributes["notes"]) == 1
+
+snippet.file_name = "bar.py"
+snippet.save()
+snippet = admin_project.snippets.get(snippet.id)
+# TO BE RE-ENABLED AFTER 13.1
+# assert snippet.content().decode() == "initial content"
+assert snippet.file_name == "bar.py"
+size = len(admin_project.snippets.list())
+snippet.delete()
+assert len(admin_project.snippets.list()) == (size - 1)
+
+# triggers
+tr1 = admin_project.triggers.create({"description": "trigger1"})
+assert len(admin_project.triggers.list()) == 1
+tr1.delete()
+
+
+# branches and merges
+to_merge = admin_project.branches.create({"branch": "branch1", "ref": "master"})
+admin_project.files.create(
+ {
+ "file_path": "README2.rst",
+ "branch": "branch1",
+ "content": "Initial content",
+ "commit_message": "New commit in new branch",
+ }
+)
+mr = admin_project.mergerequests.create(
+ {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"}
+)
+
+# discussion
+size = len(mr.discussions.list())
+discussion = mr.discussions.create({"body": "Discussion body"})
+assert len(mr.discussions.list()) == size + 1
+d_note = discussion.notes.create({"body": "first note"})
+d_note_from_get = discussion.notes.get(d_note.id)
+d_note_from_get.body = "updated body"
+d_note_from_get.save()
+discussion = mr.discussions.get(discussion.id)
+assert discussion.attributes["notes"][-1]["body"] == "updated body"
+d_note_from_get.delete()
+discussion = mr.discussions.get(discussion.id)
+assert len(discussion.attributes["notes"]) == 1
+
+# mr labels and events
+mr.labels = ["label2"]
+mr.save()
+events = mr.resourcelabelevents.list()
+assert events
+event = mr.resourcelabelevents.get(events[0].id)
+assert event
+
+# mr milestone events
+mr.milestone_id = m1.id
+mr.save()
+milestones = mr.resourcemilestoneevents.list()
+assert milestones
+milestone = mr.resourcemilestoneevents.get(milestones[0].id)
+assert milestone
+
+# rebasing
+assert mr.rebase()
+
+# basic testing: only make sure that the methods exist
+mr.commits()
+mr.changes()
+assert mr.participants()
+
+mr.merge()
+admin_project.branches.delete("branch1")
+
+try:
+ mr.merge()
+except gitlab.GitlabMRClosedError:
+ pass
+
+# protected branches
+p_b = admin_project.protectedbranches.create({"name": "*-stable"})
+assert p_b.name == "*-stable"
+p_b = admin_project.protectedbranches.get("*-stable")
+# master is protected by default when a branch has been created
+assert len(admin_project.protectedbranches.list()) == 2
+admin_project.protectedbranches.delete("master")
+p_b.delete()
+assert len(admin_project.protectedbranches.list()) == 0
+
+# stars
+admin_project.star()
+assert admin_project.star_count == 1
+admin_project.unstar()
+assert admin_project.star_count == 0
+
+# project boards
+# boards = admin_project.boards.list()
+# assert(len(boards))
+# board = boards[0]
+# lists = board.lists.list()
+# begin_size = len(lists)
+# last_list = lists[-1]
+# last_list.position = 0
+# last_list.save()
+# last_list.delete()
+# lists = board.lists.list()
+# assert(len(lists) == begin_size - 1)
+
+# project badges
+badge_image = "http://example.com"
+badge_link = "http://example/img.svg"
+badge = admin_project.badges.create({"link_url": badge_link, "image_url": badge_image})
+assert len(admin_project.badges.list()) == 1
+badge.image_url = "http://another.example.com"
+badge.save()
+badge = admin_project.badges.get(badge.id)
+assert badge.image_url == "http://another.example.com"
+badge.delete()
+assert len(admin_project.badges.list()) == 0
+
+# project wiki
+wiki_content = "Wiki page content"
+wp = admin_project.wikis.create({"title": "wikipage", "content": wiki_content})
+assert len(admin_project.wikis.list()) == 1
+wp = admin_project.wikis.get(wp.slug)
+assert wp.content == wiki_content
+# update and delete seem broken
+# wp.content = 'new content'
+# wp.save()
+# wp.delete()
+# assert(len(admin_project.wikis.list()) == 0)
+
+# namespaces
+ns = gl.namespaces.list(all=True)
+assert len(ns) != 0
+ns = gl.namespaces.list(search="root", all=True)[0]
+assert ns.kind == "user"
+
+# features
+# Disabled as this fails with GitLab 11.11
+# feat = gl.features.set("foo", 30)
+# assert feat.name == "foo"
+# assert len(gl.features.list()) == 1
+# feat.delete()
+# assert len(gl.features.list()) == 0
+
+# broadcast messages
+msg = gl.broadcastmessages.create({"message": "this is the message"})
+msg.color = "#444444"
+msg.save()
+msg_id = msg.id
+msg = gl.broadcastmessages.list(all=True)[0]
+assert msg.color == "#444444"
+msg = gl.broadcastmessages.get(msg_id)
+assert msg.color == "#444444"
+msg.delete()
+assert len(gl.broadcastmessages.list()) == 0
+
+# notification settings
+settings = gl.notificationsettings.get()
+settings.level = gitlab.NOTIFICATION_LEVEL_WATCH
+settings.save()
+settings = gl.notificationsettings.get()
+assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH
+
+# services
+service = admin_project.services.get("asana")
+service.api_key = "whatever"
+service.save()
+service = admin_project.services.get("asana")
+assert service.active == True
+service.delete()
+service = admin_project.services.get("asana")
+assert service.active == False
+
+# snippets
+snippets = gl.snippets.list(all=True)
+assert len(snippets) == 0
+snippet = gl.snippets.create(
+ {"title": "snippet1", "file_name": "snippet1.py", "content": "import gitlab"}
+)
+snippet = gl.snippets.get(snippet.id)
+snippet.title = "updated_title"
+snippet.save()
+snippet = gl.snippets.get(snippet.id)
+assert snippet.title == "updated_title"
+content = snippet.content()
+assert content.decode() == "import gitlab"
+
+assert snippet.user_agent_detail()["user_agent"]
+
+snippet.delete()
+snippets = gl.snippets.list(all=True)
+assert len(snippets) == 0
+
+# user activities
+gl.user_activities.list(query_parameters={"from": "2019-01-01"})
+
+# events
+gl.events.list()
+
+# rate limit
+settings = gl.settings.get()
+settings.throttle_authenticated_api_enabled = True
+settings.throttle_authenticated_api_requests_per_period = 1
+settings.throttle_authenticated_api_period_in_seconds = 3
+settings.save()
+projects = list()
+for i in range(0, 20):
+ projects.append(gl.projects.create({"name": str(i) + "ok"}))
+
+error_message = None
+for i in range(20, 40):
+ try:
+ projects.append(
+ gl.projects.create({"name": str(i) + "shouldfail"}, obey_rate_limit=False)
+ )
+ except gitlab.GitlabCreateError as e:
+ error_message = e.error_message
+ break
+assert "Retry later" in error_message
+settings.throttle_authenticated_api_enabled = False
+settings.save()
+[current_project.delete() for current_project in projects]
+
+# project import/export
+ex = admin_project.exports.create()
+ex.refresh()
+count = 0
+while ex.export_status != "finished":
+ time.sleep(1)
+ ex.refresh()
+ count += 1
+ if count == 10:
+ raise Exception("Project export taking too much time")
+with open(TEMP_DIR / "gitlab-export.tgz", "wb") as f:
+ ex.download(streamed=True, action=f.write)
+
+output = gl.projects.import_project(
+ open(TEMP_DIR / "gitlab-export.tgz", "rb"),
+ "imported_project",
+ name="Imported Project",
+)
+project_import = gl.projects.get(output["id"], lazy=True).imports.get()
+
+assert project_import.path == "imported_project"
+assert project_import.name == "Imported Project"
+
+count = 0
+while project_import.import_status != "finished":
+ time.sleep(1)
+ project_import.refresh()
+ count += 1
+ if count == 10:
+ raise Exception("Project import taking too much time")
+
+# project releases
+release_test_project = gl.projects.create(
+ {"name": "release-test-project", "initialize_with_readme": True}
+)
+release_name = "Demo Release"
+release_tag_name = "v1.2.3"
+release_description = "release notes go here"
+release_test_project.releases.create(
+ {
+ "name": release_name,
+ "tag_name": release_tag_name,
+ "description": release_description,
+ "ref": "master",
+ }
+)
+assert len(release_test_project.releases.list()) == 1
+
+# get single release
+retrieved_project = release_test_project.releases.get(release_tag_name)
+assert retrieved_project.name == release_name
+assert retrieved_project.tag_name == release_tag_name
+assert retrieved_project.description == release_description
+
+# delete release
+release_test_project.releases.delete(release_tag_name)
+assert len(release_test_project.releases.list()) == 0
+release_test_project.delete()
+
+# project remote mirrors
+mirror_url = "http://gitlab.test/root/mirror.git"
+
+# create remote mirror
+mirror = admin_project.remote_mirrors.create({"url": mirror_url})
+assert mirror.url == mirror_url
+
+# update remote mirror
+mirror.enabled = True
+mirror.save()
+
+# list remote mirrors
+mirror = admin_project.remote_mirrors.list()[0]
+assert isinstance(mirror, gitlab.v4.objects.ProjectRemoteMirror)
+assert mirror.url == mirror_url
+assert mirror.enabled is True
+
+# status
+message = "Test"
+emoji = "thumbsup"
+status = gl.user.status.get()
+status.message = message
+status.emoji = emoji
+status.save()
+new_status = gl.user.status.get()
+assert new_status.message == message
+assert new_status.emoji == emoji