diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ed1ed3849..8433be243 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -75,6 +75,9 @@ You need to install ``tox`` (``pip3 install tox``) to run tests and lint checks # run unit tests in one python environment only (useful for quick testing during development): tox -e py311 + # run unit and smoke tests in one python environment only + tox -e py312,smoke + # build the documentation - the result will be generated in build/sphinx/html/: tox -e docs diff --git a/docs/cli-examples.rst b/docs/cli-examples.rst index 7408d9bad..2ed1c5804 100644 --- a/docs/cli-examples.rst +++ b/docs/cli-examples.rst @@ -9,9 +9,11 @@ CLI examples CI Lint ------- +**ci-lint has been Removed in Gitlab 16, use project-ci-lint instead** + Lint a CI YAML configuration from a string: -.. note:: +.. note:: To see output, you will need to use the ``-v``/``--verbose`` flag. @@ -39,6 +41,9 @@ Validate a CI YAML configuration from a file (lints and exits with non-zero on f $ gitlab ci-lint validate --content @.gitlab-ci.yml +Project CI Lint +--------------- + Lint a project's CI YAML configuration: .. code-block:: console diff --git a/tests/functional/api/test_boards.py b/tests/functional/api/test_boards.py index 24c62ca67..1679a14e9 100644 --- a/tests/functional/api/test_boards.py +++ b/tests/functional/api/test_boards.py @@ -5,7 +5,6 @@ def test_project_boards(project): board = project.boards.get(board.id) project.boards.delete(board.id) - assert not project.boards.list() def test_group_boards(group): @@ -15,4 +14,3 @@ def test_group_boards(group): board = group.boards.get(board.id) group.boards.delete(board.id) - assert not group.boards.list() diff --git a/tests/functional/api/test_bulk_imports.py b/tests/functional/api/test_bulk_imports.py index 899d35840..a9a649fcc 100644 --- a/tests/functional/api/test_bulk_imports.py +++ b/tests/functional/api/test_bulk_imports.py @@ -1,4 +1,32 @@ -def test_bulk_imports(gl, group): +import time + +import pytest + +import gitlab + + +@pytest.fixture +def bulk_import_enabled(gl: gitlab.Gitlab): + settings = gl.settings.get() + bulk_import_default = settings.bulk_import_enabled + + settings.bulk_import_enabled = True + settings.save() + + # todo: why so fussy with feature flag timing? + time.sleep(5) + get_settings = gl.settings.get() + assert get_settings.bulk_import_enabled is True + + yield settings + + settings.bulk_import_enabled = bulk_import_default + settings.save() + + +# https://github.com/python-gitlab/python-gitlab/pull/2790#pullrequestreview-1873617123 +@pytest.mark.xfail(reason="Bulk Imports to be worked on in a follow up") +def test_bulk_imports(gl, group, bulk_import_enabled): destination = f"{group.full_path}-import" configuration = { "url": gl.url, diff --git a/tests/functional/api/test_current_user.py b/tests/functional/api/test_current_user.py index b12145e48..561dbe4b0 100644 --- a/tests/functional/api/test_current_user.py +++ b/tests/functional/api/test_current_user.py @@ -4,7 +4,6 @@ def test_current_user_email(gl): assert mail in gl.user.emails.list() mail.delete() - assert mail not in gl.user.emails.list() def test_current_user_gpg_keys(gl, GPG_KEY): @@ -14,8 +13,8 @@ def test_current_user_gpg_keys(gl, GPG_KEY): # Seems broken on the gitlab side gkey = gl.user.gpgkeys.get(gkey.id) + gkey.delete() - assert gkey not in gl.user.gpgkeys.list() def test_current_user_ssh_keys(gl, SSH_KEY): @@ -24,7 +23,6 @@ def test_current_user_ssh_keys(gl, SSH_KEY): assert key in gl.user.keys.list() key.delete() - assert key not in gl.user.keys.list() def test_current_user_status(gl): diff --git a/tests/functional/api/test_deploy_keys.py b/tests/functional/api/test_deploy_keys.py index 1fbaa18e6..ac65555cc 100644 --- a/tests/functional/api/test_deploy_keys.py +++ b/tests/functional/api/test_deploy_keys.py @@ -7,5 +7,5 @@ def test_project_deploy_keys(gl, project, DEPLOY_KEY): assert deploy_key in project2.keys.list() project2.keys.delete(deploy_key.id) - assert deploy_key not in project2.keys.list() + project2.delete() diff --git a/tests/functional/api/test_deploy_tokens.py b/tests/functional/api/test_deploy_tokens.py index 538dabe53..0b506e078 100644 --- a/tests/functional/api/test_deploy_tokens.py +++ b/tests/functional/api/test_deploy_tokens.py @@ -1,9 +1,13 @@ +import datetime + + def test_project_deploy_tokens(gl, project): + today = datetime.date.today().isoformat() deploy_token = project.deploytokens.create( { "name": "foo", "username": "bar", - "expires_at": "2022-01-01", + "expires_at": today, "scopes": ["read_registry"], } ) @@ -12,13 +16,11 @@ def test_project_deploy_tokens(gl, project): deploy_token = project.deploytokens.get(deploy_token.id) assert deploy_token.name == "foo" - assert deploy_token.expires_at == "2022-01-01T00:00:00.000Z" + assert deploy_token.expires_at == f"{today}T00:00:00.000Z" assert deploy_token.scopes == ["read_registry"] assert deploy_token.username == "bar" deploy_token.delete() - assert deploy_token not in project.deploytokens.list() - assert deploy_token not in gl.deploytokens.list() def test_group_deploy_tokens(gl, group): @@ -37,5 +39,3 @@ def test_group_deploy_tokens(gl, group): assert deploy_token.scopes == ["read_registry"] deploy_token.delete() - assert deploy_token not in group.deploytokens.list() - assert deploy_token not in gl.deploytokens.list() diff --git a/tests/functional/api/test_epics.py b/tests/functional/api/test_epics.py index 073ca2ad7..a4f6765da 100644 --- a/tests/functional/api/test_epics.py +++ b/tests/functional/api/test_epics.py @@ -23,7 +23,6 @@ def test_epic_issues(epic, issue): assert epic.issues.list() epic_issue.delete() - assert not epic.issues.list() def test_epic_notes(epic): diff --git a/tests/functional/api/test_gitlab.py b/tests/functional/api/test_gitlab.py index 4668fa71e..af505c73b 100644 --- a/tests/functional/api/test_gitlab.py +++ b/tests/functional/api/test_gitlab.py @@ -53,7 +53,6 @@ def test_broadcast_messages(gl, get_all_kwargs): assert msg.color == "#444444" msg.delete() - assert msg not in gl.broadcastmessages.list() def test_markdown(gl): @@ -151,7 +150,6 @@ def test_hooks(gl): assert hook in gl.hooks.list() hook.delete() - assert hook not in gl.hooks.list() def test_namespaces(gl, get_all_kwargs): @@ -202,7 +200,6 @@ def test_features(gl): assert feat in gl.features.list() feat.delete() - assert feat not in gl.features.list() def test_pagination(gl, project): diff --git a/tests/functional/api/test_groups.py b/tests/functional/api/test_groups.py index ec381d594..8efc3245d 100644 --- a/tests/functional/api/test_groups.py +++ b/tests/functional/api/test_groups.py @@ -10,7 +10,7 @@ def test_groups(gl): "email": "user@test.com", "username": "user", "name": "user", - "password": "user_pass", + "password": "E4596f8be406Bc3a14a4ccdb1df80587#!1", } ) user2 = gl.users.create( @@ -18,7 +18,7 @@ def test_groups(gl): "email": "user2@test.com", "username": "user2", "name": "user2", - "password": "user2_pass", + "password": "E4596f8be406Bc3a14a4ccdb1df80587#!#2", } ) group1 = gl.groups.create( @@ -105,8 +105,9 @@ def test_groups(gl): assert result[0].id == user.id group1.members.delete(user.id) - assert user not in group1.members.list() + assert group1.members_all.list() + member = group1.members.get(user2.id) member.access_level = gitlab.const.AccessLevel.OWNER member.save() @@ -135,7 +136,6 @@ def test_group_labels(group): assert label.name == "Label:that requires:encoding" label.delete() - assert label not in group.labels.list() @pytest.mark.gitlab_premium @@ -194,7 +194,6 @@ def test_group_badges(group): assert badge.image_url == "http://another.example.com" badge.delete() - assert badge not in group.badges.list() def test_group_milestones(group): @@ -228,7 +227,6 @@ def test_group_custom_attributes(gl, group): assert attr in group.customattributes.list() attr.delete() - assert attr not in group.customattributes.list() def test_group_subgroups_projects(gl, user): @@ -270,7 +268,6 @@ def test_group_wiki(group): wiki.save() wiki.delete() - assert wiki not in group.wikis.list() @pytest.mark.gitlab_premium @@ -285,7 +282,6 @@ def test_group_hooks(group): assert hook.note_events is True hook.delete() - assert hook not in group.hooks.list() def test_group_transfer(gl, group): diff --git a/tests/functional/api/test_import_export.py b/tests/functional/api/test_import_export.py index 8f9db9c60..6f70a810a 100644 --- a/tests/functional/api/test_import_export.py +++ b/tests/functional/api/test_import_export.py @@ -5,6 +5,7 @@ import gitlab +# https://github.com/python-gitlab/python-gitlab/pull/2790#pullrequestreview-1873617123 def test_group_import_export(gl, group, temp_dir): export = group.exports.create() assert export.message == "202 Accepted" @@ -31,6 +32,8 @@ def test_group_import_export(gl, group, temp_dir): assert group_import.name == import_name +# https://github.com/python-gitlab/python-gitlab/pull/2790#pullrequestreview-1873617123 +@pytest.mark.xfail(reason="test_project_import_export to be worked on in a follow up") def test_project_import_export(gl, project, temp_dir): export = project.exports.create() assert export.message == "202 Accepted" @@ -68,6 +71,8 @@ def test_project_import_export(gl, project, temp_dir): raise Exception("Project import taking too much time") +# https://github.com/python-gitlab/python-gitlab/pull/2790#pullrequestreview-1873617123 +@pytest.mark.xfail(reason="test_project_remote_import to be worked on in a follow up") def test_project_remote_import(gl): with pytest.raises(gitlab.exceptions.GitlabImportError) as err_info: gl.projects.remote_import( @@ -80,6 +85,10 @@ def test_project_remote_import(gl): ) +# https://github.com/python-gitlab/python-gitlab/pull/2790#pullrequestreview-1873617123 +@pytest.mark.xfail( + reason="test_project_remote_import_s3 to be worked on in a follow up" +) def test_project_remote_import_s3(gl): gl.features.set("import_project_from_remote_file_s3", True) with pytest.raises(gitlab.exceptions.GitlabImportError) as err_info: diff --git a/tests/functional/api/test_issues.py b/tests/functional/api/test_issues.py index 1a373bd96..98825d027 100644 --- a/tests/functional/api/test_issues.py +++ b/tests/functional/api/test_issues.py @@ -18,8 +18,6 @@ def test_create_issue(project): assert issue in project.issues.list(state="opened") assert issue2 in project.issues.list(state="closed") - 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 @@ -33,10 +31,7 @@ def test_issue_notes(issue): assert emoji in note.awardemojis.list() emoji.delete() - assert emoji not in note.awardemojis.list() - note.delete() - assert note not in issue.notes.list() def test_issue_labels(project, issue): @@ -62,8 +57,8 @@ def test_issue_links(project, issue): assert links link_id = links[0].issue_link_id + issue.links.delete(link_id) - assert not issue.links.list() def test_issue_label_events(issue): @@ -114,5 +109,3 @@ def test_issue_discussions(issue): 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_lazy_objects.py b/tests/functional/api/test_lazy_objects.py index cd149b422..607a63648 100644 --- a/tests/functional/api/test_lazy_objects.py +++ b/tests/functional/api/test_lazy_objects.py @@ -1,3 +1,5 @@ +import time + import pytest import gitlab @@ -27,9 +29,10 @@ def test_save_after_lazy_get_with_path(project, lazy_project): assert lazy_project.description == "A new description" -def test_delete_after_lazy_get_with_path(gl, group, wait_for_sidekiq): +def test_delete_after_lazy_get_with_path(gl, group): project = gl.projects.create({"name": "lazy_project", "namespace_id": group.id}) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) lazy_project = gl.projects.get(project.path_with_namespace, lazy=True) lazy_project.delete() diff --git a/tests/functional/api/test_merge_requests.py b/tests/functional/api/test_merge_requests.py index 7557c6ac9..fe54abd94 100644 --- a/tests/functional/api/test_merge_requests.py +++ b/tests/functional/api/test_merge_requests.py @@ -1,3 +1,4 @@ +import datetime import time import pytest @@ -16,7 +17,7 @@ def test_merge_requests(project): } ) - source_branch = "branch1" + source_branch = "branch-merge-request-api" project.branches.create({"branch": source_branch, "ref": "main"}) project.files.create( @@ -28,7 +29,7 @@ def test_merge_requests(project): } ) project.mergerequests.create( - {"source_branch": "branch1", "target_branch": "main", "title": "MR readme2"} + {"source_branch": source_branch, "target_branch": "main", "title": "MR readme2"} ) @@ -72,8 +73,6 @@ def test_merge_request_discussion(project): 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): @@ -164,27 +163,33 @@ def test_project_merge_request_approval_rules(group, project): assert approval_rules[0].approvals_required == 2 approval_rules[0].delete() - ars = project.approvalrules.list(get_all=True) - assert len(ars) == 0 -def test_merge_request_reset_approvals(gitlab_url, project, wait_for_sidekiq): - bot = project.access_tokens.create({"name": "bot", "scopes": ["api"]}) +def test_merge_request_reset_approvals(gitlab_url, project): + today = datetime.date.today() + future_date = today + datetime.timedelta(days=4) + bot = project.access_tokens.create( + {"name": "bot", "scopes": ["api"], "expires_at": future_date.isoformat()} + ) + bot_gitlab = gitlab.Gitlab(gitlab_url, private_token=bot.token) bot_project = bot_gitlab.projects.get(project.id, lazy=True) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) + mr = bot_project.mergerequests.list()[0] # type: ignore[index] + assert mr.reset_approvals() -def test_cancel_merge_when_pipeline_succeeds( - project, merge_request_with_pipeline, wait_for_sidekiq -): - wait_for_sidekiq(timeout=60) +def test_cancel_merge_when_pipeline_succeeds(project, merge_request_with_pipeline): + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Set to merge when the pipeline succeeds, which should never happen merge_request_with_pipeline.merge(merge_when_pipeline_succeeds=True) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) mr = project.mergerequests.get(merge_request_with_pipeline.iid) assert mr.merged_at is None @@ -193,9 +198,10 @@ def test_cancel_merge_when_pipeline_succeeds( assert cancel == {"status": "success"} -def test_merge_request_merge(project, merge_request, wait_for_sidekiq): +def test_merge_request_merge(project, merge_request): merge_request.merge() - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) mr = project.mergerequests.get(merge_request.iid) assert mr.merged_at is not None @@ -205,15 +211,14 @@ def test_merge_request_merge(project, merge_request, wait_for_sidekiq): mr.merge() -def test_merge_request_should_remove_source_branch( - project, merge_request, wait_for_sidekiq -) -> None: +def test_merge_request_should_remove_source_branch(project, merge_request) -> 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""" merge_request.merge(should_remove_source_branch=True) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Wait until it is merged mr = None @@ -227,7 +232,8 @@ def test_merge_request_should_remove_source_branch( assert mr is not None assert mr.merged_at is not None time.sleep(0.5) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Ensure we can NOT get the MR branch with pytest.raises(gitlab.exceptions.GitlabGetError): @@ -240,9 +246,7 @@ def test_merge_request_should_remove_source_branch( print("result:", pprint.pformat(result)) -def test_merge_request_large_commit_message( - project, merge_request, wait_for_sidekiq -) -> None: +def test_merge_request_large_commit_message(project, merge_request) -> 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 @@ -255,7 +259,8 @@ def test_merge_request_large_commit_message( merge_commit_message=merge_commit_message, should_remove_source_branch=False ) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Wait until it is merged mr = None @@ -279,9 +284,7 @@ def test_merge_request_merge_ref(merge_request) -> None: assert response and "commit_id" in response -def test_merge_request_merge_ref_should_fail( - project, merge_request, wait_for_sidekiq -) -> None: +def test_merge_request_merge_ref_should_fail(project, merge_request) -> None: # Create conflict project.files.create( { @@ -291,7 +294,8 @@ def test_merge_request_merge_ref_should_fail( "commit_message": "Another commit in main branch", } ) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Check for non-existing merge_ref for MR with conflicts with pytest.raises(gitlab.exceptions.GitlabGetError): diff --git a/tests/functional/api/test_projects.py b/tests/functional/api/test_projects.py index 77bb8348b..75d402a0d 100644 --- a/tests/functional/api/test_projects.py +++ b/tests/functional/api/test_projects.py @@ -1,3 +1,4 @@ +import time import uuid import pytest @@ -45,7 +46,6 @@ def test_project_members(user, project): assert member.access_level == 30 member.delete() - assert member not in project.members.list() def test_project_badges(project): @@ -62,7 +62,6 @@ def test_project_badges(project): assert badge.image_url == "http://another.example.com" badge.delete() - assert badge not in project.badges.list() @pytest.mark.skip(reason="Commented out in legacy test") @@ -78,7 +77,6 @@ def test_project_boards(project): last_list.save() last_list.delete() - assert last_list not in board.lists.list() def test_project_custom_attributes(gl, project): @@ -97,7 +95,6 @@ def test_project_custom_attributes(gl, project): assert attr in project.customattributes.list() attr.delete() - assert attr not in project.customattributes.list() def test_project_environments(project): @@ -115,8 +112,8 @@ def test_project_environments(project): assert environment.external_url == "http://new.env/whatever" environment.stop() + environment.delete() - assert environment not in project.environments.list() def test_project_events(project): @@ -156,7 +153,6 @@ def test_project_hooks(project): assert hook.note_events is True hook.delete() - assert hook not in project.hooks.list() def test_project_housekeeping(project): @@ -184,7 +180,6 @@ def test_project_labels(project): assert label.subscribed is False label.delete() - assert label not in project.labels.list() def test_project_label_promotion(gl, group): @@ -206,7 +201,6 @@ def test_project_label_promotion(gl, group): assert any(label.name == label_name for label in group.labels.list()) group.labels.delete(label_name) - assert not any(label.name == label_name for label in group.labels.list()) def test_project_milestones(project): @@ -255,10 +249,9 @@ def test_project_pages_domains(gl, project): assert domain.domain == "foo.domain.com" domain.delete() - assert domain not in project.pagesdomains.list() -def test_project_protected_branches(project, wait_for_sidekiq, gitlab_version): +def test_project_protected_branches(project, gitlab_version): # Updating a protected branch is possible from Gitlab 15.6 # https://docs.gitlab.com/ee/api/protected_branches.html#update-a-protected-branch can_update_prot_branch = gitlab_version.major > 15 or ( @@ -278,13 +271,14 @@ def test_project_protected_branches(project, wait_for_sidekiq, gitlab_version): if can_update_prot_branch: p_b.allow_force_push = True p_b.save() - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) p_b = project.protectedbranches.get("*-stable") if can_update_prot_branch: assert p_b.allow_force_push - p_b.delete() - assert p_b not in project.protectedbranches.list() + + p_b.delete() def test_project_remote_mirrors(project): @@ -319,9 +313,6 @@ def test_project_services(project): service.delete() - service = project.services.get("asana") - assert service.active is False - def test_project_stars(project): project.star() @@ -342,7 +333,6 @@ def test_project_tags(project, project_file): assert tag in project.tags.list() tag.delete() - assert tag not in project.tags.list() def test_project_triggers(project): @@ -350,7 +340,6 @@ def test_project_triggers(project): assert trigger in project.triggers.list() trigger.delete() - assert trigger not in project.triggers.list() def test_project_wiki(project): @@ -364,8 +353,8 @@ def test_project_wiki(project): # update and delete seem broken wiki.content = "new content" wiki.save() + wiki.delete() - assert wiki not in project.wikis.list() def test_project_groups_list(gl, group): diff --git a/tests/functional/api/test_releases.py b/tests/functional/api/test_releases.py index c52e396c1..33b059c04 100644 --- a/tests/functional/api/test_releases.py +++ b/tests/functional/api/test_releases.py @@ -52,7 +52,6 @@ def test_update_save_project_release(project, release): 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): diff --git a/tests/functional/api/test_repository.py b/tests/functional/api/test_repository.py index dd70f10b1..03cca583b 100644 --- a/tests/functional/api/test_repository.py +++ b/tests/functional/api/test_repository.py @@ -158,10 +158,8 @@ def test_commit_discussion(project): 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): diff --git a/tests/functional/api/test_services.py b/tests/functional/api/test_services.py index 51805ef37..ce9503080 100644 --- a/tests/functional/api/test_services.py +++ b/tests/functional/api/test_services.py @@ -32,7 +32,5 @@ def test_get_service(project, service): def test_delete_service(project, service): service_object = project.services.get(service["slug"]) - service_object.delete() - service_object = project.services.get(service["slug"]) - assert not service_object.active + service_object.delete() diff --git a/tests/functional/api/test_snippets.py b/tests/functional/api/test_snippets.py index b6b1f0123..b0ea54d1d 100644 --- a/tests/functional/api/test_snippets.py +++ b/tests/functional/api/test_snippets.py @@ -1,3 +1,5 @@ +import pytest + import gitlab @@ -20,10 +22,8 @@ def test_snippets(gl): content = snippet.content() assert content.decode() == "import gitlab" - assert snippet.user_agent_detail()["user_agent"] snippet.delete() - assert snippet not in gl.snippets.list(get_all=True) def test_project_snippets(project): @@ -38,7 +38,16 @@ def test_project_snippets(project): } ) - assert snippet.user_agent_detail()["user_agent"] + assert snippet.title == "snip1" + + +@pytest.mark.xfail(reason="Returning 404 UserAgentDetail not found in GL 16") +def test_project_snippet_user_agent_detail(project): + snippet = project.snippets.list()[0] + + user_agent_detail = snippet.user_agent_detail() + + assert user_agent_detail["user_agent"] def test_project_snippet_discussion(project): @@ -56,8 +65,6 @@ def test_project_snippet_discussion(project): 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): @@ -71,4 +78,3 @@ def test_project_snippet_file(project): assert snippet in project.snippets.list() snippet.delete() - assert snippet not in project.snippets.list() diff --git a/tests/functional/api/test_topics.py b/tests/functional/api/test_topics.py index 7777725a9..1fb7c8d63 100644 --- a/tests/functional/api/test_topics.py +++ b/tests/functional/api/test_topics.py @@ -31,4 +31,3 @@ def test_topics(gl, gitlab_version): assert merged_topic["id"] == topic2.id topic2.delete() - assert not gl.topics.list() diff --git a/tests/functional/api/test_users.py b/tests/functional/api/test_users.py index 9c300365b..58c90c646 100644 --- a/tests/functional/api/test_users.py +++ b/tests/functional/api/test_users.py @@ -4,6 +4,9 @@ https://docs.gitlab.com/ee/api/users.html#delete-authentication-identity-from-user """ +import datetime +import time + import requests @@ -13,7 +16,7 @@ def test_create_user(gl, fixture_dir): "email": "foo@bar.com", "username": "foo", "name": "foo", - "password": "foo_password", + "password": "E4596f8be406Bc3a14a4ccdb1df80587$3", "avatar": open(fixture_dir / "avatar.png", "rb"), } ) @@ -60,20 +63,20 @@ def test_ban_user(gl, user): assert retrieved_user.state == "active" -def test_delete_user(gl, wait_for_sidekiq): +def test_delete_user(gl): new_user = gl.users.create( { "email": "delete-user@test.com", "username": "delete-user", "name": "delete-user", - "password": "delete-user-pass", + "password": "E4596f8be406Bc3a14a4ccdb1df80587#15", } ) - new_user.delete() - wait_for_sidekiq(timeout=60) + # We don't need to validate Gitlab's behaviour by checking if user is present after a delay etc, + # just that python-gitlab acted correctly to produce a 2xx from Gitlab - assert new_user.id not in [user.id for user in gl.users.list()] + new_user.delete() def test_user_projects_list(gl, user): @@ -101,7 +104,7 @@ def test_list_multiple_users(gl, user): "email": second_email, "username": second_username, "name": "Foo Bar", - "password": "foobar_password", + "password": "E4596f8be406Bc3a14a4ccdb1df80587#!", } ) assert gl.users.list(search=second_user.username)[0].id == second_user.id @@ -117,11 +120,7 @@ def test_user_gpg_keys(gl, user, GPG_KEY): gkey = user.gpgkeys.create({"key": GPG_KEY}) assert gkey in user.gpgkeys.list() - # Seems broken on the gitlab side - # gkey = user.gpgkeys.get(gkey.id) - gkey.delete() - assert gkey not in user.gpgkeys.list() def test_user_ssh_keys(gl, user, SSH_KEY): @@ -132,7 +131,6 @@ def test_user_ssh_keys(gl, user, SSH_KEY): assert get_key.key == key.key key.delete() - assert key not in user.keys.list() def test_user_email(gl, user): @@ -140,37 +138,45 @@ def test_user_email(gl, user): assert email in user.emails.list() email.delete() - assert email not in user.emails.list() def test_user_custom_attributes(gl, user): - attrs = user.customattributes.list() - assert not attrs + user.customattributes.list() attr = user.customattributes.set("key", "value1") - assert user in gl.users.list(custom_attributes={"key": "value1"}) + users_with_attribute = gl.users.list(custom_attributes={"key": "value1"}) + + assert user in users_with_attribute + assert attr.key == "key" assert attr.value == "value1" assert attr in user.customattributes.list() - attr = user.customattributes.set("key", "value2") - attr = user.customattributes.get("key") - assert attr.value == "value2" - assert attr in user.customattributes.list() + user.customattributes.set("key", "value2") + attr_2 = user.customattributes.get("key") + assert attr_2.value == "value2" + assert attr_2 in user.customattributes.list() - attr.delete() - assert attr not in user.customattributes.list() + attr_2.delete() def test_user_impersonation_tokens(gl, user): + today = datetime.date.today() + future_date = today + datetime.timedelta(days=4) + token = user.impersonationtokens.create( - {"name": "token1", "scopes": ["api", "read_user"]} + { + "name": "user_impersonation_token", + "scopes": ["api", "read_user"], + "expires_at": future_date.isoformat(), + } ) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(30) + assert token in user.impersonationtokens.list(state="active") token.delete() - assert token not in user.impersonationtokens.list(state="active") - assert token in user.impersonationtokens.list(state="inactive") def test_user_identities(gl, user): @@ -182,5 +188,3 @@ def test_user_identities(gl, user): 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 index 867e563a3..eeed51da7 100644 --- a/tests/functional/api/test_variables.py +++ b/tests/functional/api/test_variables.py @@ -17,7 +17,6 @@ def test_instance_variables(gl): assert variable.value == "new_value1" variable.delete() - assert variable not in gl.variables.list() def test_group_variables(group): @@ -31,7 +30,6 @@ def test_group_variables(group): assert variable.value == "new_value1" variable.delete() - assert variable not in group.variables.list() def test_project_variables(project): @@ -45,4 +43,3 @@ def test_project_variables(project): assert variable.value == "new_value1" variable.delete() - assert variable not in project.variables.list() diff --git a/tests/functional/cli/test_cli_repository.py b/tests/functional/cli/test_cli_repository.py index 2726d34ec..d6bd1d2e4 100644 --- a/tests/functional/cli/test_cli_repository.py +++ b/tests/functional/cli/test_cli_repository.py @@ -31,9 +31,13 @@ def test_list_all_commits(gitlab_cli, project): data = { "branch": "new-branch", "start_branch": "main", - "commit_message": "New commit on new branch", + "commit_message": "chore: test commit on new branch", "actions": [ - {"action": "create", "file_path": "new-file", "content": "new content"} + { + "action": "create", + "file_path": "test-cli-repo.md", + "content": "new content", + } ], } commit = project.commits.create(data) @@ -72,12 +76,16 @@ def test_list_merge_request_commits(gitlab_cli, merge_request, project): assert ret.stdout -def test_commit_merge_requests(gitlab_cli, project, merge_request, wait_for_sidekiq): +def test_commit_merge_requests(gitlab_cli, project, merge_request): """This tests the `project-commit merge-requests` command and also tests that we can print the result using the `json` formatter""" - # Merge the MR first + + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(30) + merge_result = merge_request.merge(should_remove_source_branch=True) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) # Wait until it is merged mr = None @@ -90,8 +98,8 @@ def test_commit_merge_requests(gitlab_cli, project, merge_request, wait_for_side assert mr is not None assert mr.merged_at is not None - time.sleep(0.5) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) commit_sha = merge_result["sha"] cmd = [ diff --git a/tests/functional/cli/test_cli_resource_access_tokens.py b/tests/functional/cli/test_cli_resource_access_tokens.py index 85136b3de..c080749b5 100644 --- a/tests/functional/cli/test_cli_resource_access_tokens.py +++ b/tests/functional/cli/test_cli_resource_access_tokens.py @@ -1,4 +1,4 @@ -import pytest +import datetime def test_list_project_access_tokens(gitlab_cli, project): @@ -18,13 +18,14 @@ def test_create_project_access_token_with_scopes(gitlab_cli, project): "test-token", "--scopes", "api,read_repository", + "--expires-at", + datetime.date.today().isoformat(), ] ret = gitlab_cli(cmd) assert ret.success -@pytest.mark.skip(reason="Requires GitLab 14.7") def test_list_group_access_tokens(gitlab_cli, group): cmd = ["group-access-token", "list", "--group-id", group.id] ret = gitlab_cli(cmd) @@ -42,6 +43,8 @@ def test_create_group_access_token_with_scopes(gitlab_cli, group): "test-token", "--scopes", "api,read_repository", + "--expires-at", + datetime.date.today().isoformat(), ] ret = gitlab_cli(cmd) diff --git a/tests/functional/cli/test_cli_users.py b/tests/functional/cli/test_cli_users.py index 8bf2fbcd4..fd1942ae1 100644 --- a/tests/functional/cli/test_cli_users.py +++ b/tests/functional/cli/test_cli_users.py @@ -1,3 +1,6 @@ +import datetime + + def test_create_user_impersonation_token_with_scopes(gitlab_cli, user): cmd = [ "user-impersonation-token", @@ -8,6 +11,8 @@ def test_create_user_impersonation_token_with_scopes(gitlab_cli, user): "test-token", "--scopes", "api,read_user", + "--expires-at", + datetime.date.today().isoformat(), ] ret = gitlab_cli(cmd) diff --git a/tests/functional/cli/test_cli_v4.py b/tests/functional/cli/test_cli_v4.py index a5b989700..4a0d07a08 100644 --- a/tests/functional/cli/test_cli_v4.py +++ b/tests/functional/cli/test_cli_v4.py @@ -1,6 +1,9 @@ +import datetime import os import time +branch = "BRANCH-cli-v4" + def test_create_project(gitlab_cli): name = "test-project1" @@ -22,28 +25,6 @@ def test_update_project(gitlab_cli, project): assert description in ret.stdout -def test_create_ci_lint(gitlab_cli, valid_gitlab_ci_yml): - cmd = ["ci-lint", "create", "--content", valid_gitlab_ci_yml] - ret = gitlab_cli(cmd) - - assert ret.success - - -def test_validate_ci_lint(gitlab_cli, valid_gitlab_ci_yml): - cmd = ["ci-lint", "validate", "--content", valid_gitlab_ci_yml] - ret = gitlab_cli(cmd) - - assert ret.success - - -def test_validate_ci_lint_invalid_exits_non_zero(gitlab_cli, invalid_gitlab_ci_yml): - cmd = ["ci-lint", "validate", "--content", invalid_gitlab_ci_yml] - ret = gitlab_cli(cmd) - - assert not ret.success - assert "CI YAML Lint failed (Invalid configuration format)" in ret.stderr - - def test_validate_project_ci_lint(gitlab_cli, project, valid_gitlab_ci_yml): cmd = [ "project-ci-lint", @@ -103,7 +84,7 @@ def test_create_user(gitlab_cli, gl): email = "fake@email.com" username = "user1" name = "User One" - password = "fakepassword" + password = "E4596f8be406Bc3a14a4ccdb1df80587" cmd = [ "user", @@ -215,8 +196,6 @@ def test_create_issue_note(gitlab_cli, issue): def test_create_branch(gitlab_cli, project): - branch = "branch1" - cmd = [ "project-branch", "create", @@ -233,7 +212,6 @@ def test_create_branch(gitlab_cli, project): def test_create_merge_request(gitlab_cli, project): - branch = "branch1" cmd = [ "project-merge-request", @@ -252,20 +230,20 @@ def test_create_merge_request(gitlab_cli, project): assert ret.success -def test_accept_request_merge(gitlab_cli, project, wait_for_sidekiq): +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", + "file_path": "test-cli-v4.md", "content": "Content", - "commit_message": "Pre-merge commit", + "commit_message": "chore: test-cli-v4 change", } project.files.create(file_data) - time.sleep(2) - wait_for_sidekiq(timeout=60) + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(30) - cmd = [ + approve_cmd = [ "project-merge-request", "merge", "--project-id", @@ -273,7 +251,7 @@ def test_accept_request_merge(gitlab_cli, project, wait_for_sidekiq): "--iid", mr.iid, ] - ret = gitlab_cli(cmd) + ret = gitlab_cli(approve_cmd) assert ret.success @@ -501,9 +479,6 @@ def test_delete_project_variable(gitlab_cli, variable): 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) @@ -590,7 +565,7 @@ def test_create_project_with_values_at_prefixed(gitlab_cli, tmpdir): def test_create_project_deploy_token(gitlab_cli, project): name = "project-token" username = "root" - expires_at = "2021-09-09" + expires_at = datetime.date.today().isoformat() scopes = "read_registry" cmd = [ @@ -666,7 +641,7 @@ def test_delete_project_deploy_token(gitlab_cli, deploy_token): def test_create_group_deploy_token(gitlab_cli, group): name = "group-token" username = "root" - expires_at = "2021-09-09" + expires_at = datetime.date.today().isoformat() scopes = "read_registry" cmd = [ diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 34b286b4d..f2f31e52f 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,4 +1,5 @@ import dataclasses +import datetime import logging import pathlib import tempfile @@ -70,17 +71,15 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None: exist.""" if helpers.get_gitlab_plan(gl): logging.info("GitLab EE detected") - # NOTE(jlvillal): By default in GitLab EE it will wait 7 days before - # deleting a group. Disable delayed group/project deletion. + # NOTE(jlvillal, timknight): By default in GitLab EE it will wait 7 days before + # deleting a group or project. + # In GL 16.0 we need to call delete with `permanently_remove=True` for projects and sub groups + # (handled in helpers.py safe_delete) settings = gl.settings.get() modified_settings = False - if settings.delayed_group_deletion: - logging.info("Setting `delayed_group_deletion` to False") - settings.delayed_group_deletion = False - modified_settings = True - if settings.delayed_project_deletion: - logging.info("Setting `delayed_project_deletion` to False") - settings.delayed_project_deletion = False + if settings.deletion_adjourned_period != 1: + logging.info("Setting `deletion_adjourned_period` to 1 Day") + settings.deletion_adjourned_period = 1 modified_settings = True if modified_settings: settings.save() @@ -122,7 +121,7 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None: for user in gl.users.list(): if user.username not in ["root", "ghost"]: logging.info(f"Deleting user: {user.username!r}") - helpers.safe_delete(user, hard_delete=True) + helpers.safe_delete(user) def set_token(container: str, fixture_dir: pathlib.Path) -> str: @@ -209,31 +208,6 @@ def _check( 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: int = 30, step: float = 0.5, allow_fail: bool = False) -> bool: - for count 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 - logging.info(f"sidekiq busy {count} of {timeout}") - assert allow_fail, "sidekiq process should have terminated but did not." - return False - - return _wait - - @pytest.fixture(scope="session") def gitlab_token( check_is_alive, @@ -376,7 +350,7 @@ def project(gl): @pytest.fixture(scope="function") -def make_merge_request(project, wait_for_sidekiq): +def make_merge_request(project): """Fixture factory used to create a merge_request. It will create a branch, add a commit to the branch, and then create a @@ -396,10 +370,11 @@ def _make_merge_request(*, source_branch: str, create_pipeline: bool = False): # 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" + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(30) project.refresh() # Gets us the current default branch + logging.info(f"Creating branch {source_branch}") mr_branch = project.branches.create( {"branch": source_branch, "ref": project.default_branch} ) @@ -413,6 +388,7 @@ def _make_merge_request(*, source_branch: str, create_pipeline: bool = False): "commit_message": "New commit in new branch", } ) + if create_pipeline: project.files.create( { @@ -436,16 +412,23 @@ def _make_merge_request(*, source_branch: str, create_pipeline: bool = False): "remove_source_branch": True, } ) - result = wait_for_sidekiq(timeout=60) - assert result is True, "sidekiq process should have terminated but did not" + + # Pause to let GL catch up (happens on hosted too, sometimes takes a while for server to be ready to merge) + time.sleep(5) mr_iid = mr.iid for _ in range(60): mr = project.mergerequests.get(mr_iid) - if mr.merge_status != "checking": + if ( + mr.detailed_merge_status == "checking" + or mr.detailed_merge_status == "unchecked" + ): + time.sleep(0.5) + else: break - time.sleep(0.5) - assert mr.merge_status != "checking" + + assert mr.detailed_merge_status != "checking" + assert mr.detailed_merge_status != "unchecked" to_delete.extend([mr, mr_branch]) return mr @@ -523,14 +506,13 @@ def user(gl): email = f"user{_id}@email.com" username = f"user{_id}" name = f"User {_id}" - password = "fakepassword" + password = "E4596f8be406Bc3a14a4ccdb1df80587" user = gl.users.create(email=email, username=username, name=name, password=password) yield user - # Use `hard_delete=True` or a 'Ghost User' may be created. - helpers.safe_delete(user, hard_delete=True) + helpers.safe_delete(user) @pytest.fixture(scope="module") @@ -599,7 +581,7 @@ def deploy_token(project): data = { "name": f"token-{_id}", "username": "root", - "expires_at": "2021-09-09", + "expires_at": datetime.date.today().isoformat(), "scopes": "read_registry", } @@ -613,7 +595,7 @@ def group_deploy_token(group): data = { "name": f"group-token-{_id}", "username": "root", - "expires_at": "2021-09-09", + "expires_at": datetime.date.today().isoformat(), "scopes": "read_registry", } diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index 449bc84a1..05d90fa61 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,2 +1,2 @@ GITLAB_IMAGE=gitlab/gitlab-ee -GITLAB_TAG=15.4.0-ee.0 +GITLAB_TAG=16.10.1-ee.0 diff --git a/tests/functional/fixtures/set_token.rb b/tests/functional/fixtures/set_token.rb index 503588b9c..26d4c0a2a 100644 --- a/tests/functional/fixtures/set_token.rb +++ b/tests/functional/fixtures/set_token.rb @@ -2,7 +2,7 @@ user = User.find_by_username('root') -token = user.personal_access_tokens.first_or_create(scopes: [:api, :sudo], name: 'default'); +token = user.personal_access_tokens.first_or_create(scopes: ['api', 'sudo'], name: 'default', expires_at: 365.days.from_now); token.set_token('python-gitlab-token'); token.save! diff --git a/tests/functional/helpers.py b/tests/functional/helpers.py index 2b95f3abf..a898aa947 100644 --- a/tests/functional/helpers.py +++ b/tests/functional/helpers.py @@ -26,11 +26,7 @@ def get_gitlab_plan(gl: gitlab.Gitlab) -> Optional[str]: return license["plan"] -def safe_delete( - object: gitlab.base.RESTObject, - *, - hard_delete: bool = False, -) -> None: +def safe_delete(object: gitlab.base.RESTObject) -> None: """Ensure the object specified can not be retrieved. If object still exists after timeout period, fail the test""" manager = object.manager @@ -43,12 +39,32 @@ def safe_delete( if index: logging.info(f"Attempt {index + 1} to delete {object!r}.") try: - if hard_delete: + if isinstance(object, gitlab.v4.objects.User): + # You can't use this option if the selected user is the sole owner of any groups + # Use `hard_delete=True` or a 'Ghost User' may be created. + # https://docs.gitlab.com/ee/api/users.html#user-deletion object.delete(hard_delete=True) + if index > 1: + # If User is the sole owner of any group it won't be deleted, + # which combined with parents group never immediately deleting in GL 16 + # we shouldn't cause test to fail if it still exists + return + elif isinstance(object, gitlab.v4.objects.Project): + # Immediately delete rather than waiting for at least 1day + # https://docs.gitlab.com/ee/api/projects.html#delete-project + object.delete(permanently_remove=True) + pass else: + # We only attempt to delete parent groups to prevent dangling sub-groups + # However parent groups can only be deleted on a delay in Gl 16 + # https://docs.gitlab.com/ee/api/groups.html#remove-group object.delete() except gitlab.exceptions.GitlabDeleteError: - logging.info(f"{object!r} already deleted.") + logging.info(f"{object!r} already deleted or scheduled for deletion.") + if isinstance(object, gitlab.v4.objects.Group): + # Parent groups can never be immediately deleted in GL 16, + # so don't cause test to fail if it still exists + return pass time.sleep(SLEEP_INTERVAL)