summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/functional/api/test_clusters.py46
-rw-r--r--tests/functional/api/test_current_user.py42
-rw-r--r--tests/functional/api/test_deploy_keys.py12
-rw-r--r--tests/functional/api/test_deploy_tokens.py36
-rw-r--r--tests/functional/api/test_gitlab.py183
-rw-r--r--tests/functional/api/test_groups.py195
-rw-r--r--tests/functional/api/test_import_export.py66
-rw-r--r--tests/functional/api/test_issues.py93
-rw-r--r--tests/functional/api/test_merge_requests.py165
-rw-r--r--tests/functional/api/test_packages.py13
-rw-r--r--tests/functional/api/test_projects.py267
-rw-r--r--tests/functional/api/test_releases.py36
-rw-r--r--tests/functional/api/test_repository.py126
-rw-r--r--tests/functional/api/test_snippets.py74
-rw-r--r--tests/functional/api/test_users.py170
-rw-r--r--tests/functional/api/test_variables.py48
-rw-r--r--tests/functional/cli/conftest.py21
-rw-r--r--tests/functional/cli/test_cli_artifacts.py51
-rw-r--r--tests/functional/cli/test_cli_packages.py12
-rw-r--r--tests/functional/cli/test_cli_v4.py715
-rw-r--r--tests/functional/cli/test_cli_variables.py19
-rw-r--r--tests/functional/conftest.py462
-rwxr-xr-xtests/functional/ee-test.py158
-rw-r--r--tests/functional/fixtures/.env2
-rw-r--r--tests/functional/fixtures/avatar.pngbin0 -> 592 bytes
-rw-r--r--tests/functional/fixtures/docker-compose.yml46
-rw-r--r--tests/functional/fixtures/set_token.rb9
27 files changed, 3067 insertions, 0 deletions
diff --git a/tests/functional/api/test_clusters.py b/tests/functional/api/test_clusters.py
new file mode 100644
index 0000000..8930aad
--- /dev/null
+++ b/tests/functional/api/test_clusters.py
@@ -0,0 +1,46 @@
+def test_project_clusters(project):
+ project.clusters.create(
+ {
+ "name": "cluster1",
+ "platform_kubernetes_attributes": {
+ "api_url": "http://url",
+ "token": "tokenval",
+ },
+ }
+ )
+ clusters = project.clusters.list()
+ assert len(clusters) == 1
+
+ cluster = clusters[0]
+ cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"}
+ cluster.save()
+
+ cluster = project.clusters.list()[0]
+ assert cluster.platform_kubernetes["api_url"] == "http://newurl"
+
+ cluster.delete()
+ assert len(project.clusters.list()) == 0
+
+
+def test_group_clusters(group):
+ group.clusters.create(
+ {
+ "name": "cluster1",
+ "platform_kubernetes_attributes": {
+ "api_url": "http://url",
+ "token": "tokenval",
+ },
+ }
+ )
+ clusters = group.clusters.list()
+ assert len(clusters) == 1
+
+ cluster = clusters[0]
+ cluster.platform_kubernetes_attributes = {"api_url": "http://newurl"}
+ cluster.save()
+
+ cluster = group.clusters.list()[0]
+ assert cluster.platform_kubernetes["api_url"] == "http://newurl"
+
+ cluster.delete()
+ assert len(group.clusters.list()) == 0
diff --git a/tests/functional/api/test_current_user.py b/tests/functional/api/test_current_user.py
new file mode 100644
index 0000000..5802457
--- /dev/null
+++ b/tests/functional/api/test_current_user.py
@@ -0,0 +1,42 @@
+def test_current_user_email(gl):
+ gl.auth()
+ 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
+
+
+def test_current_user_gpg_keys(gl, GPG_KEY):
+ gl.auth()
+ 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
+
+
+def test_current_user_ssh_keys(gl, SSH_KEY):
+ gl.auth()
+ 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
+
+
+def test_current_user_status(gl):
+ gl.auth()
+ 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
diff --git a/tests/functional/api/test_deploy_keys.py b/tests/functional/api/test_deploy_keys.py
new file mode 100644
index 0000000..18828a2
--- /dev/null
+++ b/tests/functional/api/test_deploy_keys.py
@@ -0,0 +1,12 @@
+def test_project_deploy_keys(gl, project, DEPLOY_KEY):
+ deploy_key = project.keys.create({"title": "foo@bar", "key": DEPLOY_KEY})
+ project_keys = list(project.keys.list())
+ assert len(project_keys) == 1
+
+ project2 = gl.projects.create({"name": "deploy-key-project"})
+ project2.keys.enable(deploy_key.id)
+ assert len(project2.keys.list()) == 1
+
+ project2.keys.delete(deploy_key.id)
+ assert len(project2.keys.list()) == 0
+ project2.delete()
diff --git a/tests/functional/api/test_deploy_tokens.py b/tests/functional/api/test_deploy_tokens.py
new file mode 100644
index 0000000..efcf8b1
--- /dev/null
+++ b/tests/functional/api/test_deploy_tokens.py
@@ -0,0 +1,36 @@
+def test_project_deploy_tokens(gl, project):
+ deploy_token = project.deploytokens.create(
+ {
+ "name": "foo",
+ "username": "bar",
+ "expires_at": "2022-01-01",
+ "scopes": ["read_registry"],
+ }
+ )
+ assert len(project.deploytokens.list()) == 1
+ assert gl.deploytokens.list() == project.deploytokens.list()
+
+ assert project.deploytokens.list()[0].name == "foo"
+ assert project.deploytokens.list()[0].expires_at == "2022-01-01T00:00:00.000Z"
+ assert project.deploytokens.list()[0].scopes == ["read_registry"]
+ assert project.deploytokens.list()[0].username == "bar"
+
+ deploy_token.delete()
+ assert len(project.deploytokens.list()) == 0
+ assert len(gl.deploytokens.list()) == 0
+
+
+def test_group_deploy_tokens(gl, group):
+ deploy_token = group.deploytokens.create(
+ {
+ "name": "foo",
+ "scopes": ["read_registry"],
+ }
+ )
+
+ assert len(group.deploytokens.list()) == 1
+ assert gl.deploytokens.list() == group.deploytokens.list()
+
+ deploy_token.delete()
+ assert len(group.deploytokens.list()) == 0
+ assert len(gl.deploytokens.list()) == 0
diff --git a/tests/functional/api/test_gitlab.py b/tests/functional/api/test_gitlab.py
new file mode 100644
index 0000000..7a70a56
--- /dev/null
+++ b/tests/functional/api/test_gitlab.py
@@ -0,0 +1,183 @@
+import pytest
+
+import gitlab
+
+
+def test_auth_from_config(gl, temp_dir):
+ """Test token authentication from config file"""
+ test_gitlab = gitlab.Gitlab.from_config(
+ config_files=[temp_dir / "python-gitlab.cfg"]
+ )
+ test_gitlab.auth()
+ assert isinstance(test_gitlab.user, gitlab.v4.objects.CurrentUser)
+
+
+def test_broadcast_messages(gl):
+ 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
+
+
+def test_markdown(gl):
+ html = gl.markdown("foo")
+ assert "foo" in html
+
+
+def test_lint(gl):
+ success, errors = gl.lint("Invalid")
+ assert success is False
+ assert errors
+
+
+def test_sidekiq_queue_metrics(gl):
+ out = gl.sidekiq.queue_metrics()
+ assert isinstance(out, dict)
+ assert "pages" in out["queues"]
+
+
+def test_sidekiq_process_metrics(gl):
+ out = gl.sidekiq.process_metrics()
+ assert isinstance(out, dict)
+ assert "hostname" in out["processes"][0]
+
+
+def test_sidekiq_job_stats(gl):
+ out = gl.sidekiq.job_stats()
+ assert isinstance(out, dict)
+ assert "processed" in out["jobs"]
+
+
+def test_sidekiq_compound_metrics(gl):
+ out = gl.sidekiq.compound_metrics()
+ assert isinstance(out, dict)
+ assert "jobs" in out
+ assert "processes" in out
+ assert "queues" in out
+
+
+def test_gitlab_settings(gl):
+ settings = gl.settings.get()
+ settings.default_projects_limit = 42
+ settings.save()
+ settings = gl.settings.get()
+ assert settings.default_projects_limit == 42
+
+
+def test_template_dockerfile(gl):
+ assert gl.dockerfiles.list()
+
+ dockerfile = gl.dockerfiles.get("Node")
+ assert dockerfile.content is not None
+
+
+def test_template_gitignore(gl):
+ assert gl.gitignores.list()
+ gitignore = gl.gitignores.get("Node")
+ assert gitignore.content is not None
+
+
+def test_template_gitlabciyml(gl):
+ assert gl.gitlabciymls.list()
+ gitlabciyml = gl.gitlabciymls.get("Nodejs")
+ assert gitlabciyml.content is not None
+
+
+def test_template_license(gl):
+ assert gl.licenses.list()
+ license = gl.licenses.get(
+ "bsd-2-clause", project="mytestproject", fullname="mytestfullname"
+ )
+ assert "mytestfullname" in license.content
+
+
+def test_hooks(gl):
+ hook = gl.hooks.create({"url": "http://whatever.com"})
+ assert len(gl.hooks.list()) == 1
+
+ hook.delete()
+ assert len(gl.hooks.list()) == 0
+
+
+def test_namespaces(gl):
+ namespace = gl.namespaces.list(all=True)
+ assert namespace
+
+ namespace = gl.namespaces.list(search="root", all=True)[0]
+ assert namespace.kind == "user"
+
+
+def test_notification_settings(gl):
+ settings = gl.notificationsettings.get()
+ settings.level = gitlab.NOTIFICATION_LEVEL_WATCH
+ settings.save()
+
+ settings = gl.notificationsettings.get()
+ assert settings.level == gitlab.NOTIFICATION_LEVEL_WATCH
+
+
+def test_user_activities(gl):
+ activities = gl.user_activities.list(query_parameters={"from": "2019-01-01"})
+ assert isinstance(activities, list)
+
+
+def test_events(gl):
+ events = gl.events.list()
+ assert isinstance(events, list)
+
+
+@pytest.mark.skip
+def test_features(gl):
+ feat = gl.features.set("foo", 30)
+ assert feat.name == "foo"
+ assert len(gl.features.list()) == 1
+
+ feat.delete()
+ assert len(gl.features.list()) == 0
+
+
+def test_pagination(gl, project):
+ project2 = gl.projects.create({"name": "project-page-2"})
+
+ list1 = gl.projects.list(per_page=1, page=1)
+ list2 = gl.projects.list(per_page=1, page=2)
+ assert len(list1) == 1
+ assert len(list2) == 1
+ assert list1[0].id != list2[0].id
+
+ project2.delete()
+
+
+def test_rate_limits(gl):
+ 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"}))
+
+ with pytest.raises(gitlab.GitlabCreateError) as e:
+ for i in range(20, 40):
+ projects.append(
+ gl.projects.create(
+ {"name": str(i) + "shouldfail"}, obey_rate_limit=False
+ )
+ )
+
+ assert "Retry later" in str(e.value)
+
+ settings.throttle_authenticated_api_enabled = False
+ settings.save()
+ [project.delete() for project in projects]
diff --git a/tests/functional/api/test_groups.py b/tests/functional/api/test_groups.py
new file mode 100644
index 0000000..eae2d9b
--- /dev/null
+++ b/tests/functional/api/test_groups.py
@@ -0,0 +1,195 @@
+import pytest
+
+import gitlab
+
+
+def test_groups(gl):
+ # TODO: This one still needs lots of work
+ user = gl.users.create(
+ {
+ "email": "user@test.com",
+ "username": "user",
+ "name": "user",
+ "password": "user_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
+
+ filtered_groups = gl.groups.list(skip_groups=[group3.id, group4.id])
+ assert group3 not in filtered_groups
+ assert group3 not in filtered_groups
+
+ group1.members.create(
+ {"access_level": gitlab.const.OWNER_ACCESS, "user_id": user.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 = user.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 = user.memberships.list(type="Project")
+ assert len(project_memberships) == 0
+
+ group_memberships = user.memberships.list(type="Namespace")
+ assert len(group_memberships) == 1
+
+ with pytest.raises(gitlab.GitlabListError) as e:
+ membership = user.memberships.list(type="Invalid")
+ assert "type does not have a valid value" in str(e.value)
+
+ with pytest.raises(gitlab.GitlabListError) as e:
+ user.memberships.list(sudo=user.name)
+ assert "403 Forbidden" in str(e.value)
+
+ # Administrator belongs to the groups
+ assert len(group1.members.list()) == 3
+ assert len(group2.members.list()) == 2
+
+ group1.members.delete(user.id)
+ assert len(group1.members.list()) == 2
+ assert len(group1.members.all()) # Deprecated
+ assert len(group1.members_all.list())
+ 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)
+
+
+@pytest.mark.skip(reason="Commented out in legacy test")
+def test_group_labels(group):
+ group.labels.create({"name": "foo", "description": "bar", "color": "#112233"})
+ label = group.labels.get("foo")
+ assert label.description == "bar"
+
+ label.description = "baz"
+ label.save()
+ label = group.labels.get("foo")
+ assert label.description == "baz"
+ assert len(group.labels.list()) == 1
+
+ label.delete()
+ assert len(group.labels.list()) == 0
+
+
+def test_group_notification_settings(group):
+ settings = group.notificationsettings.get()
+ settings.level = "disabled"
+ settings.save()
+
+ settings = group.notificationsettings.get()
+ assert settings.level == "disabled"
+
+
+def test_group_badges(group):
+ badge_image = "http://example.com"
+ badge_link = "http://example/img.svg"
+ badge = group.badges.create({"link_url": badge_link, "image_url": badge_image})
+ assert len(group.badges.list()) == 1
+
+ badge.image_url = "http://another.example.com"
+ badge.save()
+
+ badge = group.badges.get(badge.id)
+ assert badge.image_url == "http://another.example.com"
+
+ badge.delete()
+ assert len(group.badges.list()) == 0
+
+
+def test_group_milestones(group):
+ milestone = group.milestones.create({"title": "groupmilestone1"})
+ assert len(group.milestones.list()) == 1
+
+ milestone.due_date = "2020-01-01T00:00:00Z"
+ milestone.save()
+ milestone.state_event = "close"
+ milestone.save()
+
+ milestone = group.milestones.get(milestone.id)
+ assert milestone.state == "closed"
+ assert len(milestone.issues()) == 0
+ assert len(milestone.merge_requests()) == 0
+
+
+def test_group_custom_attributes(gl, group):
+ attrs = group.customattributes.list()
+ assert len(attrs) == 0
+
+ attr = group.customattributes.set("key", "value1")
+ assert len(gl.groups.list(custom_attributes={"key": "value1"})) == 1
+ assert attr.key == "key"
+ assert attr.value == "value1"
+ assert len(group.customattributes.list()) == 1
+
+ attr = group.customattributes.set("key", "value2")
+ attr = group.customattributes.get("key")
+ assert attr.value == "value2"
+ assert len(group.customattributes.list()) == 1
+
+ attr.delete()
+ assert len(group.customattributes.list()) == 0
+
+
+def test_group_subgroups_projects(gl, user):
+ # TODO: fixture factories
+ group1 = gl.groups.list(search="group1")[0]
+ group2 = gl.groups.list(search="group2")[0]
+
+ group3 = gl.groups.create(
+ {"name": "subgroup1", "path": "subgroup1", "parent_id": group1.id}
+ )
+ group4 = gl.groups.create(
+ {"name": "subgroup2", "path": "subgroup2", "parent_id": group2.id}
+ )
+
+ gr1_project = gl.projects.create({"name": "gr1_project", "namespace_id": group1.id})
+ gr2_project = gl.projects.create({"name": "gr2_project", "namespace_id": group3.id})
+
+ assert group3.parent_id == group1.id
+ assert group4.parent_id == group2.id
+ assert gr1_project.namespace["id"] == group1.id
+ assert gr2_project.namespace["parent_id"] == group1.id
diff --git a/tests/functional/api/test_import_export.py b/tests/functional/api/test_import_export.py
new file mode 100644
index 0000000..d4bdd19
--- /dev/null
+++ b/tests/functional/api/test_import_export.py
@@ -0,0 +1,66 @@
+import time
+
+import gitlab
+
+
+def test_group_import_export(gl, group, temp_dir):
+ export = group.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
+
+
+def test_project_import_export(gl, project, temp_dir):
+ export = project.exports.create()
+ assert export.message == "202 Accepted"
+
+ export = project.exports.get()
+ assert isinstance(export, gitlab.v4.objects.ProjectExport)
+
+ count = 0
+ while export.export_status != "finished":
+ time.sleep(1)
+ export.refresh()
+ count += 1
+ if count == 15:
+ raise Exception("Project export taking too much time")
+
+ with open(temp_dir / "gitlab-export.tgz", "wb") as f:
+ export.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 == 15:
+ raise Exception("Project import taking too much time")
diff --git a/tests/functional/api/test_issues.py b/tests/functional/api/test_issues.py
new file mode 100644
index 0000000..f3a606b
--- /dev/null
+++ b/tests/functional/api/test_issues.py
@@ -0,0 +1,93 @@
+import gitlab
+
+
+def test_create_issue(project):
+ issue = project.issues.create({"title": "my issue 1"})
+ issue2 = project.issues.create({"title": "my issue 2"})
+ issue_ids = [issue.id for issue in project.issues.list()]
+ assert len(issue_ids) == 2
+
+ # Test 'iids' as a list
+ assert len(project.issues.list(iids=issue_ids)) == 2
+
+ issue2.state_event = "close"
+ issue2.save()
+ assert len(project.issues.list(state="closed")) == 1
+ assert len(project.issues.list(state="opened")) == 1
+
+ assert isinstance(issue.user_agent_detail(), dict)
+ assert issue.user_agent_detail()["user_agent"]
+ assert issue.participants()
+ assert type(issue.closed_by()) == list
+ assert type(issue.related_merge_requests()) == list
+
+
+def test_issue_notes(issue):
+ size = len(issue.notes.list())
+
+ note = issue.notes.create({"body": "This is an issue note"})
+ assert len(issue.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(issue.notes.list()) == size
+
+
+def test_issue_labels(project, issue):
+ project.labels.create({"name": "label2", "color": "#aabbcc"})
+ issue.labels = ["label2"]
+ issue.save()
+
+ assert issue in project.issues.list(labels=["label2"])
+ assert issue in project.issues.list(labels="label2")
+ assert issue in project.issues.list(labels="Any")
+ assert issue not in project.issues.list(labels="None")
+
+
+def test_issue_events(issue):
+ events = issue.resourcelabelevents.list()
+ assert isinstance(events, list)
+
+ event = issue.resourcelabelevents.get(events[0].id)
+ assert isinstance(event, gitlab.v4.objects.ProjectIssueResourceLabelEvent)
+
+
+def test_issue_milestones(project, milestone):
+ data = {"title": "my issue 1", "milestone_id": milestone.id}
+ issue = project.issues.create(data)
+ assert milestone.issues().next().title == "my issue 1"
+
+ milestone_events = issue.resourcemilestoneevents.list()
+ assert isinstance(milestone_events, list)
+
+ milestone_event = issue.resourcemilestoneevents.get(milestone_events[0].id)
+ assert isinstance(
+ milestone_event, gitlab.v4.objects.ProjectIssueResourceMilestoneEvent
+ )
+
+ milestone_issues = project.issues.list(milestone=milestone.title)
+ assert len(milestone_issues) == 1
+
+
+def test_issue_discussions(issue):
+ size = len(issue.discussions.list())
+
+ discussion = issue.discussions.create({"body": "Discussion body"})
+ assert len(issue.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 = issue.discussions.get(discussion.id)
+ assert discussion.attributes["notes"][-1]["body"] == "updated body"
+
+ d_note_from_get.delete()
+ discussion = issue.discussions.get(discussion.id)
+ assert len(discussion.attributes["notes"]) == 1
diff --git a/tests/functional/api/test_merge_requests.py b/tests/functional/api/test_merge_requests.py
new file mode 100644
index 0000000..e768234
--- /dev/null
+++ b/tests/functional/api/test_merge_requests.py
@@ -0,0 +1,165 @@
+import time
+
+import pytest
+
+import gitlab
+import gitlab.v4.objects
+
+
+def test_merge_requests(project):
+ project.files.create(
+ {
+ "file_path": "README.rst",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "Initial commit",
+ }
+ )
+
+ source_branch = "branch1"
+ project.branches.create({"branch": source_branch, "ref": "master"})
+
+ project.files.create(
+ {
+ "file_path": "README2.rst",
+ "branch": source_branch,
+ "content": "Initial content",
+ "commit_message": "New commit in new branch",
+ }
+ )
+ project.mergerequests.create(
+ {"source_branch": "branch1", "target_branch": "master", "title": "MR readme2"}
+ )
+
+
+def test_merge_request_discussion(project):
+ mr = project.mergerequests.list()[0]
+ size = len(mr.discussions.list())
+
+ discussion = mr.discussions.create({"body": "Discussion body"})
+ assert len(mr.discussions.list()) == size + 1
+
+ note = discussion.notes.create({"body": "first note"})
+ note_from_get = discussion.notes.get(note.id)
+ note_from_get.body = "updated body"
+ note_from_get.save()
+
+ discussion = mr.discussions.get(discussion.id)
+ assert discussion.attributes["notes"][-1]["body"] == "updated body"
+
+ note_from_get.delete()
+ discussion = mr.discussions.get(discussion.id)
+ assert len(discussion.attributes["notes"]) == 1
+
+
+def test_merge_request_labels(project):
+ mr = project.mergerequests.list()[0]
+ mr.labels = ["label2"]
+ mr.save()
+
+ events = mr.resourcelabelevents.list()
+ assert events
+
+ event = mr.resourcelabelevents.get(events[0].id)
+ assert event
+
+
+def test_merge_request_milestone_events(project, milestone):
+ mr = project.mergerequests.list()[0]
+ mr.milestone_id = milestone.id
+ mr.save()
+
+ milestones = mr.resourcemilestoneevents.list()
+ assert milestones
+
+ milestone = mr.resourcemilestoneevents.get(milestones[0].id)
+ assert milestone
+
+
+def test_merge_request_basic(project):
+ mr = project.mergerequests.list()[0]
+ # basic testing: only make sure that the methods exist
+ mr.commits()
+ mr.changes()
+ assert mr.participants()
+
+
+def test_merge_request_rebase(project):
+ mr = project.mergerequests.list()[0]
+ assert mr.rebase()
+
+
+@pytest.mark.skip(reason="flaky test")
+def test_merge_request_merge(project):
+ mr = project.mergerequests.list()[0]
+ mr.merge()
+ project.branches.delete(mr.source_branch)
+
+ with pytest.raises(gitlab.GitlabMRClosedError):
+ # Two merge attempts should raise GitlabMRClosedError
+ mr.merge()
+
+
+def test_merge_request_should_remove_source_branch(
+ project, merge_request, wait_for_sidekiq
+) -> None:
+ """Test to ensure
+ https://github.com/python-gitlab/python-gitlab/issues/1120 is fixed.
+ Bug reported that they could not use 'should_remove_source_branch' in
+ mr.merge() call"""
+
+ source_branch = "remove_source_branch"
+ mr = merge_request(source_branch=source_branch)
+
+ mr.merge(should_remove_source_branch=True)
+
+ result = wait_for_sidekiq(timeout=60)
+ assert result is True, "sidekiq process should have terminated but did not"
+
+ # Wait until it is merged
+ mr_iid = mr.iid
+ for _ in range(60):
+ mr = project.mergerequests.get(mr_iid)
+ if mr.merged_at is not None:
+ break
+ time.sleep(0.5)
+ assert mr.merged_at is not None
+ time.sleep(0.5)
+
+ # Ensure we can NOT get the MR branch
+ with pytest.raises(gitlab.exceptions.GitlabGetError):
+ project.branches.get(source_branch)
+
+
+def test_merge_request_large_commit_message(
+ project, merge_request, wait_for_sidekiq
+) -> None:
+ """Test to ensure https://github.com/python-gitlab/python-gitlab/issues/1452
+ is fixed.
+ Bug reported that very long 'merge_commit_message' in mr.merge() would
+ cause an error: 414 Request too large
+ """
+
+ source_branch = "large_commit_message"
+ mr = merge_request(source_branch=source_branch)
+
+ merge_commit_message = "large_message\r\n" * 1_000
+ assert len(merge_commit_message) > 10_000
+
+ mr.merge(merge_commit_message=merge_commit_message)
+
+ result = wait_for_sidekiq(timeout=60)
+ assert result is True, "sidekiq process should have terminated but did not"
+
+ # Wait until it is merged
+ mr_iid = mr.iid
+ for _ in range(60):
+ mr = project.mergerequests.get(mr_iid)
+ if mr.merged_at is not None:
+ break
+ time.sleep(0.5)
+ assert mr.merged_at is not None
+ time.sleep(0.5)
+
+ # Ensure we can get the MR branch
+ project.branches.get(source_branch)
diff --git a/tests/functional/api/test_packages.py b/tests/functional/api/test_packages.py
new file mode 100644
index 0000000..9160a68
--- /dev/null
+++ b/tests/functional/api/test_packages.py
@@ -0,0 +1,13 @@
+"""
+GitLab API: https://docs.gitlab.com/ce/api/packages.html
+"""
+
+
+def test_list_project_packages(project):
+ packages = project.packages.list()
+ assert isinstance(packages, list)
+
+
+def test_list_group_packages(group):
+ packages = group.packages.list()
+ assert isinstance(packages, list)
diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py
new file mode 100644
index 0000000..0823c00
--- /dev/null
+++ b/tests/functional/api/test_projects.py
@@ -0,0 +1,267 @@
+import pytest
+
+import gitlab
+
+
+def test_create_project(gl, user):
+ # Moved from group tests chunk in legacy tests, TODO cleanup
+ admin_project = gl.projects.create({"name": "admin_project"})
+ assert isinstance(admin_project, gitlab.v4.objects.Project)
+ assert len(gl.projects.list(search="admin")) == 1
+
+ sudo_project = gl.projects.create({"name": "sudo_project"}, sudo=user.id)
+
+ created = gl.projects.list()
+ created_gen = gl.projects.list(as_list=False)
+ owned = gl.projects.list(owned=True)
+
+ assert admin_project in created and sudo_project in created
+ assert admin_project in owned and sudo_project not in owned
+ assert len(created) == len(list(created_gen))
+
+ admin_project.delete()
+ sudo_project.delete()
+
+
+def test_project_badges(project):
+ badge_image = "http://example.com"
+ badge_link = "http://example/img.svg"
+
+ badge = project.badges.create({"link_url": badge_link, "image_url": badge_image})
+ assert len(project.badges.list()) == 1
+
+ badge.image_url = "http://another.example.com"
+ badge.save()
+
+ badge = project.badges.get(badge.id)
+ assert badge.image_url == "http://another.example.com"
+
+ badge.delete()
+ assert len(project.badges.list()) == 0
+
+
+@pytest.mark.skip(reason="Commented out in legacy test")
+def test_project_boards(project):
+ boards = 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
+
+
+def test_project_custom_attributes(gl, project):
+ attrs = project.customattributes.list()
+ assert len(attrs) == 0
+
+ attr = project.customattributes.set("key", "value1")
+ assert attr.key == "key"
+ assert attr.value == "value1"
+ assert len(project.customattributes.list()) == 1
+ assert len(gl.projects.list(custom_attributes={"key": "value1"})) == 1
+
+ attr = project.customattributes.set("key", "value2")
+ attr = project.customattributes.get("key")
+ assert attr.value == "value2"
+ assert len(project.customattributes.list()) == 1
+
+ attr.delete()
+ assert len(project.customattributes.list()) == 0
+
+
+def test_project_environments(project):
+ project.environments.create(
+ {"name": "env1", "external_url": "http://fake.env/whatever"}
+ )
+ environments = project.environments.list()
+ assert len(environments) == 1
+
+ environment = environments[0]
+ environment.external_url = "http://new.env/whatever"
+ environment.save()
+
+ environment = project.environments.list()[0]
+ assert environment.external_url == "http://new.env/whatever"
+
+ environment.stop()
+ environment.delete()
+ assert len(project.environments.list()) == 0
+
+
+def test_project_events(project):
+ events = project.events.list()
+ assert isinstance(events, list)
+
+
+def test_project_file_uploads(project):
+ filename = "test.txt"
+ file_contents = "testing contents"
+
+ uploaded_file = 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"]
+ )
+
+
+def test_project_forks(gl, project, user):
+ fork = project.forks.create({"namespace": user.username})
+ fork_project = gl.projects.get(fork.id)
+ assert fork_project.forked_from_project["id"] == project.id
+
+ forks = project.forks.list()
+ assert fork.id in map(lambda fork_project: fork_project.id, forks)
+
+
+def test_project_hooks(project):
+ hook = project.hooks.create({"url": "http://hook.url"})
+ assert len(project.hooks.list()) == 1
+
+ hook.note_events = True
+ hook.save()
+
+ hook = project.hooks.get(hook.id)
+ assert hook.note_events is True
+ hook.delete()
+
+
+def test_project_housekeeping(project):
+ project.housekeeping()
+
+
+def test_project_labels(project):
+ label = project.labels.create({"name": "label", "color": "#778899"})
+ labels = project.labels.list()
+ assert len(labels) == 1
+
+ label = project.labels.get("label")
+ assert label == labels[0]
+
+ label.new_name = "labelupdated"
+ label.save()
+ assert label.name == "labelupdated"
+
+ label.subscribe()
+ assert label.subscribed is True
+
+ label.unsubscribe()
+ assert label.subscribed is False
+
+ label.delete()
+ assert len(project.labels.list()) == 0
+
+
+def test_project_milestones(project):
+ milestone = project.milestones.create({"title": "milestone1"})
+ assert len(project.milestones.list()) == 1
+
+ milestone.due_date = "2020-01-01T00:00:00Z"
+ milestone.save()
+
+ milestone.state_event = "close"
+ milestone.save()
+
+ milestone = project.milestones.get(milestone.id)
+ assert milestone.state == "closed"
+ assert len(milestone.issues()) == 0
+ assert len(milestone.merge_requests()) == 0
+
+
+def test_project_pages_domains(gl, project):
+ domain = project.pagesdomains.create({"domain": "foo.domain.com"})
+ assert len(project.pagesdomains.list()) == 1
+ assert len(gl.pagesdomains.list()) == 1
+
+ domain = project.pagesdomains.get("foo.domain.com")
+ assert domain.domain == "foo.domain.com"
+
+ domain.delete()
+ assert len(project.pagesdomains.list()) == 0
+
+
+def test_project_protected_branches(project):
+ p_b = project.protectedbranches.create({"name": "*-stable"})
+ assert p_b.name == "*-stable"
+ assert len(project.protectedbranches.list()) == 1
+
+ p_b = project.protectedbranches.get("*-stable")
+ p_b.delete()
+ assert len(project.protectedbranches.list()) == 0
+
+
+def test_project_remote_mirrors(project):
+ mirror_url = "http://gitlab.test/root/mirror.git"
+
+ mirror = project.remote_mirrors.create({"url": mirror_url})
+ assert mirror.url == mirror_url
+
+ mirror.enabled = True
+ mirror.save()
+
+ mirror = project.remote_mirrors.list()[0]
+ assert isinstance(mirror, gitlab.v4.objects.ProjectRemoteMirror)
+ assert mirror.url == mirror_url
+ assert mirror.enabled is True
+
+
+def test_project_services(project):
+ service = project.services.get("asana")
+ service.api_key = "whatever"
+ service.save()
+
+ service = project.services.get("asana")
+ assert service.active is True
+
+ service.delete()
+
+ service = project.services.get("asana")
+ assert service.active is False
+
+
+def test_project_stars(project):
+ project.star()
+ assert project.star_count == 1
+
+ project.unstar()
+ assert project.star_count == 0
+
+
+def test_project_tags(project, project_file):
+ tag = project.tags.create({"tag_name": "v1.0", "ref": "master"})
+ assert len(project.tags.list()) == 1
+
+ tag.set_release_description("Description 1")
+ tag.set_release_description("Description 2")
+ assert tag.release["description"] == "Description 2"
+
+ tag.delete()
+ assert len(project.tags.list()) == 0
+
+
+def test_project_triggers(project):
+ trigger = project.triggers.create({"description": "trigger1"})
+ assert len(project.triggers.list()) == 1
+ trigger.delete()
+
+
+def test_project_wiki(project):
+ content = "Wiki page content"
+ wiki = project.wikis.create({"title": "wikipage", "content": content})
+ assert len(project.wikis.list()) == 1
+
+ wiki = project.wikis.get(wiki.slug)
+ assert wiki.content == content
+
+ # update and delete seem broken
+ wiki.content = "new content"
+ wiki.save()
+ wiki.delete()
+ assert len(project.wikis.list()) == 0
diff --git a/tests/functional/api/test_releases.py b/tests/functional/api/test_releases.py
new file mode 100644
index 0000000..f49181a
--- /dev/null
+++ b/tests/functional/api/test_releases.py
@@ -0,0 +1,36 @@
+release_name = "Demo Release"
+release_tag_name = "v1.2.3"
+release_description = "release notes go here"
+
+link_data = {"url": "https://example.com", "name": "link_name"}
+
+
+def test_create_project_release(project, project_file):
+ project.refresh() # Gets us the current default branch
+ release = project.releases.create(
+ {
+ "name": release_name,
+ "tag_name": release_tag_name,
+ "description": release_description,
+ "ref": project.default_branch,
+ }
+ )
+
+ assert len(project.releases.list()) == 1
+ assert project.releases.get(release_tag_name)
+ assert release.name == release_name
+ assert release.tag_name == release_tag_name
+ assert release.description == release_description
+
+
+def test_delete_project_release(project, release):
+ project.releases.delete(release.tag_name)
+ assert release not in project.releases.list()
+
+
+def test_create_project_release_links(project, release):
+ release.links.create(link_data)
+
+ release = project.releases.get(release.tag_name)
+ assert release.assets["links"][0]["url"] == link_data["url"]
+ assert release.assets["links"][0]["name"] == link_data["name"]
diff --git a/tests/functional/api/test_repository.py b/tests/functional/api/test_repository.py
new file mode 100644
index 0000000..7ba84ea
--- /dev/null
+++ b/tests/functional/api/test_repository.py
@@ -0,0 +1,126 @@
+import base64
+import time
+
+import pytest
+
+import gitlab
+
+
+def test_repository_files(project):
+ project.files.create(
+ {
+ "file_path": "README",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "Initial commit",
+ }
+ )
+ readme = 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")
+
+ project.files.create(
+ {
+ "file_path": "README.rst",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "New commit",
+ }
+ )
+ readme = 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 = project.files.blame(file_path="README.rst", ref="master")
+ assert blame
+
+
+def test_repository_tree(project):
+ tree = project.repository_tree()
+ assert tree
+ assert tree[0]["name"] == "README.rst"
+
+ blob_id = tree[0]["id"]
+ blob = project.repository_raw_blob(blob_id)
+ assert blob.decode() == "Initial content"
+
+ archive = project.repository_archive()
+ assert isinstance(archive, bytes)
+
+ archive2 = project.repository_archive("master")
+ assert archive == archive2
+
+ snapshot = project.snapshot()
+ assert isinstance(snapshot, bytes)
+
+
+def test_create_commit(project):
+ data = {
+ "branch": "master",
+ "commit_message": "blah blah blah",
+ "actions": [{"action": "create", "file_path": "blah", "content": "blah"}],
+ }
+ commit = project.commits.create(data)
+
+ assert "@@" in project.commits.list()[0].diff()[0]["diff"]
+ assert isinstance(commit.refs(), list)
+ assert isinstance(commit.merge_requests(), list)
+
+
+def test_create_commit_status(project):
+ commit = project.commits.list()[0]
+ size = len(commit.statuses.list())
+ commit.statuses.create({"state": "success", "sha": commit.id})
+ assert len(commit.statuses.list()) == size + 1
+
+
+def test_commit_signature(project):
+ commit = project.commits.list()[0]
+
+ with pytest.raises(gitlab.GitlabGetError) as e:
+ commit.signature()
+
+ assert "404 Signature Not Found" in str(e.value)
+
+
+def test_commit_comment(project):
+ commit = project.commits.list()[0]
+
+ commit.comments.create({"note": "This is a commit comment"})
+ assert len(commit.comments.list()) == 1
+
+
+def test_commit_discussion(project):
+ commit = project.commits.list()[0]
+ count = len(commit.discussions.list())
+
+ discussion = commit.discussions.create({"body": "Discussion body"})
+ assert len(commit.discussions.list()) == (count + 1)
+
+ note = discussion.notes.create({"body": "first note"})
+ note_from_get = discussion.notes.get(note.id)
+ note_from_get.body = "updated body"
+ note_from_get.save()
+ discussion = commit.discussions.get(discussion.id)
+ # assert discussion.attributes["notes"][-1]["body"] == "updated body"
+ note_from_get.delete()
+ discussion = commit.discussions.get(discussion.id)
+ # assert len(discussion.attributes["notes"]) == 1
+
+
+def test_revert_commit(project):
+ commit = project.commits.list()[0]
+ revert_commit = commit.revert(branch="master")
+
+ expected_message = 'Revert "{}"\n\nThis reverts commit {}'.format(
+ commit.message, commit.id
+ )
+ assert revert_commit["message"] == expected_message
+
+ with pytest.raises(gitlab.GitlabRevertError):
+ # Two revert attempts should raise GitlabRevertError
+ commit.revert(branch="master")
diff --git a/tests/functional/api/test_snippets.py b/tests/functional/api/test_snippets.py
new file mode 100644
index 0000000..936fbfb
--- /dev/null
+++ b/tests/functional/api/test_snippets.py
@@ -0,0 +1,74 @@
+import gitlab
+
+
+def test_snippets(gl):
+ 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
+
+
+def test_project_snippets(project):
+ project.snippets_enabled = True
+ project.save()
+
+ snippet = 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"]
+
+
+def test_project_snippet_discussion(project):
+ snippet = project.snippets.list()[0]
+ size = len(snippet.discussions.list())
+
+ discussion = snippet.discussions.create({"body": "Discussion body"})
+ assert len(snippet.discussions.list()) == size + 1
+
+ note = discussion.notes.create({"body": "first note"})
+ note_from_get = discussion.notes.get(note.id)
+ note_from_get.body = "updated body"
+ note_from_get.save()
+
+ discussion = snippet.discussions.get(discussion.id)
+ assert discussion.attributes["notes"][-1]["body"] == "updated body"
+
+ note_from_get.delete()
+ discussion = snippet.discussions.get(discussion.id)
+ assert len(discussion.attributes["notes"]) == 1
+
+
+def test_project_snippet_file(project):
+ snippet = project.snippets.list()[0]
+ snippet.file_name = "bar.py"
+ snippet.save()
+
+ snippet = project.snippets.get(snippet.id)
+ assert snippet.content().decode() == "initial content"
+ assert snippet.file_name == "bar.py"
+
+ size = len(project.snippets.list())
+ snippet.delete()
+ assert len(project.snippets.list()) == (size - 1)
diff --git a/tests/functional/api/test_users.py b/tests/functional/api/test_users.py
new file mode 100644
index 0000000..1ef237c
--- /dev/null
+++ b/tests/functional/api/test_users.py
@@ -0,0 +1,170 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/users.html
+https://docs.gitlab.com/ee/api/users.html#delete-authentication-identity-from-user
+"""
+import pytest
+import requests
+
+
+@pytest.fixture(scope="session")
+def avatar_path(test_dir):
+ return test_dir / "fixtures" / "avatar.png"
+
+
+def test_create_user(gl, avatar_path):
+ user = gl.users.create(
+ {
+ "email": "foo@bar.com",
+ "username": "foo",
+ "name": "foo",
+ "password": "foo_password",
+ "avatar": open(avatar_path, "rb"),
+ }
+ )
+
+ created_user = gl.users.list(username="foo")[0]
+ assert created_user.username == user.username
+ assert created_user.email == user.email
+
+ avatar_url = user.avatar_url.replace("gitlab.test", "localhost:8080")
+ uploaded_avatar = requests.get(avatar_url).content
+ assert uploaded_avatar == open(avatar_path, "rb").read()
+
+
+def test_block_user(gl, user):
+ user.block()
+ users = gl.users.list(blocked=True)
+ assert user in users
+
+ user.unblock()
+ users = gl.users.list(blocked=False)
+ assert user in users
+
+
+def test_delete_user(gl, wait_for_sidekiq):
+ new_user = gl.users.create(
+ {
+ "email": "delete-user@test.com",
+ "username": "delete-user",
+ "name": "delete-user",
+ "password": "delete-user-pass",
+ }
+ )
+
+ new_user.delete()
+ result = wait_for_sidekiq(timeout=60)
+ assert result is True, "sidekiq process should have terminated but did not"
+
+ assert new_user.id not in [user.id for user in gl.users.list()]
+
+
+def test_user_projects_list(gl, user):
+ projects = user.projects.list()
+ assert isinstance(projects, list)
+ assert not projects
+
+
+def test_user_events_list(gl, user):
+ events = user.events.list()
+ assert isinstance(events, list)
+ assert not events
+
+
+def test_user_bio(gl, user):
+ user.bio = "This is the user bio"
+ user.save()
+
+
+def test_list_multiple_users(gl, user):
+ second_email = f"{user.email}.2"
+ second_username = f"{user.username}_2"
+ second_user = gl.users.create(
+ {
+ "email": second_email,
+ "username": second_username,
+ "name": "Foo Bar",
+ "password": "foobar_password",
+ }
+ )
+ assert gl.users.list(search=second_user.username)[0].id == second_user.id
+
+ expected = [user, second_user]
+ actual = list(gl.users.list(search=user.username))
+
+ assert len(expected) == len(actual)
+ assert len(gl.users.list(search="asdf")) == 0
+
+
+def test_user_gpg_keys(gl, user, GPG_KEY):
+ gkey = user.gpgkeys.create({"key": GPG_KEY})
+ assert len(user.gpgkeys.list()) == 1
+
+ # Seems broken on the gitlab side
+ # gkey = user.gpgkeys.get(gkey.id)
+
+ gkey.delete()
+ assert len(user.gpgkeys.list()) == 0
+
+
+def test_user_ssh_keys(gl, user, SSH_KEY):
+ key = user.keys.create({"title": "testkey", "key": SSH_KEY})
+ assert len(user.keys.list()) == 1
+
+ key.delete()
+ assert len(user.keys.list()) == 0
+
+
+def test_user_email(gl, user):
+ email = user.emails.create({"email": "foo2@bar.com"})
+ assert len(user.emails.list()) == 1
+
+ email.delete()
+ assert len(user.emails.list()) == 0
+
+
+def test_user_custom_attributes(gl, user):
+ attrs = user.customattributes.list()
+ assert len(attrs) == 0
+
+ attr = 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(user.customattributes.list()) == 1
+
+ attr = user.customattributes.set("key", "value2")
+ attr = user.customattributes.get("key")
+ assert attr.value == "value2"
+ assert len(user.customattributes.list()) == 1
+
+ attr.delete()
+ assert len(user.customattributes.list()) == 0
+
+
+def test_user_impersonation_tokens(gl, user):
+ token = user.impersonationtokens.create(
+ {"name": "token1", "scopes": ["api", "read_user"]}
+ )
+
+ tokens = user.impersonationtokens.list(state="active")
+ assert len(tokens) == 1
+
+ token.delete()
+ tokens = user.impersonationtokens.list(state="active")
+ assert len(tokens) == 0
+ tokens = user.impersonationtokens.list(state="inactive")
+ assert len(tokens) == 1
+
+
+def test_user_identities(gl, user):
+ provider = "test_provider"
+
+ user.provider = provider
+ user.extern_uid = "1"
+ user.save()
+ assert provider in [item["provider"] for item in user.identities]
+
+ user.identityproviders.delete(provider)
+ user = gl.users.get(user.id)
+ assert provider not in [item["provider"] for item in user.identities]
diff --git a/tests/functional/api/test_variables.py b/tests/functional/api/test_variables.py
new file mode 100644
index 0000000..d20ebba
--- /dev/null
+++ b/tests/functional/api/test_variables.py
@@ -0,0 +1,48 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/instance_level_ci_variables.html
+https://docs.gitlab.com/ee/api/project_level_variables.html
+https://docs.gitlab.com/ee/api/group_level_variables.html
+"""
+
+
+def test_instance_variables(gl):
+ variable = gl.variables.create({"key": "key1", "value": "value1"})
+ assert variable.value == "value1"
+ assert len(gl.variables.list()) == 1
+
+ variable.value = "new_value1"
+ variable.save()
+ variable = gl.variables.get(variable.key)
+ assert variable.value == "new_value1"
+
+ variable.delete()
+ assert len(gl.variables.list()) == 0
+
+
+def test_group_variables(group):
+ variable = group.variables.create({"key": "key1", "value": "value1"})
+ assert variable.value == "value1"
+ assert len(group.variables.list()) == 1
+
+ variable.value = "new_value1"
+ variable.save()
+ variable = group.variables.get(variable.key)
+ assert variable.value == "new_value1"
+
+ variable.delete()
+ assert len(group.variables.list()) == 0
+
+
+def test_project_variables(project):
+ variable = project.variables.create({"key": "key1", "value": "value1"})
+ assert variable.value == "value1"
+ assert len(project.variables.list()) == 1
+
+ variable.value = "new_value1"
+ variable.save()
+ variable = project.variables.get(variable.key)
+ assert variable.value == "new_value1"
+
+ variable.delete()
+ assert len(project.variables.list()) == 0
diff --git a/tests/functional/cli/conftest.py b/tests/functional/cli/conftest.py
new file mode 100644
index 0000000..ba94dcb
--- /dev/null
+++ b/tests/functional/cli/conftest.py
@@ -0,0 +1,21 @@
+import pytest
+
+
+@pytest.fixture
+def gitlab_cli(script_runner, gitlab_config):
+ """Wrapper fixture to help make test cases less verbose."""
+
+ def _gitlab_cli(subcommands):
+ """
+ Return a script_runner.run method that takes a default gitlab
+ command, and subcommands passed as arguments inside test cases.
+ """
+ command = ["gitlab", "--config-file", gitlab_config]
+
+ for subcommand in subcommands:
+ # ensure we get strings (e.g from IDs)
+ command.append(str(subcommand))
+
+ return script_runner.run(*command)
+
+ return _gitlab_cli
diff --git a/tests/functional/cli/test_cli_artifacts.py b/tests/functional/cli/test_cli_artifacts.py
new file mode 100644
index 0000000..4cb69aa
--- /dev/null
+++ b/tests/functional/cli/test_cli_artifacts.py
@@ -0,0 +1,51 @@
+import subprocess
+import sys
+import textwrap
+import time
+from io import BytesIO
+from zipfile import is_zipfile
+
+import pytest
+
+content = textwrap.dedent(
+ """\
+ test-artifact:
+ script: echo "test" > artifact.txt
+ artifacts:
+ untracked: true
+ """
+)
+data = {
+ "file_path": ".gitlab-ci.yml",
+ "branch": "master",
+ "content": content,
+ "commit_message": "Initial commit",
+}
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason="I am the walrus")
+def test_cli_artifacts(capsysbinary, gitlab_config, gitlab_runner, project):
+ project.files.create(data)
+
+ while not (jobs := project.jobs.list(scope="success")):
+ time.sleep(0.5)
+
+ job = project.jobs.get(jobs[0].id)
+ cmd = [
+ "gitlab",
+ "--config-file",
+ gitlab_config,
+ "project-job",
+ "artifacts",
+ "--id",
+ str(job.id),
+ "--project-id",
+ str(project.id),
+ ]
+
+ with capsysbinary.disabled():
+ artifacts = subprocess.check_output(cmd)
+ assert isinstance(artifacts, bytes)
+
+ artifacts_zip = BytesIO(artifacts)
+ assert is_zipfile(artifacts_zip)
diff --git a/tests/functional/cli/test_cli_packages.py b/tests/functional/cli/test_cli_packages.py
new file mode 100644
index 0000000..a3734a2
--- /dev/null
+++ b/tests/functional/cli/test_cli_packages.py
@@ -0,0 +1,12 @@
+def test_list_project_packages(gitlab_cli, project):
+ cmd = ["project-package", "list", "--project-id", project.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_group_packages(gitlab_cli, group):
+ cmd = ["group-package", "list", "--group-id", group.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
diff --git a/tests/functional/cli/test_cli_v4.py b/tests/functional/cli/test_cli_v4.py
new file mode 100644
index 0000000..a63c1b1
--- /dev/null
+++ b/tests/functional/cli/test_cli_v4.py
@@ -0,0 +1,715 @@
+import os
+import time
+
+
+def test_create_project(gitlab_cli):
+ name = "test-project1"
+
+ cmd = ["project", "create", "--name", name]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert name in ret.stdout
+
+
+def test_update_project(gitlab_cli, project):
+ description = "My New Description"
+
+ cmd = ["project", "update", "--id", project.id, "--description", description]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert description in ret.stdout
+
+
+def test_create_group(gitlab_cli):
+ name = "test-group1"
+ path = "group1"
+
+ cmd = ["group", "create", "--name", name, "--path", path]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert name in ret.stdout
+ assert path in ret.stdout
+
+
+def test_update_group(gitlab_cli, gl, group):
+ description = "My New Description"
+
+ cmd = ["group", "update", "--id", group.id, "--description", description]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+ group = gl.groups.get(group.id)
+ assert group.description == description
+
+
+def test_create_user(gitlab_cli, gl):
+ email = "fake@email.com"
+ username = "user1"
+ name = "User One"
+ password = "fakepassword"
+
+ cmd = [
+ "user",
+ "create",
+ "--email",
+ email,
+ "--username",
+ username,
+ "--name",
+ name,
+ "--password",
+ password,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+ user = gl.users.list(username=username)[0]
+
+ assert user.email == email
+ assert user.username == username
+ assert user.name == name
+
+
+def test_get_user_by_id(gitlab_cli, user):
+ cmd = ["user", "get", "--id", user.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert str(user.id) in ret.stdout
+
+
+def test_list_users_verbose_output(gitlab_cli):
+ cmd = ["-v", "user", "list"]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert "avatar-url" in ret.stdout
+
+
+def test_cli_args_not_in_output(gitlab_cli):
+ cmd = ["-v", "user", "list"]
+ ret = gitlab_cli(cmd)
+
+ assert "config-file" not in ret.stdout
+
+
+def test_add_member_to_project(gitlab_cli, project, user):
+ access_level = "40"
+
+ cmd = [
+ "project-member",
+ "create",
+ "--project-id",
+ project.id,
+ "--user-id",
+ user.id,
+ "--access-level",
+ access_level,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_user_memberships(gitlab_cli, user):
+ cmd = ["user-membership", "list", "--user-id", user.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_project_create_file(gitlab_cli, project):
+ file_path = "README"
+ branch = "master"
+ content = "CONTENT"
+ commit_message = "Initial commit"
+
+ cmd = [
+ "project-file",
+ "create",
+ "--project-id",
+ project.id,
+ "--file-path",
+ file_path,
+ "--branch",
+ branch,
+ "--content",
+ content,
+ "--commit-message",
+ commit_message,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_project_issue(gitlab_cli, project):
+ title = "my issue"
+ description = "my issue description"
+
+ cmd = [
+ "project-issue",
+ "create",
+ "--project-id",
+ project.id,
+ "--title",
+ title,
+ "--description",
+ description,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert title in ret.stdout
+
+
+def test_create_issue_note(gitlab_cli, issue):
+ body = "body"
+
+ cmd = [
+ "project-issue-note",
+ "create",
+ "--project-id",
+ issue.project_id,
+ "--issue-iid",
+ issue.iid,
+ "--body",
+ body,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_branch(gitlab_cli, project):
+ branch = "branch1"
+
+ cmd = [
+ "project-branch",
+ "create",
+ "--project-id",
+ project.id,
+ "--branch",
+ branch,
+ "--ref",
+ "master",
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_merge_request(gitlab_cli, project):
+ branch = "branch1"
+
+ cmd = [
+ "project-merge-request",
+ "create",
+ "--project-id",
+ project.id,
+ "--source-branch",
+ branch,
+ "--target-branch",
+ "master",
+ "--title",
+ "Update README",
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_accept_request_merge(gitlab_cli, project):
+ # MR needs at least 1 commit before we can merge
+ mr = project.mergerequests.list()[0]
+ file_data = {
+ "branch": mr.source_branch,
+ "file_path": "README2",
+ "content": "Content",
+ "commit_message": "Pre-merge commit",
+ }
+ project.files.create(file_data)
+ time.sleep(2)
+
+ cmd = [
+ "project-merge-request",
+ "merge",
+ "--project-id",
+ project.id,
+ "--iid",
+ mr.iid,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_revert_commit(gitlab_cli, project):
+ commit = project.commits.list()[0]
+
+ cmd = [
+ "project-commit",
+ "revert",
+ "--project-id",
+ project.id,
+ "--id",
+ commit.id,
+ "--branch",
+ "master",
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_get_commit_signature_not_found(gitlab_cli, project):
+ commit = project.commits.list()[0]
+
+ cmd = ["project-commit", "signature", "--project-id", project.id, "--id", commit.id]
+ ret = gitlab_cli(cmd)
+
+ assert not ret.success
+ assert "404 Signature Not Found" in ret.stderr
+
+
+def test_create_project_label(gitlab_cli, project):
+ name = "prjlabel1"
+ description = "prjlabel1 description"
+ color = "#112233"
+
+ cmd = [
+ "-v",
+ "project-label",
+ "create",
+ "--project-id",
+ project.id,
+ "--name",
+ name,
+ "--description",
+ description,
+ "--color",
+ color,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_project_labels(gitlab_cli, project):
+ cmd = ["-v", "project-label", "list", "--project-id", project.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_update_project_label(gitlab_cli, label):
+ new_label = "prjlabel2"
+ new_description = "prjlabel2 description"
+ new_color = "#332211"
+
+ cmd = [
+ "-v",
+ "project-label",
+ "update",
+ "--project-id",
+ label.project_id,
+ "--name",
+ label.name,
+ "--new-name",
+ new_label,
+ "--description",
+ new_description,
+ "--color",
+ new_color,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_delete_project_label(gitlab_cli, label):
+ # TODO: due to update above, we'd need a function-scope label fixture
+ label_name = "prjlabel2"
+
+ cmd = [
+ "-v",
+ "project-label",
+ "delete",
+ "--project-id",
+ label.project_id,
+ "--name",
+ label_name,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_group_label(gitlab_cli, group):
+ name = "grouplabel1"
+ description = "grouplabel1 description"
+ color = "#112233"
+
+ cmd = [
+ "-v",
+ "group-label",
+ "create",
+ "--group-id",
+ group.id,
+ "--name",
+ name,
+ "--description",
+ description,
+ "--color",
+ color,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_group_labels(gitlab_cli, group):
+ cmd = ["-v", "group-label", "list", "--group-id", group.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_update_group_label(gitlab_cli, group_label):
+ new_label = "grouplabel2"
+ new_description = "grouplabel2 description"
+ new_color = "#332211"
+
+ cmd = [
+ "-v",
+ "group-label",
+ "update",
+ "--group-id",
+ group_label.group_id,
+ "--name",
+ group_label.name,
+ "--new-name",
+ new_label,
+ "--description",
+ new_description,
+ "--color",
+ new_color,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_delete_group_label(gitlab_cli, group_label):
+ # TODO: due to update above, we'd need a function-scope label fixture
+ new_label = "grouplabel2"
+
+ cmd = [
+ "-v",
+ "group-label",
+ "delete",
+ "--group-id",
+ group_label.group_id,
+ "--name",
+ new_label,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_project_variable(gitlab_cli, project):
+ key = "junk"
+ value = "car"
+
+ cmd = [
+ "-v",
+ "project-variable",
+ "create",
+ "--project-id",
+ project.id,
+ "--key",
+ key,
+ "--value",
+ value,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_get_project_variable(gitlab_cli, variable):
+ cmd = [
+ "-v",
+ "project-variable",
+ "get",
+ "--project-id",
+ variable.project_id,
+ "--key",
+ variable.key,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_update_project_variable(gitlab_cli, variable):
+ new_value = "bus"
+
+ cmd = [
+ "-v",
+ "project-variable",
+ "update",
+ "--project-id",
+ variable.project_id,
+ "--key",
+ variable.key,
+ "--value",
+ new_value,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_project_variables(gitlab_cli, project):
+ cmd = ["-v", "project-variable", "list", "--project-id", project.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_delete_project_variable(gitlab_cli, variable):
+ cmd = [
+ "-v",
+ "project-variable",
+ "delete",
+ "--project-id",
+ variable.project_id,
+ "--key",
+ variable.key,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_delete_branch(gitlab_cli, project):
+ # TODO: branch fixture
+ branch = "branch1"
+
+ cmd = ["project-branch", "delete", "--project-id", project.id, "--name", branch]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_project_upload_file(gitlab_cli, project):
+ cmd = [
+ "project",
+ "upload",
+ "--id",
+ project.id,
+ "--filename",
+ __file__,
+ "--filepath",
+ os.path.realpath(__file__),
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_get_application_settings(gitlab_cli):
+ cmd = ["application-settings", "get"]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_update_application_settings(gitlab_cli):
+ cmd = ["application-settings", "update", "--signup-enabled", "false"]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_create_project_with_values_from_file(gitlab_cli, tmpdir):
+ name = "gitlab-project-from-file"
+ description = "Multiline\n\nData\n"
+ from_file = tmpdir.join(name)
+ from_file.write(description)
+ from_file_path = f"@{str(from_file)}"
+
+ cmd = [
+ "-v",
+ "project",
+ "create",
+ "--name",
+ name,
+ "--description",
+ from_file_path,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert description in ret.stdout
+
+
+def test_create_project_deploy_token(gitlab_cli, project):
+ name = "project-token"
+ username = "root"
+ expires_at = "2021-09-09"
+ scopes = "read_registry"
+
+ cmd = [
+ "-v",
+ "project-deploy-token",
+ "create",
+ "--project-id",
+ project.id,
+ "--name",
+ name,
+ "--username",
+ username,
+ "--expires-at",
+ expires_at,
+ "--scopes",
+ scopes,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert name in ret.stdout
+ assert username in ret.stdout
+ assert expires_at in ret.stdout
+ assert scopes in ret.stdout
+
+
+def test_list_all_deploy_tokens(gitlab_cli, deploy_token):
+ cmd = ["-v", "deploy-token", "list"]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert deploy_token.name in ret.stdout
+ assert str(deploy_token.id) in ret.stdout
+ assert deploy_token.username in ret.stdout
+ assert deploy_token.expires_at in ret.stdout
+ assert deploy_token.scopes[0] in ret.stdout
+
+
+def test_list_project_deploy_tokens(gitlab_cli, deploy_token):
+ cmd = [
+ "-v",
+ "project-deploy-token",
+ "list",
+ "--project-id",
+ deploy_token.project_id,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert deploy_token.name in ret.stdout
+ assert str(deploy_token.id) in ret.stdout
+ assert deploy_token.username in ret.stdout
+ assert deploy_token.expires_at in ret.stdout
+ assert deploy_token.scopes[0] in ret.stdout
+
+
+def test_delete_project_deploy_token(gitlab_cli, deploy_token):
+ cmd = [
+ "-v",
+ "project-deploy-token",
+ "delete",
+ "--project-id",
+ deploy_token.project_id,
+ "--id",
+ deploy_token.id,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ # TODO assert not in list
+
+
+def test_create_group_deploy_token(gitlab_cli, group):
+ name = "group-token"
+ username = "root"
+ expires_at = "2021-09-09"
+ scopes = "read_registry"
+
+ cmd = [
+ "-v",
+ "group-deploy-token",
+ "create",
+ "--group-id",
+ group.id,
+ "--name",
+ name,
+ "--username",
+ username,
+ "--expires-at",
+ expires_at,
+ "--scopes",
+ scopes,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert name in ret.stdout
+ assert username in ret.stdout
+ assert expires_at in ret.stdout
+ assert scopes in ret.stdout
+
+
+def test_list_group_deploy_tokens(gitlab_cli, group_deploy_token):
+ cmd = [
+ "-v",
+ "group-deploy-token",
+ "list",
+ "--group-id",
+ group_deploy_token.group_id,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ assert group_deploy_token.name in ret.stdout
+ assert str(group_deploy_token.id) in ret.stdout
+ assert group_deploy_token.username in ret.stdout
+ assert group_deploy_token.expires_at in ret.stdout
+ assert group_deploy_token.scopes[0] in ret.stdout
+
+
+def test_delete_group_deploy_token(gitlab_cli, group_deploy_token):
+ cmd = [
+ "-v",
+ "group-deploy-token",
+ "delete",
+ "--group-id",
+ group_deploy_token.group_id,
+ "--id",
+ group_deploy_token.id,
+ ]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+ # TODO assert not in list
+
+
+def test_delete_project(gitlab_cli, project):
+ cmd = ["project", "delete", "--id", project.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_delete_group(gitlab_cli, group):
+ cmd = ["group", "delete", "--id", group.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
diff --git a/tests/functional/cli/test_cli_variables.py b/tests/functional/cli/test_cli_variables.py
new file mode 100644
index 0000000..9b1b16d
--- /dev/null
+++ b/tests/functional/cli/test_cli_variables.py
@@ -0,0 +1,19 @@
+def test_list_instance_variables(gitlab_cli, gl):
+ cmd = ["variable", "list"]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_group_variables(gitlab_cli, group):
+ cmd = ["group-variable", "list", "--group-id", group.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
+
+
+def test_list_project_variables(gitlab_cli, project):
+ cmd = ["project-variable", "list", "--project-id", project.id]
+ ret = gitlab_cli(cmd)
+
+ assert ret.success
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
new file mode 100644
index 0000000..5d3b1b9
--- /dev/null
+++ b/tests/functional/conftest.py
@@ -0,0 +1,462 @@
+import tempfile
+import time
+import uuid
+from pathlib import Path
+from subprocess import check_output
+
+import pytest
+
+import gitlab
+
+
+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, rootdir):
+ set_token_rb = rootdir / "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
+
+
+def pytest_report_collectionfinish(config, startdir, items):
+ return [
+ "",
+ "Starting GitLab container.",
+ "Waiting for GitLab to reconfigure.",
+ "This may take a few minutes.",
+ ]
+
+
+@pytest.fixture(scope="session")
+def temp_dir():
+ return Path(tempfile.gettempdir())
+
+
+@pytest.fixture(scope="session")
+def test_dir(pytestconfig):
+ return pytestconfig.rootdir / "tests" / "functional"
+
+
+@pytest.fixture(scope="session")
+def docker_compose_file(test_dir):
+ return test_dir / "fixtures" / "docker-compose.yml"
+
+
+@pytest.fixture(scope="session")
+def check_is_alive():
+ """
+ Return a healthcheck function fixture for the GitLab container spinup.
+ """
+
+ def _check(container):
+ logs = ["docker", "logs", container]
+ return "gitlab Reconfigured!" in check_output(logs).decode()
+
+ return _check
+
+
+@pytest.fixture
+def wait_for_sidekiq(gl):
+ """
+ Return a helper function to wait until there are no busy sidekiq processes.
+
+ Use this with asserts for slow tasks (group/project/user creation/deletion).
+ """
+
+ def _wait(timeout=30, step=0.5):
+ for _ in range(timeout):
+ time.sleep(step)
+ busy = False
+ processes = gl.sidekiq.process_metrics()["processes"]
+ for process in processes:
+ if process["busy"]:
+ busy = True
+ if not busy:
+ return True
+ return False
+
+ return _wait
+
+
+@pytest.fixture(scope="session")
+def gitlab_config(check_is_alive, docker_ip, docker_services, temp_dir, test_dir):
+ config_file = temp_dir / "python-gitlab.cfg"
+ port = docker_services.port_for("gitlab", 80)
+
+ docker_services.wait_until_responsive(
+ timeout=200, pause=5, check=lambda: check_is_alive("gitlab-test")
+ )
+
+ token = set_token("gitlab-test", rootdir=test_dir)
+
+ 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."""
+
+ instance = gitlab.Gitlab.from_config("local", [gitlab_config])
+ reset_gitlab(instance)
+
+ return instance
+
+
+@pytest.fixture(scope="session")
+def gitlab_runner(gl):
+ container = "gitlab-runner-test"
+ runner_name = "python-gitlab-runner"
+ token = "registration-token"
+ url = "http://gitlab"
+
+ docker_exec = ["docker", "exec", container, "gitlab-runner"]
+ register = [
+ "register",
+ "--run-untagged",
+ "--non-interactive",
+ "--registration-token",
+ token,
+ "--name",
+ runner_name,
+ "--url",
+ url,
+ "--clone-url",
+ url,
+ "--executor",
+ "shell",
+ ]
+ unregister = ["unregister", "--name", runner_name]
+
+ yield check_output(docker_exec + register).decode()
+
+ check_output(docker_exec + unregister).decode()
+
+
+@pytest.fixture(scope="module")
+def group(gl):
+ """Group fixture for group API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {
+ "name": f"test-group-{_id}",
+ "path": f"group-{_id}",
+ }
+ group = gl.groups.create(data)
+
+ yield group
+
+ try:
+ group.delete()
+ except gitlab.exceptions.GitlabDeleteError as e:
+ print(f"Group already deleted: {e}")
+
+
+@pytest.fixture(scope="module")
+def project(gl):
+ """Project fixture for project API resource tests."""
+ _id = uuid.uuid4().hex
+ name = f"test-project-{_id}"
+
+ project = gl.projects.create(name=name)
+
+ yield project
+
+ try:
+ project.delete()
+ except gitlab.exceptions.GitlabDeleteError as e:
+ print(f"Project already deleted: {e}")
+
+
+@pytest.fixture(scope="function")
+def merge_request(project, wait_for_sidekiq):
+ """Fixture used to create a merge_request.
+
+ It will create a branch, add a commit to the branch, and then create a
+ merge request against project.default_branch. The MR will be returned.
+
+ When finished any created merge requests and branches will be deleted.
+
+ NOTE: No attempt is made to restore project.default_branch to its previous
+ state. So if the merge request is merged then its content will be in the
+ project.default_branch branch.
+ """
+
+ to_delete = []
+
+ def _merge_request(*, source_branch: str):
+ # Wait for processes to be done before we start...
+ # NOTE(jlvillal): Sometimes the CI would give a "500 Internal Server
+ # Error". Hoping that waiting until all other processes are done will
+ # help with that.
+ result = wait_for_sidekiq(timeout=60)
+ assert result is True, "sidekiq process should have terminated but did not"
+
+ project.refresh() # Gets us the current default branch
+ project.branches.create(
+ {"branch": source_branch, "ref": project.default_branch}
+ )
+ # NOTE(jlvillal): Must create a commit in the new branch before we can
+ # create an MR that will work.
+ project.files.create(
+ {
+ "file_path": f"README.{source_branch}",
+ "branch": source_branch,
+ "content": "Initial content",
+ "commit_message": "New commit in new branch",
+ }
+ )
+ mr = project.mergerequests.create(
+ {
+ "source_branch": source_branch,
+ "target_branch": project.default_branch,
+ "title": "Should remove source branch",
+ "remove_source_branch": True,
+ }
+ )
+ result = wait_for_sidekiq(timeout=60)
+ assert result is True, "sidekiq process should have terminated but did not"
+
+ mr_iid = mr.iid
+ for _ in range(60):
+ mr = project.mergerequests.get(mr_iid)
+ if mr.merge_status != "checking":
+ break
+ time.sleep(0.5)
+ assert mr.merge_status != "checking"
+
+ to_delete.append((mr.iid, source_branch))
+ return mr
+
+ yield _merge_request
+
+ for mr_iid, source_branch in to_delete:
+ project.mergerequests.delete(mr_iid)
+ try:
+ project.branches.delete(source_branch)
+ except gitlab.exceptions.GitlabDeleteError:
+ # Ignore if branch was already deleted
+ pass
+
+
+@pytest.fixture(scope="module")
+def project_file(project):
+ """File fixture for tests requiring a project with files and branches."""
+ project_file = project.files.create(
+ {
+ "file_path": "README",
+ "branch": "master",
+ "content": "Initial content",
+ "commit_message": "Initial commit",
+ }
+ )
+
+ return project_file
+
+
+@pytest.fixture(scope="function")
+def release(project, project_file):
+ _id = uuid.uuid4().hex
+ name = f"test-release-{_id}"
+
+ project.refresh() # Gets us the current default branch
+ release = project.releases.create(
+ {
+ "name": name,
+ "tag_name": _id,
+ "description": "description",
+ "ref": project.default_branch,
+ }
+ )
+
+ return release
+
+
+@pytest.fixture(scope="module")
+def user(gl):
+ """User fixture for user API resource tests."""
+ _id = uuid.uuid4().hex
+ email = f"user{_id}@email.com"
+ username = f"user{_id}"
+ name = f"User {_id}"
+ password = "fakepassword"
+
+ user = gl.users.create(email=email, username=username, name=name, password=password)
+
+ yield user
+
+ try:
+ user.delete()
+ except gitlab.exceptions.GitlabDeleteError as e:
+ print(f"User already deleted: {e}")
+
+
+@pytest.fixture(scope="module")
+def issue(project):
+ """Issue fixture for issue API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {"title": f"Issue {_id}", "description": f"Issue {_id} description"}
+
+ return project.issues.create(data)
+
+
+@pytest.fixture(scope="module")
+def milestone(project):
+ _id = uuid.uuid4().hex
+ data = {"title": f"milestone{_id}"}
+
+ return project.milestones.create(data)
+
+
+@pytest.fixture(scope="module")
+def label(project):
+ """Label fixture for project label API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {
+ "name": f"prjlabel{_id}",
+ "description": f"prjlabel1 {_id} description",
+ "color": "#112233",
+ }
+
+ return project.labels.create(data)
+
+
+@pytest.fixture(scope="module")
+def group_label(group):
+ """Label fixture for group label API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {
+ "name": f"grplabel{_id}",
+ "description": f"grplabel1 {_id} description",
+ "color": "#112233",
+ }
+
+ return group.labels.create(data)
+
+
+@pytest.fixture(scope="module")
+def variable(project):
+ """Variable fixture for project variable API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {"key": f"var{_id}", "value": f"Variable {_id}"}
+
+ return project.variables.create(data)
+
+
+@pytest.fixture(scope="module")
+def deploy_token(project):
+ """Deploy token fixture for project deploy token API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {
+ "name": f"token-{_id}",
+ "username": "root",
+ "expires_at": "2021-09-09",
+ "scopes": "read_registry",
+ }
+
+ return project.deploytokens.create(data)
+
+
+@pytest.fixture(scope="module")
+def group_deploy_token(group):
+ """Deploy token fixture for group deploy token API resource tests."""
+ _id = uuid.uuid4().hex
+ data = {
+ "name": f"group-token-{_id}",
+ "username": "root",
+ "expires_at": "2021-09-09",
+ "scopes": "read_registry",
+ }
+
+ return group.deploytokens.create(data)
+
+
+@pytest.fixture(scope="session")
+def GPG_KEY():
+ return """-----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-----"""
+
+
+@pytest.fixture(scope="session")
+def SSH_KEY():
+ return (
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZAjAX8vTiHD7Yi3/EzuVaDChtih"
+ "79HyJZ6H9dEqxFfmGA1YnncE0xujQ64TCebhkYJKzmTJCImSVkOu9C4hZgsw6eE76n"
+ "+Cg3VwEeDUFy+GXlEJWlHaEyc3HWioxgOALbUp3rOezNh+d8BDwwqvENGoePEBsz5l"
+ "a6WP5lTi/HJIjAl6Hu+zHgdj1XVExeH+S52EwpZf/ylTJub0Bl5gHwf/siVE48mLMI"
+ "sqrukXTZ6Zg+8EHAIvIQwJ1dKcXe8P5IoLT7VKrbkgAnolS0I8J+uH7KtErZJb5oZh"
+ "S4OEwsNpaXMAr+6/wWSpircV2/e7sFLlhlKBC4Iq1MpqlZ7G3p foo@bar"
+ )
+
+
+@pytest.fixture(scope="session")
+def DEPLOY_KEY():
+ return (
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFdRyjJQh+1niBpXqE2I8dzjG"
+ "MXFHlRjX9yk/UfOn075IdaockdU58sw2Ai1XIWFpZpfJkW7z+P47ZNSqm1gzeXI"
+ "rtKa9ZUp8A7SZe8vH4XVn7kh7bwWCUirqtn8El9XdqfkzOs/+FuViriUWoJVpA6"
+ "WZsDNaqINFKIA5fj/q8XQw+BcS92L09QJg9oVUuH0VVwNYbU2M2IRmSpybgC/gu"
+ "uWTrnCDMmLItksATifLvRZwgdI8dr+q6tbxbZknNcgEPrI2jT0hYN9ZcjNeWuyv"
+ "rke9IepE7SPBT41C+YtUX4dfDZDmczM1cE0YL/krdUCfuZHMa4ZS2YyNd6slufc"
+ "vn bar@foo"
+ )
diff --git a/tests/functional/ee-test.py b/tests/functional/ee-test.py
new file mode 100755
index 0000000..3a99951
--- /dev/null
+++ b/tests/functional/ee-test.py
@@ -0,0 +1,158 @@
+#!/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 is False
+pr.delete()
+end_log()
+
+start_log("license")
+license = gl.get_license()
+assert "user_limit" in license
+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/tests/functional/fixtures/.env b/tests/functional/fixtures/.env
new file mode 100644
index 0000000..eacfb28
--- /dev/null
+++ b/tests/functional/fixtures/.env
@@ -0,0 +1,2 @@
+GITLAB_IMAGE=gitlab/gitlab-ce
+GITLAB_TAG=13.11.4-ce.0
diff --git a/tests/functional/fixtures/avatar.png b/tests/functional/fixtures/avatar.png
new file mode 100644
index 0000000..a3a767c
--- /dev/null
+++ b/tests/functional/fixtures/avatar.png
Binary files differ
diff --git a/tests/functional/fixtures/docker-compose.yml b/tests/functional/fixtures/docker-compose.yml
new file mode 100644
index 0000000..a0794d6
--- /dev/null
+++ b/tests/functional/fixtures/docker-compose.yml
@@ -0,0 +1,46 @@
+version: '3'
+
+networks:
+ gitlab-network:
+ name: gitlab-network
+
+services:
+ gitlab:
+ image: '${GITLAB_IMAGE}:${GITLAB_TAG}'
+ 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'] = 'registration-token'
+ 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'
+ networks:
+ - gitlab-network
+
+ gitlab-runner:
+ image: gitlab/gitlab-runner:latest
+ container_name: 'gitlab-runner-test'
+ depends_on:
+ - gitlab
+ networks:
+ - gitlab-network
diff --git a/tests/functional/fixtures/set_token.rb b/tests/functional/fixtures/set_token.rb
new file mode 100644
index 0000000..735dcd5
--- /dev/null
+++ b/tests/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