diff options
Diffstat (limited to 'tests')
55 files changed, 5852 insertions, 0 deletions
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/__init__.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..64df051 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,73 @@ +import pytest + +import gitlab + + +@pytest.fixture +def gl(): + return gitlab.Gitlab( + "http://localhost", + private_token="private_token", + ssl_verify=True, + api_version=4, + ) + + +# Todo: parametrize, but check what tests it's really useful for +@pytest.fixture +def gl_trailing(): + return gitlab.Gitlab( + "http://localhost/", private_token="private_token", api_version=4 + ) + + +@pytest.fixture +def default_config(tmpdir): + valid_config = """[global] + default = one + ssl_verify = true + timeout = 2 + + [one] + url = http://one.url + private_token = ABCDEF + """ + + config_path = tmpdir.join("python-gitlab.cfg") + config_path.write(valid_config) + return str(config_path) + + +@pytest.fixture +def tag_name(): + return "v1.0.0" + + +@pytest.fixture +def group(gl): + return gl.groups.get(1, lazy=True) + + +@pytest.fixture +def project(gl): + return gl.projects.get(1, lazy=True) + + +@pytest.fixture +def project_issue(project): + return project.issues.get(1, lazy=True) + + +@pytest.fixture +def project_merge_request(project): + return project.mergerequests.get(1, lazy=True) + + +@pytest.fixture +def release(project, tag_name): + return project.releases.get(tag_name, lazy=True) + + +@pytest.fixture +def user(gl): + return gl.users.get(1, lazy=True) diff --git a/tests/unit/data/todo.json b/tests/unit/data/todo.json new file mode 100644 index 0000000..93b2151 --- /dev/null +++ b/tests/unit/data/todo.json @@ -0,0 +1,75 @@ +[ + { + "id": 102, + "project": { + "id": 2, + "name": "Gitlab Ce", + "name_with_namespace": "Gitlab Org / Gitlab Ce", + "path": "gitlab-ce", + "path_with_namespace": "gitlab-org/gitlab-ce" + }, + "author": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "action_name": "marked", + "target_type": "MergeRequest", + "target": { + "id": 34, + "iid": 7, + "project_id": 2, + "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.", + "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.", + "state": "opened", + "created_at": "2016-06-17T07:49:24.419Z", + "updated_at": "2016-06-17T07:52:43.484Z", + "target_branch": "tutorials_git_tricks", + "source_branch": "DNSBL_docs", + "upvotes": 0, + "downvotes": 0, + "author": { + "name": "Maxie Medhurst", + "username": "craig_rutherford", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", + "web_url": "https://gitlab.example.com/craig_rutherford" + }, + "assignee": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root" + }, + "source_project_id": 2, + "target_project_id": 2, + "labels": [], + "work_in_progress": false, + "milestone": { + "id": 32, + "iid": 2, + "project_id": 2, + "title": "v1.0", + "description": "Assumenda placeat ea voluptatem voluptate qui.", + "state": "active", + "created_at": "2016-06-17T07:47:34.163Z", + "updated_at": "2016-06-17T07:47:34.163Z", + "due_date": null + }, + "merge_when_pipeline_succeeds": false, + "merge_status": "cannot_be_merged", + "subscribed": true, + "user_notes_count": 7 + }, + "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7", + "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.", + "state": "pending", + "created_at": "2016-06-17T07:52:35.225Z" + } +] diff --git a/tests/unit/mixins/test_meta_mixins.py b/tests/unit/mixins/test_meta_mixins.py new file mode 100644 index 0000000..4c8845b --- /dev/null +++ b/tests/unit/mixins/test_meta_mixins.py @@ -0,0 +1,58 @@ +from gitlab.mixins import ( + CreateMixin, + CRUDMixin, + DeleteMixin, + GetMixin, + ListMixin, + NoUpdateMixin, + RetrieveMixin, + UpdateMixin, +) + + +def test_retrieve_mixin(): + class M(RetrieveMixin): + pass + + obj = M() + assert hasattr(obj, "list") + assert hasattr(obj, "get") + assert not hasattr(obj, "create") + assert not hasattr(obj, "update") + assert not hasattr(obj, "delete") + assert isinstance(obj, ListMixin) + assert isinstance(obj, GetMixin) + + +def test_crud_mixin(): + class M(CRUDMixin): + pass + + obj = M() + assert hasattr(obj, "get") + assert hasattr(obj, "list") + assert hasattr(obj, "create") + assert hasattr(obj, "update") + assert hasattr(obj, "delete") + assert isinstance(obj, ListMixin) + assert isinstance(obj, GetMixin) + assert isinstance(obj, CreateMixin) + assert isinstance(obj, UpdateMixin) + assert isinstance(obj, DeleteMixin) + + +def test_no_update_mixin(): + class M(NoUpdateMixin): + pass + + obj = M() + assert hasattr(obj, "get") + assert hasattr(obj, "list") + assert hasattr(obj, "create") + assert not hasattr(obj, "update") + assert hasattr(obj, "delete") + assert isinstance(obj, ListMixin) + assert isinstance(obj, GetMixin) + assert isinstance(obj, CreateMixin) + assert not isinstance(obj, UpdateMixin) + assert isinstance(obj, DeleteMixin) diff --git a/tests/unit/mixins/test_mixin_methods.py b/tests/unit/mixins/test_mixin_methods.py new file mode 100644 index 0000000..626230e --- /dev/null +++ b/tests/unit/mixins/test_mixin_methods.py @@ -0,0 +1,300 @@ +import pytest +from httmock import HTTMock, response, urlmatch # noqa + +from gitlab import base +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + GetMixin, + GetWithoutIdMixin, + ListMixin, + RefreshMixin, + SaveMixin, + SetMixin, + UpdateMixin, +) + + +class FakeObject(base.RESTObject): + pass + + +class FakeManager(base.RESTManager): + _path = "/tests" + _obj_cls = FakeObject + + +def test_get_mixin(gl): + class M(GetMixin, FakeManager): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = mgr.get(42) + assert isinstance(obj, FakeObject) + assert obj.foo == "bar" + assert obj.id == 42 + + +def test_refresh_mixin(gl): + class TestClass(RefreshMixin, FakeObject): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="get") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = FakeManager(gl) + obj = TestClass(mgr, {"id": 42}) + res = obj.refresh() + assert res is None + assert obj.foo == "bar" + assert obj.id == 42 + + +def test_get_without_id_mixin(gl): + class M(GetWithoutIdMixin, FakeManager): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"foo": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = mgr.get() + assert isinstance(obj, FakeObject) + assert obj.foo == "bar" + assert not hasattr(obj, "id") + + +def test_list_mixin(gl): + class M(ListMixin, FakeManager): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '[{"id": 42, "foo": "bar"},{"id": 43, "foo": "baz"}]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + # test RESTObjectList + mgr = M(gl) + obj_list = mgr.list(as_list=False) + assert isinstance(obj_list, base.RESTObjectList) + for obj in obj_list: + assert isinstance(obj, FakeObject) + assert obj.id in (42, 43) + + # test list() + obj_list = mgr.list(all=True) + assert isinstance(obj_list, list) + assert obj_list[0].id == 42 + assert obj_list[1].id == 43 + assert isinstance(obj_list[0], FakeObject) + assert len(obj_list) == 2 + + +def test_list_other_url(gl): + class M(ListMixin, FakeManager): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/others", method="get") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '[{"id": 42, "foo": "bar"}]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj_list = mgr.list(path="/others", as_list=False) + assert isinstance(obj_list, base.RESTObjectList) + obj = obj_list.next() + assert obj.id == 42 + assert obj.foo == "bar" + with pytest.raises(StopIteration): + obj_list.next() + + +def test_create_mixin_missing_attrs(gl): + class M(CreateMixin, FakeManager): + _create_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + + mgr = M(gl) + data = {"foo": "bar", "baz": "blah"} + mgr._check_missing_create_attrs(data) + + data = {"baz": "blah"} + with pytest.raises(AttributeError) as error: + mgr._check_missing_create_attrs(data) + assert "foo" in str(error.value) + + +def test_create_mixin(gl): + class M(CreateMixin, FakeManager): + _create_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",)) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="post") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = mgr.create({"foo": "bar"}) + assert isinstance(obj, FakeObject) + assert obj.id == 42 + assert obj.foo == "bar" + + +def test_create_mixin_custom_path(gl): + class M(CreateMixin, FakeManager): + _create_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",)) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/others", method="post") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = mgr.create({"foo": "bar"}, path="/others") + assert isinstance(obj, FakeObject) + assert obj.id == 42 + assert obj.foo == "bar" + + +def test_update_mixin_missing_attrs(gl): + class M(UpdateMixin, FakeManager): + _update_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + + mgr = M(gl) + data = {"foo": "bar", "baz": "blah"} + mgr._check_missing_update_attrs(data) + + data = {"baz": "blah"} + with pytest.raises(AttributeError) as error: + mgr._check_missing_update_attrs(data) + assert "foo" in str(error.value) + + +def test_update_mixin(gl): + class M(UpdateMixin, FakeManager): + _create_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",)) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "baz"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + server_data = mgr.update(42, {"foo": "baz"}) + assert isinstance(server_data, dict) + assert server_data["id"] == 42 + assert server_data["foo"] == "baz" + + +def test_update_mixin_no_id(gl): + class M(UpdateMixin, FakeManager): + _create_attrs = base.RequiredOptional( + required=("foo",), optional=("bar", "baz") + ) + _update_attrs = base.RequiredOptional(required=("foo",), optional=("bam",)) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="put") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"foo": "baz"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + server_data = mgr.update(new_data={"foo": "baz"}) + assert isinstance(server_data, dict) + assert server_data["foo"] == "baz" + + +def test_delete_mixin(gl): + class M(DeleteMixin, FakeManager): + pass + + @urlmatch( + scheme="http", netloc="localhost", path="/api/v4/tests/42", method="delete" + ) + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = "" + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + mgr.delete(42) + + +def test_save_mixin(gl): + class M(UpdateMixin, FakeManager): + pass + + class TestClass(SaveMixin, base.RESTObject): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/42", method="put") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"id": 42, "foo": "baz"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = TestClass(mgr, {"id": 42, "foo": "bar"}) + obj.foo = "baz" + obj.save() + assert obj._attrs["foo"] == "baz" + assert obj._updated_attrs == {} + + +def test_set_mixin(gl): + class M(SetMixin, FakeManager): + pass + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests/foo", method="put") + def resp_cont(url, request): + headers = {"Content-Type": "application/json"} + content = '{"key": "foo", "value": "bar"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + mgr = M(gl) + obj = mgr.set("foo", "bar") + assert isinstance(obj, FakeObject) + assert obj.key == "foo" + assert obj.value == "bar" diff --git a/tests/unit/mixins/test_object_mixins_attributes.py b/tests/unit/mixins/test_object_mixins_attributes.py new file mode 100644 index 0000000..d54fa3a --- /dev/null +++ b/tests/unit/mixins/test_object_mixins_attributes.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 Mika Mäenpää <mika.j.maenpaa@tut.fi>, +# Tampere University of Technology +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from gitlab.mixins import ( + AccessRequestMixin, + SetMixin, + SubscribableMixin, + TimeTrackingMixin, + TodoMixin, + UserAgentDetailMixin, +) + + +def test_access_request_mixin(): + class TestClass(AccessRequestMixin): + pass + + obj = TestClass() + assert hasattr(obj, "approve") + + +def test_subscribable_mixin(): + class TestClass(SubscribableMixin): + pass + + obj = TestClass() + assert hasattr(obj, "subscribe") + assert hasattr(obj, "unsubscribe") + + +def test_todo_mixin(): + class TestClass(TodoMixin): + pass + + obj = TestClass() + assert hasattr(obj, "todo") + + +def test_time_tracking_mixin(): + class TestClass(TimeTrackingMixin): + pass + + obj = TestClass() + assert hasattr(obj, "time_stats") + assert hasattr(obj, "time_estimate") + assert hasattr(obj, "reset_time_estimate") + assert hasattr(obj, "add_spent_time") + assert hasattr(obj, "reset_spent_time") + + +def test_set_mixin(): + class TestClass(SetMixin): + pass + + obj = TestClass() + assert hasattr(obj, "set") + + +def test_user_agent_detail_mixin(): + class TestClass(UserAgentDetailMixin): + pass + + obj = TestClass() + assert hasattr(obj, "user_agent_detail") diff --git a/tests/unit/objects/__init__.py b/tests/unit/objects/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/objects/__init__.py diff --git a/tests/unit/objects/conftest.py b/tests/unit/objects/conftest.py new file mode 100644 index 0000000..d8a40d9 --- /dev/null +++ b/tests/unit/objects/conftest.py @@ -0,0 +1,70 @@ +"""Common mocks for resources in gitlab.v4.objects""" + +import re + +import pytest +import responses + + +@pytest.fixture +def binary_content(): + return b"binary content" + + +@pytest.fixture +def accepted_content(): + return {"message": "202 Accepted"} + + +@pytest.fixture +def created_content(): + return {"message": "201 Created"} + + +@pytest.fixture +def no_content(): + return {"message": "204 No Content"} + + +@pytest.fixture +def resp_export(accepted_content, binary_content): + """Common fixture for group and project exports.""" + export_status_content = { + "id": 1, + "description": "Itaque perspiciatis minima aspernatur", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "created_at": "2017-08-29T04:36:44.383Z", + "export_status": "finished", + "_links": { + "api_url": "https://gitlab.test/api/v4/projects/1/export/download", + "web_url": "https://gitlab.test/gitlab-test/download_export", + }, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.POST, + url=re.compile(r".*/api/v4/(groups|projects)/1/export"), + json=accepted_content, + content_type="application/json", + status=202, + ) + rsps.add( + method=responses.GET, + url=re.compile(r".*/api/v4/(groups|projects)/1/export/download"), + body=binary_content, + content_type="application/octet-stream", + status=200, + ) + # Currently only project export supports status checks + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/export", + json=export_status_content, + content_type="application/json", + status=200, + ) + yield rsps diff --git a/tests/unit/objects/test_appearance.py b/tests/unit/objects/test_appearance.py new file mode 100644 index 0000000..0de6524 --- /dev/null +++ b/tests/unit/objects/test_appearance.py @@ -0,0 +1,65 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/appearance.html +""" + +import pytest +import responses + +title = "GitLab Test Instance" +description = "gitlab-test.example.com" +new_title = "new-title" +new_description = "new-description" + + +@pytest.fixture +def resp_application_appearance(): + content = { + "title": title, + "description": description, + "logo": "/uploads/-/system/appearance/logo/1/logo.png", + "header_logo": "/uploads/-/system/appearance/header_logo/1/header.png", + "favicon": "/uploads/-/system/appearance/favicon/1/favicon.png", + "new_project_guidelines": "Please read the FAQs for help.", + "header_message": "", + "footer_message": "", + "message_background_color": "#e75e40", + "message_font_color": "#ffffff", + "email_header_and_footer_enabled": False, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/application/appearance", + json=content, + content_type="application/json", + status=200, + ) + + updated_content = dict(content) + updated_content["title"] = new_title + updated_content["description"] = new_description + + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/application/appearance", + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_get_update_appearance(gl, resp_application_appearance): + appearance = gl.appearance.get() + assert appearance.title == title + assert appearance.description == description + appearance.title = new_title + appearance.description = new_description + appearance.save() + assert appearance.title == new_title + assert appearance.description == new_description + + +def test_update_appearance(gl, resp_application_appearance): + gl.appearance.update(title=new_title, description=new_description) diff --git a/tests/unit/objects/test_applications.py b/tests/unit/objects/test_applications.py new file mode 100644 index 0000000..61de019 --- /dev/null +++ b/tests/unit/objects/test_applications.py @@ -0,0 +1,44 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/applications.html +""" + +import pytest +import responses + +title = "GitLab Test Instance" +description = "gitlab-test.example.com" +new_title = "new-title" +new_description = "new-description" + + +@pytest.fixture +def resp_application_create(): + content = { + "name": "test_app", + "redirect_uri": "http://localhost:8080", + "scopes": ["api", "email"], + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/applications", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_create_application(gl, resp_application_create): + application = gl.applications.create( + { + "name": "test_app", + "redirect_uri": "http://localhost:8080", + "scopes": ["api", "email"], + "confidential": False, + } + ) + assert application.name == "test_app" + assert application.redirect_uri == "http://localhost:8080" + assert application.scopes == ["api", "email"] diff --git a/tests/unit/objects/test_audit_events.py b/tests/unit/objects/test_audit_events.py new file mode 100644 index 0000000..aba778b --- /dev/null +++ b/tests/unit/objects/test_audit_events.py @@ -0,0 +1,109 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/audit_events.html#project-audit-events +""" + +import re + +import pytest +import responses + +from gitlab.v4.objects.audit_events import ( + AuditEvent, + GroupAuditEvent, + ProjectAuditEvent, +) + +id = 5 + +audit_events_content = { + "id": 5, + "author_id": 1, + "entity_id": 7, + "entity_type": "Project", + "details": { + "change": "prevent merge request approval from reviewers", + "from": "", + "to": "true", + "author_name": "Administrator", + "target_id": 7, + "target_type": "Project", + "target_details": "twitter/typeahead-js", + "ip_address": "127.0.0.1", + "entity_path": "twitter/typeahead-js", + }, + "created_at": "2020-05-26T22:55:04.230Z", +} + +audit_events_url = re.compile( + r"http://localhost/api/v4/((groups|projects)/1/)?audit_events" +) + +audit_events_url_id = re.compile( + rf"http://localhost/api/v4/((groups|projects)/1/)?audit_events/{id}" +) + + +@pytest.fixture +def resp_list_audit_events(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=audit_events_url, + json=[audit_events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_audit_event(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=audit_events_url_id, + json=audit_events_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_instance_audit_events(gl, resp_list_audit_events): + audit_events = gl.audit_events.list() + assert isinstance(audit_events, list) + assert isinstance(audit_events[0], AuditEvent) + assert audit_events[0].id == id + + +def test_get_instance_audit_events(gl, resp_get_audit_event): + audit_event = gl.audit_events.get(id) + assert isinstance(audit_event, AuditEvent) + assert audit_event.id == id + + +def test_list_group_audit_events(group, resp_list_audit_events): + audit_events = group.audit_events.list() + assert isinstance(audit_events, list) + assert isinstance(audit_events[0], GroupAuditEvent) + assert audit_events[0].id == id + + +def test_get_group_audit_events(group, resp_get_audit_event): + audit_event = group.audit_events.get(id) + assert isinstance(audit_event, GroupAuditEvent) + assert audit_event.id == id + + +def test_list_project_audit_events(project, resp_list_audit_events): + audit_events = project.audit_events.list() + assert isinstance(audit_events, list) + assert isinstance(audit_events[0], ProjectAuditEvent) + assert audit_events[0].id == id + + +def test_get_project_audit_events(project, resp_get_audit_event): + audit_event = project.audit_events.get(id) + assert isinstance(audit_event, ProjectAuditEvent) + assert audit_event.id == id diff --git a/tests/unit/objects/test_badges.py b/tests/unit/objects/test_badges.py new file mode 100644 index 0000000..e226684 --- /dev/null +++ b/tests/unit/objects/test_badges.py @@ -0,0 +1,210 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/project_badges.html +GitLab API: https://docs.gitlab.com/ee/api/group_badges.html +""" +import re + +import pytest +import responses + +from gitlab.v4.objects import GroupBadge, ProjectBadge + +link_url = ( + "http://example.com/ci_status.svg?project=example-org/example-project&ref=master" +) +image_url = "https://example.io/my/badge" + +rendered_link_url = ( + "http://example.com/ci_status.svg?project=example-org/example-project&ref=master" +) +rendered_image_url = "https://example.io/my/badge" + +new_badge = { + "link_url": link_url, + "image_url": image_url, +} + +badge_content = { + "name": "Coverage", + "id": 1, + "link_url": link_url, + "image_url": image_url, + "rendered_link_url": rendered_image_url, + "rendered_image_url": rendered_image_url, +} + +preview_badge_content = { + "link_url": link_url, + "image_url": image_url, + "rendered_link_url": rendered_link_url, + "rendered_image_url": rendered_image_url, +} + + +@pytest.fixture() +def resp_get_badge(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), + json=badge_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_list_badges(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"), + json=[badge_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_create_badge(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges"), + json=badge_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_update_badge(): + updated_content = dict(badge_content) + updated_content["link_url"] = "http://link_url" + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_delete_badge(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url=re.compile(r"http://localhost/api/v4/(projects|groups)/1/badges/1"), + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +@pytest.fixture() +def resp_preview_badge(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile( + r"http://localhost/api/v4/(projects|groups)/1/badges/render" + ), + json=preview_badge_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_badges(project, resp_list_badges): + badges = project.badges.list() + assert isinstance(badges, list) + assert isinstance(badges[0], ProjectBadge) + + +def test_list_group_badges(group, resp_list_badges): + badges = group.badges.list() + assert isinstance(badges, list) + assert isinstance(badges[0], GroupBadge) + + +def test_get_project_badge(project, resp_get_badge): + badge = project.badges.get(1) + assert isinstance(badge, ProjectBadge) + assert badge.name == "Coverage" + assert badge.id == 1 + + +def test_get_group_badge(group, resp_get_badge): + badge = group.badges.get(1) + assert isinstance(badge, GroupBadge) + assert badge.name == "Coverage" + assert badge.id == 1 + + +def test_delete_project_badge(project, resp_delete_badge): + badge = project.badges.get(1, lazy=True) + badge.delete() + + +def test_delete_group_badge(group, resp_delete_badge): + badge = group.badges.get(1, lazy=True) + badge.delete() + + +def test_create_project_badge(project, resp_create_badge): + badge = project.badges.create(new_badge) + assert isinstance(badge, ProjectBadge) + assert badge.image_url == image_url + + +def test_create_group_badge(group, resp_create_badge): + badge = group.badges.create(new_badge) + assert isinstance(badge, GroupBadge) + assert badge.image_url == image_url + + +def test_preview_project_badge(project, resp_preview_badge): + output = project.badges.render( + link_url=link_url, + image_url=image_url, + ) + assert isinstance(output, dict) + assert "rendered_link_url" in output + assert "rendered_image_url" in output + assert output["link_url"] == output["rendered_link_url"] + assert output["image_url"] == output["rendered_image_url"] + + +def test_preview_group_badge(group, resp_preview_badge): + output = group.badges.render( + link_url=link_url, + image_url=image_url, + ) + assert isinstance(output, dict) + assert "rendered_link_url" in output + assert "rendered_image_url" in output + assert output["link_url"] == output["rendered_link_url"] + assert output["image_url"] == output["rendered_image_url"] + + +def test_update_project_badge(project, resp_update_badge): + badge = project.badges.get(1, lazy=True) + badge.link_url = "http://link_url" + badge.save() + assert badge.link_url == "http://link_url" + + +def test_update_group_badge(group, resp_update_badge): + badge = group.badges.get(1, lazy=True) + badge.link_url = "http://link_url" + badge.save() + assert badge.link_url == "http://link_url" diff --git a/tests/unit/objects/test_bridges.py b/tests/unit/objects/test_bridges.py new file mode 100644 index 0000000..4d39186 --- /dev/null +++ b/tests/unit/objects/test_bridges.py @@ -0,0 +1,109 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/jobs.html#list-pipeline-bridges +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectPipelineBridge + + +@pytest.fixture +def resp_list_bridges(): + export_bridges_content = { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + "created_at": "2015-12-24T16:51:14.000+01:00", + "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "message": "Test the CI integration.", + "short_id": "0ff3ae19", + "title": "Test the CI integration.", + }, + "allow_failure": False, + "created_at": "2015-12-24T15:51:21.802Z", + "started_at": "2015-12-24T17:54:27.722Z", + "finished_at": "2015-12-24T17:58:27.895Z", + "duration": 240, + "id": 7, + "name": "teaspoon", + "pipeline": { + "id": 6, + "ref": "master", + "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd", + "status": "pending", + "created_at": "2015-12-24T15:50:16.123Z", + "updated_at": "2015-12-24T18:00:44.432Z", + "web_url": "https://example.com/foo/bar/pipelines/6", + }, + "ref": "master", + "stage": "test", + "status": "pending", + "tag": False, + "web_url": "https://example.com/foo/bar/-/jobs/7", + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.dev/root", + "created_at": "2015-12-21T13:14:24.077Z", + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": "", + }, + "downstream_pipeline": { + "id": 5, + "sha": "f62a4b2fb89754372a346f24659212eb8da13601", + "ref": "master", + "status": "pending", + "created_at": "2015-12-24T17:54:27.722Z", + "updated_at": "2015-12-24T17:58:27.896Z", + "web_url": "https://example.com/diaspora/diaspora-client/pipelines/5", + }, + } + + export_pipelines_content = [ + { + "id": 6, + "status": "pending", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "web_url": "https://example.com/foo/bar/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + }, + ] + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/pipelines/6/bridges", + json=[export_bridges_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/pipelines", + json=export_pipelines_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_projects_pipelines_bridges(project, resp_list_bridges): + pipeline = project.pipelines.list()[0] + bridges = pipeline.bridges.list() + + assert isinstance(bridges, list) + assert isinstance(bridges[0], ProjectPipelineBridge) + assert bridges[0].downstream_pipeline["id"] == 5 + assert ( + bridges[0].downstream_pipeline["sha"] + == "f62a4b2fb89754372a346f24659212eb8da13601" + ) diff --git a/tests/unit/objects/test_commits.py b/tests/unit/objects/test_commits.py new file mode 100644 index 0000000..6b98117 --- /dev/null +++ b/tests/unit/objects/test_commits.py @@ -0,0 +1,115 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/commits.html +""" + +import pytest +import responses + + +@pytest.fixture +def resp_create_commit(): + content = { + "id": "ed899a2f4b50b4370feeea94676502b42383c746", + "short_id": "ed899a2f", + "title": "Commit message", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/repository/commits", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_commit(): + get_content = { + "id": "6b2257eabcec3db1f59dafbd84935e3caea04235", + "short_id": "6b2257ea", + "title": "Initial commit", + } + revert_content = { + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title": 'Revert "Initial commit"', + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea", + json=get_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/revert", + json=revert_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_commit_gpg_signature(): + content = { + "gpg_key_id": 1, + "gpg_key_primary_keyid": "8254AAB3FBD54AC9", + "gpg_key_user_name": "John Doe", + "gpg_key_user_email": "johndoe@example.com", + "verification_status": "verified", + "gpg_key_subkey_id": None, + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/repository/commits/6b2257ea/signature", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_get_commit(project, resp_commit): + commit = project.commits.get("6b2257ea") + assert commit.short_id == "6b2257ea" + assert commit.title == "Initial commit" + + +def test_create_commit(project, resp_create_commit): + data = { + "branch": "master", + "commit_message": "Commit message", + "actions": [ + { + "action": "create", + "file_path": "README", + "content": "", + } + ], + } + commit = project.commits.create(data) + assert commit.short_id == "ed899a2f" + assert commit.title == data["commit_message"] + + +def test_revert_commit(project, resp_commit): + commit = project.commits.get("6b2257ea", lazy=True) + revert_commit = commit.revert(branch="master") + assert revert_commit["short_id"] == "8b090c1b" + assert revert_commit["title"] == 'Revert "Initial commit"' + + +def test_get_commit_gpg_signature(project, resp_get_commit_gpg_signature): + commit = project.commits.get("6b2257ea", lazy=True) + signature = commit.signature() + assert signature["gpg_key_primary_keyid"] == "8254AAB3FBD54AC9" + assert signature["verification_status"] == "verified" diff --git a/tests/unit/objects/test_deploy_tokens.py b/tests/unit/objects/test_deploy_tokens.py new file mode 100644 index 0000000..66a79fa --- /dev/null +++ b/tests/unit/objects/test_deploy_tokens.py @@ -0,0 +1,45 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/deploy_tokens.html +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectDeployToken + +create_content = { + "id": 1, + "name": "test_deploy_token", + "username": "custom-user", + "expires_at": "2022-01-01T00:00:00.000Z", + "token": "jMRvtPNxrn3crTAGukpZ", + "scopes": ["read_repository"], +} + + +@pytest.fixture +def resp_deploy_token_create(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/deploy_tokens", + json=create_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_deploy_tokens(gl, resp_deploy_token_create): + deploy_token = gl.projects.get(1, lazy=True).deploytokens.create( + { + "name": "test_deploy_token", + "expires_at": "2022-01-01T00:00:00.000Z", + "username": "custom-user", + "scopes": ["read_repository"], + } + ) + assert isinstance(deploy_token, ProjectDeployToken) + assert deploy_token.id == 1 + assert deploy_token.expires_at == "2022-01-01T00:00:00.000Z" + assert deploy_token.username == "custom-user" + assert deploy_token.scopes == ["read_repository"] diff --git a/tests/unit/objects/test_deployments.py b/tests/unit/objects/test_deployments.py new file mode 100644 index 0000000..3cde8fe --- /dev/null +++ b/tests/unit/objects/test_deployments.py @@ -0,0 +1,50 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/deployments.html +""" +import pytest +import responses + + +@pytest.fixture +def resp_deployment(): + content = {"id": 42, "status": "success", "ref": "master"} + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/deployments", + json=content, + content_type="application/json", + status=200, + ) + + updated_content = dict(content) + updated_content["status"] = "failed" + + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/deployments/42", + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_deployment(project, resp_deployment): + deployment = project.deployments.create( + { + "environment": "Test", + "sha": "1agf4gs", + "ref": "master", + "tag": False, + "status": "created", + } + ) + assert deployment.id == 42 + assert deployment.status == "success" + assert deployment.ref == "master" + + deployment.status = "failed" + deployment.save() + assert deployment.status == "failed" diff --git a/tests/unit/objects/test_environments.py b/tests/unit/objects/test_environments.py new file mode 100644 index 0000000..b49a1db --- /dev/null +++ b/tests/unit/objects/test_environments.py @@ -0,0 +1,30 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/environments.html +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectEnvironment + + +@pytest.fixture +def resp_get_environment(): + content = {"name": "environment_name", "id": 1, "last_deployment": "sometime"} + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/environments/1", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_project_environments(project, resp_get_environment): + environment = project.environments.get(1) + assert isinstance(environment, ProjectEnvironment) + assert environment.id == 1 + assert environment.last_deployment == "sometime" + assert environment.name == "environment_name" diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py new file mode 100644 index 0000000..d4786f4 --- /dev/null +++ b/tests/unit/objects/test_groups.py @@ -0,0 +1,97 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/groups.html +""" + +import pytest +import responses + +import gitlab + + +@pytest.fixture +def resp_groups(): + content = {"name": "name", "id": 1, "path": "path"} + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1", + json=content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups", + json=[content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_import(accepted_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/import", + json=accepted_content, + content_type="application/json", + status=202, + ) + yield rsps + + +def test_get_group(gl, resp_groups): + data = gl.groups.get(1) + assert isinstance(data, gitlab.v4.objects.Group) + assert data.name == "name" + assert data.path == "path" + assert data.id == 1 + + +def test_create_group(gl, resp_groups): + name, path = "name", "path" + data = gl.groups.create({"name": name, "path": path}) + assert isinstance(data, gitlab.v4.objects.Group) + assert data.name == name + assert data.path == path + + +def test_create_group_export(group, resp_export): + export = group.exports.create() + assert export.message == "202 Accepted" + + +@pytest.mark.skip("GitLab API endpoint not implemented") +def test_refresh_group_export_status(group, resp_export): + export = group.exports.create() + export.refresh() + assert export.export_status == "finished" + + +def test_download_group_export(group, resp_export, binary_content): + export = group.exports.create() + download = export.download() + assert isinstance(download, bytes) + assert download == binary_content + + +def test_import_group(gl, resp_create_import): + group_import = gl.groups.import_group("file", "api-group", "API Group") + assert group_import["message"] == "202 Accepted" + + +@pytest.mark.skip("GitLab API endpoint not implemented") +def test_refresh_group_import_status(group, resp_groups): + group_import = group.imports.get() + group_import.refresh() + assert group_import.import_status == "finished" diff --git a/tests/unit/objects/test_hooks.py b/tests/unit/objects/test_hooks.py new file mode 100644 index 0000000..fe5c21c --- /dev/null +++ b/tests/unit/objects/test_hooks.py @@ -0,0 +1,29 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/system_hooks.html +""" +import pytest +import responses + +from gitlab.v4.objects import Hook + + +@pytest.fixture +def resp_get_hook(): + content = {"url": "testurl", "id": 1} + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/hooks/1", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_hooks(gl, resp_get_hook): + data = gl.hooks.get(1) + assert isinstance(data, Hook) + assert data.url == "testurl" + assert data.id == 1 diff --git a/tests/unit/objects/test_issues.py b/tests/unit/objects/test_issues.py new file mode 100644 index 0000000..93d8e0c --- /dev/null +++ b/tests/unit/objects/test_issues.py @@ -0,0 +1,69 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/issues.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ProjectIssuesStatistics + + +@pytest.fixture +def resp_list_issues(): + content = [{"name": "name", "id": 1}, {"name": "other_name", "id": 2}] + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/issues", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_issue(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/issues/1", + json={"name": "name", "id": 1}, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_issue_statistics(): + content = {"statistics": {"counts": {"all": 20, "closed": 5, "opened": 15}}} + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues_statistics", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_issues(gl, resp_list_issues): + data = gl.issues.list() + assert data[1].id == 2 + assert data[1].name == "other_name" + + +def test_get_issue(gl, resp_get_issue): + issue = gl.issues.get(1) + assert issue.id == 1 + assert issue.name == "name" + + +def test_project_issues_statistics(project, resp_issue_statistics): + statistics = project.issuesstatistics.get() + assert isinstance(statistics, ProjectIssuesStatistics) + assert statistics.statistics["counts"]["all"] == 20 diff --git a/tests/unit/objects/test_job_artifacts.py b/tests/unit/objects/test_job_artifacts.py new file mode 100644 index 0000000..7c5f1df --- /dev/null +++ b/tests/unit/objects/test_job_artifacts.py @@ -0,0 +1,30 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/job_artifacts.html +""" + +import pytest +import responses + +ref_name = "master" +job = "build" + + +@pytest.fixture +def resp_artifacts_by_ref_name(binary_content): + url = f"http://localhost/api/v4/projects/1/jobs/artifacts/{ref_name}/download?job={job}" + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=url, + body=binary_content, + content_type="application/octet-stream", + status=200, + ) + yield rsps + + +def test_download_artifacts_by_ref_name(gl, binary_content, resp_artifacts_by_ref_name): + project = gl.projects.get(1, lazy=True) + artifacts = project.artifacts(ref_name=ref_name, job=job) + assert artifacts == binary_content diff --git a/tests/unit/objects/test_jobs.py b/tests/unit/objects/test_jobs.py new file mode 100644 index 0000000..104d59d --- /dev/null +++ b/tests/unit/objects/test_jobs.py @@ -0,0 +1,96 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/jobs.html +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectJob + +job_content = { + "commit": { + "author_email": "admin@example.com", + "author_name": "Administrator", + }, + "coverage": None, + "allow_failure": False, + "created_at": "2015-12-24T15:51:21.880Z", + "started_at": "2015-12-24T17:54:30.733Z", + "finished_at": "2015-12-24T17:54:31.198Z", + "duration": 0.465, + "queued_duration": 0.010, + "artifacts_expire_at": "2016-01-23T17:54:31.198Z", + "tag_list": ["docker runner", "macos-10.15"], + "id": 1, + "name": "rubocop", + "pipeline": { + "id": 1, + "project_id": 1, + }, + "ref": "master", + "artifacts": [], + "runner": None, + "stage": "test", + "status": "failed", + "tag": False, + "web_url": "https://example.com/foo/bar/-/jobs/1", + "user": {"id": 1}, +} + + +@pytest.fixture +def resp_get_job(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/jobs/1", + json=job_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_cancel_job(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/jobs/1/cancel", + json=job_content, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_retry_job(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/jobs/1/retry", + json=job_content, + content_type="application/json", + status=201, + ) + yield rsps + + +def test_get_project_job(project, resp_get_job): + job = project.jobs.get(1) + assert isinstance(job, ProjectJob) + assert job.ref == "master" + + +def test_cancel_project_job(project, resp_cancel_job): + job = project.jobs.get(1, lazy=True) + + output = job.cancel() + assert output["ref"] == "master" + + +def test_retry_project_job(project, resp_retry_job): + job = project.jobs.get(1, lazy=True) + + output = job.retry() + assert output["ref"] == "master" diff --git a/tests/unit/objects/test_members.py b/tests/unit/objects/test_members.py new file mode 100644 index 0000000..6a39369 --- /dev/null +++ b/tests/unit/objects/test_members.py @@ -0,0 +1,58 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/members.html +""" +import pytest +import responses + +from gitlab.v4.objects import GroupBillableMember + +billable_members_content = [ + { + "id": 1, + "username": "raymond_smith", + "name": "Raymond Smith", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon", + "web_url": "http://192.168.1.8:3000/root", + "last_activity_on": "2021-01-27", + "membership_type": "group_member", + "removable": True, + } +] + + +@pytest.fixture +def resp_list_billable_group_members(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/billable_members", + json=billable_members_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_billable_group_member(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/groups/1/billable_members/1", + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_list_group_billable_members(group, resp_list_billable_group_members): + billable_members = group.billable_members.list() + assert isinstance(billable_members, list) + assert isinstance(billable_members[0], GroupBillableMember) + assert billable_members[0].removable is True + + +def test_delete_group_billable_member(group, resp_delete_billable_group_member): + group.billable_members.delete(1) diff --git a/tests/unit/objects/test_mro.py b/tests/unit/objects/test_mro.py new file mode 100644 index 0000000..8f67b77 --- /dev/null +++ b/tests/unit/objects/test_mro.py @@ -0,0 +1,122 @@ +""" +Ensure objects defined in gitlab.v4.objects have REST* as last item in class +definition + +Original notes by John L. Villalovos + +An example of an incorrect definition: + class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin): + ^^^^^^^^^^ This should be at the end. + +Correct way would be: + class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject): + Correctly at the end ^^^^^^^^^^ + + +Why this is an issue: + + When we do type-checking for gitlab/mixins.py we make RESTObject or + RESTManager the base class for the mixins + + Here is how our classes look when type-checking: + + class RESTObject(object): + def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: + ... + + class Mixin(RESTObject): + ... + + # Wrong ordering here + class Wrongv4Object(RESTObject, RefreshMixin): + ... + + If we actually ran this in Python we would get the following error: + class Wrongv4Object(RESTObject, Mixin): + TypeError: Cannot create a consistent method resolution + order (MRO) for bases RESTObject, Mixin + + When we are type-checking it fails to understand the class Wrongv4Object + and thus we can't type check it correctly. + +Almost all classes in gitlab/v4/objects/*py were already correct before this +check was added. +""" +import inspect + +import pytest + +import gitlab.v4.objects + + +def test_show_issue(): + """Test case to demonstrate the TypeError that occurs""" + + class RESTObject(object): + def __init__(self, manager: str, attrs: int) -> None: + ... + + class Mixin(RESTObject): + ... + + with pytest.raises(TypeError) as exc_info: + # Wrong ordering here + class Wrongv4Object(RESTObject, Mixin): + ... + + # The error message in the exception should be: + # TypeError: Cannot create a consistent method resolution + # order (MRO) for bases RESTObject, Mixin + + # Make sure the exception string contains "MRO" + assert "MRO" in exc_info.exconly() + + # Correctly ordered class, no exception + class Correctv4Object(Mixin, RESTObject): + ... + + +def test_mros(): + """Ensure objects defined in gitlab.v4.objects have REST* as last item in + class definition. + + We do this as we need to ensure the MRO (Method Resolution Order) is + correct. + """ + + failed_messages = [] + for module_name, module_value in inspect.getmembers(gitlab.v4.objects): + if not inspect.ismodule(module_value): + # We only care about the modules + continue + # Iterate through all the classes in our module + for class_name, class_value in inspect.getmembers(module_value): + if not inspect.isclass(class_value): + continue + + # Ignore imported classes from gitlab.base + if class_value.__module__ == "gitlab.base": + continue + + mro = class_value.mro() + + # We only check classes which have a 'gitlab.base' class in their MRO + has_base = False + for count, obj in enumerate(mro, start=1): + if obj.__module__ == "gitlab.base": + has_base = True + base_classname = obj.__name__ + if has_base: + filename = inspect.getfile(class_value) + # NOTE(jlvillal): The very last item 'mro[-1]' is always going + # to be 'object'. That is why we are checking 'mro[-2]'. + if mro[-2].__module__ != "gitlab.base": + failed_messages.append( + ( + f"class definition for {class_name!r} in file {filename!r} " + f"must have {base_classname!r} as the last class in the " + f"class definition" + ) + ) + failed_msg = "\n".join(failed_messages) + assert not failed_messages, failed_msg diff --git a/tests/unit/objects/test_packages.py b/tests/unit/objects/test_packages.py new file mode 100644 index 0000000..672eee0 --- /dev/null +++ b/tests/unit/objects/test_packages.py @@ -0,0 +1,186 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/packages.html +""" +import re + +import pytest +import responses + +from gitlab.v4.objects import GroupPackage, ProjectPackage, ProjectPackageFile + +package_content = { + "id": 1, + "name": "com/mycompany/my-app", + "version": "1.0-SNAPSHOT", + "package_type": "maven", + "_links": { + "web_path": "/namespace1/project1/-/packages/1", + "delete_api_path": "/namespace1/project1/-/packages/1", + }, + "created_at": "2019-11-27T03:37:38.711Z", + "pipeline": { + "id": 123, + "status": "pending", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "web_url": "https://example.com/foo/bar/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "user": { + "name": "Administrator", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + }, + }, + "versions": [ + { + "id": 2, + "version": "2.0-SNAPSHOT", + "created_at": "2020-04-28T04:42:11.573Z", + "pipeline": { + "id": 234, + "status": "pending", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "web_url": "https://example.com/foo/bar/pipelines/58", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "user": { + "name": "Administrator", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + }, + }, + } + ], +} + +package_file_content = [ + { + "id": 25, + "package_id": 1, + "created_at": "2018-11-07T15:25:52.199Z", + "file_name": "my-app-1.5-20181107.152550-1.jar", + "size": 2421, + "file_md5": "58e6a45a629910c6ff99145a688971ac", + "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe", + "pipelines": [ + { + "id": 123, + "status": "pending", + "ref": "new-pipeline", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "web_url": "https://example.com/foo/bar/pipelines/47", + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "user": { + "name": "Administrator", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + }, + } + ], + }, + { + "id": 26, + "package_id": 1, + "created_at": "2018-11-07T15:25:56.776Z", + "file_name": "my-app-1.5-20181107.152550-1.pom", + "size": 1122, + "file_md5": "d90f11d851e17c5513586b4a7e98f1b2", + "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5", + }, + { + "id": 27, + "package_id": 1, + "created_at": "2018-11-07T15:26:00.556Z", + "file_name": "maven-metadata.xml", + "size": 767, + "file_md5": "6dfd0cce1203145a927fef5e3a1c650c", + "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2", + }, +] + + +@pytest.fixture +def resp_list_packages(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/packages"), + json=[package_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_package(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/packages/1", + json=package_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_package(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/packages/1", + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +@pytest.fixture +def resp_list_package_files(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile( + r"http://localhost/api/v4/projects/1/packages/1/package_files" + ), + json=package_file_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_packages(project, resp_list_packages): + packages = project.packages.list() + assert isinstance(packages, list) + assert isinstance(packages[0], ProjectPackage) + assert packages[0].version == "1.0-SNAPSHOT" + + +def test_list_group_packages(group, resp_list_packages): + packages = group.packages.list() + assert isinstance(packages, list) + assert isinstance(packages[0], GroupPackage) + assert packages[0].version == "1.0-SNAPSHOT" + + +def test_get_project_package(project, resp_get_package): + package = project.packages.get(1) + assert isinstance(package, ProjectPackage) + assert package.version == "1.0-SNAPSHOT" + + +def test_delete_project_package(project, resp_delete_package): + package = project.packages.get(1, lazy=True) + package.delete() + + +def test_list_project_package_files(project, resp_list_package_files): + package = project.packages.get(1, lazy=True) + package_files = package.package_files.list() + assert isinstance(package_files, list) + assert isinstance(package_files[0], ProjectPackageFile) + assert package_files[0].id == 25 diff --git a/tests/unit/objects/test_personal_access_tokens.py b/tests/unit/objects/test_personal_access_tokens.py new file mode 100644 index 0000000..920cb1d --- /dev/null +++ b/tests/unit/objects/test_personal_access_tokens.py @@ -0,0 +1,46 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/personal_access_tokens.html +""" + +import pytest +import responses + + +@pytest.fixture +def resp_list_personal_access_token(): + content = [ + { + "id": 4, + "name": "Test Token", + "revoked": False, + "created_at": "2020-07-23T14:31:47.729Z", + "scopes": ["api"], + "active": True, + "user_id": 24, + "expires_at": None, + } + ] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/personal_access_tokens", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_personal_access_tokens(gl, resp_list_personal_access_token): + access_tokens = gl.personal_access_tokens.list() + assert len(access_tokens) == 1 + assert access_tokens[0].revoked is False + assert access_tokens[0].name == "Test Token" + + +def test_list_personal_access_tokens_filter(gl, resp_list_personal_access_token): + access_tokens = gl.personal_access_tokens.list(user_id=24) + assert len(access_tokens) == 1 + assert access_tokens[0].revoked is False + assert access_tokens[0].user_id == 24 diff --git a/tests/unit/objects/test_pipeline_schedules.py b/tests/unit/objects/test_pipeline_schedules.py new file mode 100644 index 0000000..c5dcc76 --- /dev/null +++ b/tests/unit/objects/test_pipeline_schedules.py @@ -0,0 +1,62 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/pipeline_schedules.html +""" +import pytest +import responses + + +@pytest.fixture +def resp_project_pipeline_schedule(created_content): + content = { + "id": 14, + "description": "Build packages", + "ref": "master", + "cron": "0 1 * * 5", + "cron_timezone": "UTC", + "next_run_at": "2017-05-26T01:00:00.000Z", + "active": True, + "created_at": "2017-05-19T13:43:08.169Z", + "updated_at": "2017-05-19T13:43:08.169Z", + "last_pipeline": None, + "owner": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "https://gitlab.example.com/root", + }, + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/pipeline_schedules", + json=content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/pipeline_schedules/14/play", + json=created_content, + content_type="application/json", + status=201, + ) + yield rsps + + +def test_project_pipeline_schedule_play(project, resp_project_pipeline_schedule): + description = "Build packages" + cronline = "0 1 * * 5" + sched = project.pipelineschedules.create( + {"ref": "master", "description": description, "cron": cronline} + ) + assert sched is not None + assert description == sched.description + assert cronline == sched.cron + + play_result = sched.play() + assert play_result is not None + assert "message" in play_result + assert play_result["message"] == "201 Created" diff --git a/tests/unit/objects/test_pipelines.py b/tests/unit/objects/test_pipelines.py new file mode 100644 index 0000000..d474296 --- /dev/null +++ b/tests/unit/objects/test_pipelines.py @@ -0,0 +1,94 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/pipelines.html +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectPipeline + +pipeline_content = { + "id": 46, + "project_id": 1, + "status": "pending", + "ref": "master", + "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a", + "tag": False, + "yaml_errors": None, + "user": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root", + }, + "created_at": "2016-08-11T11:28:34.085Z", + "updated_at": "2016-08-11T11:32:35.169Z", + "started_at": None, + "finished_at": "2016-08-11T11:32:35.145Z", + "committed_at": None, + "duration": None, + "queued_duration": 0.010, + "coverage": None, + "web_url": "https://example.com/foo/bar/pipelines/46", +} + + +@pytest.fixture +def resp_get_pipeline(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/pipelines/1", + json=pipeline_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_cancel_pipeline(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/pipelines/1/cancel", + json=pipeline_content, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_retry_pipeline(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/pipelines/1/retry", + json=pipeline_content, + content_type="application/json", + status=201, + ) + yield rsps + + +def test_get_project_pipeline(project, resp_get_pipeline): + pipeline = project.pipelines.get(1) + assert isinstance(pipeline, ProjectPipeline) + assert pipeline.ref == "master" + + +def test_cancel_project_pipeline(project, resp_cancel_pipeline): + pipeline = project.pipelines.get(1, lazy=True) + + output = pipeline.cancel() + assert output["ref"] == "master" + + +def test_retry_project_pipeline(project, resp_retry_pipeline): + pipeline = project.pipelines.get(1, lazy=True) + + output = pipeline.retry() + assert output["ref"] == "master" diff --git a/tests/unit/objects/test_project_access_tokens.py b/tests/unit/objects/test_project_access_tokens.py new file mode 100644 index 0000000..4d4788d --- /dev/null +++ b/tests/unit/objects/test_project_access_tokens.py @@ -0,0 +1,113 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/resource_access_tokens.html +""" + +import pytest +import responses + + +@pytest.fixture +def resp_list_project_access_token(): + content = [ + { + "user_id": 141, + "scopes": ["api"], + "name": "token", + "expires_at": "2021-01-31", + "id": 42, + "active": True, + "created_at": "2021-01-20T22:11:48.151Z", + "revoked": False, + } + ] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/access_tokens", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_project_access_token(): + content = { + "user_id": 141, + "scopes": ["api"], + "name": "token", + "expires_at": "2021-01-31", + "id": 42, + "active": True, + "created_at": "2021-01-20T22:11:48.151Z", + "revoked": False, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/access_tokens", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_revoke_project_access_token(): + content = [ + { + "user_id": 141, + "scopes": ["api"], + "name": "token", + "expires_at": "2021-01-31", + "id": 42, + "active": True, + "created_at": "2021-01-20T22:11:48.151Z", + "revoked": False, + } + ] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/projects/1/access_tokens/42", + json=content, + content_type="application/json", + status=204, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/access_tokens", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_access_tokens(gl, resp_list_project_access_token): + access_tokens = gl.projects.get(1, lazy=True).access_tokens.list() + assert len(access_tokens) == 1 + assert access_tokens[0].revoked is False + assert access_tokens[0].name == "token" + + +def test_create_project_access_token(gl, resp_create_project_access_token): + access_tokens = gl.projects.get(1, lazy=True).access_tokens.create( + {"name": "test", "scopes": ["api"]} + ) + assert access_tokens.revoked is False + assert access_tokens.user_id == 141 + assert access_tokens.expires_at == "2021-01-31" + + +def test_revoke_project_access_token( + gl, resp_list_project_access_token, resp_revoke_project_access_token +): + gl.projects.get(1, lazy=True).access_tokens.delete(42) + access_token = gl.projects.get(1, lazy=True).access_tokens.list()[0] + access_token.delete() diff --git a/tests/unit/objects/test_project_import_export.py b/tests/unit/objects/test_project_import_export.py new file mode 100644 index 0000000..78e51b1 --- /dev/null +++ b/tests/unit/objects/test_project_import_export.py @@ -0,0 +1,112 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/project_import_export.html +""" +import pytest +import responses + + +@pytest.fixture +def resp_import_project(): + content = { + "id": 1, + "description": None, + "name": "api-project", + "name_with_namespace": "Administrator / api-project", + "path": "api-project", + "path_with_namespace": "root/api-project", + "created_at": "2018-02-13T09:05:58.023Z", + "import_status": "scheduled", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/import", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_import_status(): + content = { + "id": 1, + "description": "Itaque perspiciatis minima aspernatur corporis consequatur.", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "created_at": "2017-08-29T04:36:44.383Z", + "import_status": "finished", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/import", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_import_github(): + content = { + "id": 27, + "name": "my-repo", + "full_path": "/root/my-repo", + "full_name": "Administrator / my-repo", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/import/github", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_import_project(gl, resp_import_project): + project_import = gl.projects.import_project("file", "api-project") + assert project_import["import_status"] == "scheduled" + + +def test_refresh_project_import_status(project, resp_import_status): + project_import = project.imports.get() + project_import.refresh() + assert project_import.import_status == "finished" + + +def test_import_github(gl, resp_import_github): + base_path = "/root" + name = "my-repo" + ret = gl.projects.import_github("githubkey", 1234, base_path, name) + assert isinstance(ret, dict) + assert ret["name"] == name + assert ret["full_path"] == "/".join((base_path, name)) + assert ret["full_name"].endswith(name) + + +def test_create_project_export(project, resp_export): + export = project.exports.create() + assert export.message == "202 Accepted" + + +def test_refresh_project_export_status(project, resp_export): + export = project.exports.create() + export.refresh() + assert export.export_status == "finished" + + +def test_download_project_export(project, resp_export, binary_content): + export = project.exports.create() + download = export.download() + assert isinstance(download, bytes) + assert download == binary_content diff --git a/tests/unit/objects/test_project_merge_request_approvals.py b/tests/unit/objects/test_project_merge_request_approvals.py new file mode 100644 index 0000000..16d58bd --- /dev/null +++ b/tests/unit/objects/test_project_merge_request_approvals.py @@ -0,0 +1,317 @@ +""" +Gitlab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html +""" + +import copy + +import pytest +import responses + +import gitlab + +approval_rule_id = 1 +approval_rule_name = "security" +approvals_required = 3 +user_ids = [5, 50] +group_ids = [5] + +new_approval_rule_name = "new approval rule" +new_approval_rule_user_ids = user_ids +new_approval_rule_approvals_required = 2 + +updated_approval_rule_user_ids = [5] +updated_approval_rule_approvals_required = 1 + + +@pytest.fixture +def resp_snippet(): + merge_request_content = [ + { + "id": 1, + "iid": 1, + "project_id": 1, + "title": "test1", + "description": "fixed login page css paddings", + "state": "merged", + "merged_by": { + "id": 87854, + "name": "Douwe Maan", + "username": "DouweM", + "state": "active", + "avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png", + "web_url": "https://gitlab.com/DouweM", + }, + "merged_at": "2018-09-07T11:16:17.520Z", + "closed_by": None, + "closed_at": None, + "created_at": "2017-04-29T08:46:00Z", + "updated_at": "2017-04-29T08:46:00Z", + "target_branch": "master", + "source_branch": "test1", + "upvotes": 0, + "downvotes": 0, + "author": { + "id": 1, + "name": "Administrator", + "username": "admin", + "state": "active", + "avatar_url": None, + "web_url": "https://gitlab.example.com/admin", + }, + "assignee": { + "id": 1, + "name": "Administrator", + "username": "admin", + "state": "active", + "avatar_url": None, + "web_url": "https://gitlab.example.com/admin", + }, + "assignees": [ + { + "name": "Miss Monserrate Beier", + "username": "axel.block", + "id": 12, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", + "web_url": "https://gitlab.example.com/axel.block", + } + ], + "source_project_id": 2, + "target_project_id": 3, + "labels": ["Community contribution", "Manage"], + "work_in_progress": None, + "milestone": { + "id": 5, + "iid": 1, + "project_id": 3, + "title": "v2.0", + "description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.", + "state": "closed", + "created_at": "2015-02-02T19:49:26.013Z", + "updated_at": "2015-02-02T19:49:26.013Z", + "due_date": "2018-09-22", + "start_date": "2018-08-08", + "web_url": "https://gitlab.example.com/my-group/my-project/milestones/1", + }, + "merge_when_pipeline_succeeds": None, + "merge_status": "can_be_merged", + "sha": "8888888888888888888888888888888888888888", + "merge_commit_sha": None, + "squash_commit_sha": None, + "user_notes_count": 1, + "discussion_locked": None, + "should_remove_source_branch": True, + "force_remove_source_branch": False, + "allow_collaboration": False, + "allow_maintainer_to_push": False, + "web_url": "http://gitlab.example.com/my-group/my-project/merge_requests/1", + "references": { + "short": "!1", + "relative": "my-group/my-project!1", + "full": "my-group/my-project!1", + }, + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": None, + "human_total_time_spent": None, + }, + "squash": False, + "task_completion_status": {"count": 0, "completed_count": 0}, + } + ] + mr_ars_content = [ + { + "id": approval_rule_id, + "name": approval_rule_name, + "rule_type": "regular", + "eligible_approvers": [ + { + "id": user_ids[0], + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe", + }, + { + "id": user_ids[1], + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1", + }, + ], + "approvals_required": approvals_required, + "source_rule": None, + "users": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe", + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": False, + "avatar_url": None, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": False, + "full_name": "group1", + "full_path": "group1", + "parent_id": None, + "ldap_cn": None, + "ldap_access": None, + } + ], + "contains_hidden_groups": False, + "overridden": False, + } + ] + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests", + json=merge_request_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1", + json=merge_request_content[0], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules", + json=mr_ars_content, + content_type="application/json", + status=200, + ) + + new_mr_ars_content = dict(mr_ars_content[0]) + new_mr_ars_content["name"] = new_approval_rule_name + new_mr_ars_content["approvals_required"] = new_approval_rule_approvals_required + + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules", + json=new_mr_ars_content, + content_type="application/json", + status=200, + ) + + updated_mr_ars_content = copy.deepcopy(mr_ars_content[0]) + updated_mr_ars_content["eligible_approvers"] = [ + mr_ars_content[0]["eligible_approvers"][0] + ] + + updated_mr_ars_content[ + "approvals_required" + ] = updated_approval_rule_approvals_required + + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/merge_requests/1/approval_rules/1", + json=updated_mr_ars_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_project_approval_manager_update_uses_post(project, resp_snippet): + """Ensure the + gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager object has + _update_uses_post set to True""" + approvals = project.approvals + assert isinstance( + approvals, gitlab.v4.objects.merge_request_approvals.ProjectApprovalManager + ) + assert approvals._update_uses_post is True + + +def test_list_merge_request_approval_rules(project, resp_snippet): + approval_rules = project.mergerequests.get(1).approval_rules.list() + assert len(approval_rules) == 1 + assert approval_rules[0].name == approval_rule_name + assert approval_rules[0].id == approval_rule_id + + +def test_update_merge_request_approvals_set_approvers(project, resp_snippet): + approvals = project.mergerequests.get(1).approvals + assert isinstance( + approvals, + gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager, + ) + assert approvals._update_uses_post is True + response = approvals.set_approvers( + updated_approval_rule_approvals_required, + approver_ids=updated_approval_rule_user_ids, + approver_group_ids=group_ids, + approval_rule_name=approval_rule_name, + ) + + assert response.approvals_required == updated_approval_rule_approvals_required + assert len(response.eligible_approvers) == len(updated_approval_rule_user_ids) + assert response.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0] + assert response.name == approval_rule_name + + +def test_create_merge_request_approvals_set_approvers(project, resp_snippet): + approvals = project.mergerequests.get(1).approvals + assert isinstance( + approvals, + gitlab.v4.objects.merge_request_approvals.ProjectMergeRequestApprovalManager, + ) + assert approvals._update_uses_post is True + response = approvals.set_approvers( + new_approval_rule_approvals_required, + approver_ids=new_approval_rule_user_ids, + approver_group_ids=group_ids, + approval_rule_name=new_approval_rule_name, + ) + assert response.approvals_required == new_approval_rule_approvals_required + assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) + assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] + assert response.name == new_approval_rule_name + + +def test_create_merge_request_approval_rule(project, resp_snippet): + approval_rules = project.mergerequests.get(1).approval_rules + data = { + "name": new_approval_rule_name, + "approvals_required": new_approval_rule_approvals_required, + "rule_type": "regular", + "user_ids": new_approval_rule_user_ids, + "group_ids": group_ids, + } + response = approval_rules.create(data) + assert response.approvals_required == new_approval_rule_approvals_required + assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) + assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] + assert response.name == new_approval_rule_name + + +def test_update_merge_request_approval_rule(project, resp_snippet): + approval_rules = project.mergerequests.get(1).approval_rules + ar_1 = approval_rules.list()[0] + ar_1.user_ids = updated_approval_rule_user_ids + ar_1.approvals_required = updated_approval_rule_approvals_required + ar_1.save() + + assert ar_1.approvals_required == updated_approval_rule_approvals_required + assert len(ar_1.eligible_approvers) == len(updated_approval_rule_user_ids) + assert ar_1.eligible_approvers[0]["id"] == updated_approval_rule_user_ids[0] diff --git a/tests/unit/objects/test_project_statistics.py b/tests/unit/objects/test_project_statistics.py new file mode 100644 index 0000000..50d9a6d --- /dev/null +++ b/tests/unit/objects/test_project_statistics.py @@ -0,0 +1,28 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/project_statistics.html +""" +import pytest +import responses + +from gitlab.v4.objects import ProjectAdditionalStatistics + + +@pytest.fixture +def resp_project_statistics(): + content = {"fetches": {"total": 50, "days": [{"count": 10, "date": "2018-01-10"}]}} + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/statistics", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_project_additional_statistics(project, resp_project_statistics): + statistics = project.additionalstatistics.get() + assert isinstance(statistics, ProjectAdditionalStatistics) + assert statistics.fetches["total"] == 50 diff --git a/tests/unit/objects/test_projects.py b/tests/unit/objects/test_projects.py new file mode 100644 index 0000000..73e119b --- /dev/null +++ b/tests/unit/objects/test_projects.py @@ -0,0 +1,262 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/projects.html +""" + +import pytest +import responses + +from gitlab.v4.objects import Project + +project_content = {"name": "name", "id": 1} +import_content = { + "id": 1, + "name": "project", + "import_status": "scheduled", +} + + +@pytest.fixture +def resp_get_project(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1", + json=project_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_list_projects(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects", + json=[project_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_import_bitbucket_server(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/import/bitbucket_server", + json=import_content, + content_type="application/json", + status=201, + ) + yield rsps + + +def test_get_project(gl, resp_get_project): + data = gl.projects.get(1) + assert isinstance(data, Project) + assert data.name == "name" + assert data.id == 1 + + +def test_list_projects(gl, resp_list_projects): + projects = gl.projects.list() + assert isinstance(projects[0], Project) + assert projects[0].name == "name" + + +def test_import_bitbucket_server(gl, resp_import_bitbucket_server): + res = gl.projects.import_bitbucket_server( + bitbucket_server_project="project", + bitbucket_server_repo="repo", + bitbucket_server_url="url", + bitbucket_server_username="username", + personal_access_token="token", + new_name="new_name", + target_namespace="namespace", + ) + assert res["id"] == 1 + assert res["name"] == "project" + assert res["import_status"] == "scheduled" + + +@pytest.mark.skip(reason="missing test") +def test_list_user_projects(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_list_user_starred_projects(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_list_project_users(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_create_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_create_user_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_update_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_fork_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_list_project_forks(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_star_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_unstar_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_list_project_starrers(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_get_project_languages(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_archive_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_unarchive_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_remove_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_restore_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_upload_file(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_share_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_delete_shared_project_link(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_list_project_hooks(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_get_project_hook(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_create_project_hook(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_update_project_hook(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_delete_project_hook(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_create_forked_from_relationship(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_delete_forked_from_relationship(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_search_projects_by_name(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_project_housekeeping(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_get_project_push_rules(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_create_project_push_rule(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_update_project_push_rule(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_delete_project_push_rule(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_transfer_project(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_project_pull_mirror(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_project_snapshot(gl): + pass + + +@pytest.mark.skip(reason="missing test") +def test_import_github(gl): + pass diff --git a/tests/unit/objects/test_releases.py b/tests/unit/objects/test_releases.py new file mode 100644 index 0000000..6c38a7c --- /dev/null +++ b/tests/unit/objects/test_releases.py @@ -0,0 +1,131 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/releases/index.html +https://docs.gitlab.com/ee/api/releases/links.html +""" +import re + +import pytest +import responses + +from gitlab.v4.objects import ProjectReleaseLink + +encoded_tag_name = "v1%2E0%2E0" +link_name = "hello-world" +link_url = "https://gitlab.example.com/group/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" +direct_url = f"https://gitlab.example.com/group/hello/-/releases/{encoded_tag_name}/downloads/hello-world" +new_link_type = "package" +link_content = { + "id": 2, + "name": link_name, + "url": link_url, + "direct_asset_url": direct_url, + "external": False, + "link_type": "other", +} + +links_url = re.compile( + rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links" +) +link_id_url = re.compile( + rf"http://localhost/api/v4/projects/1/releases/{encoded_tag_name}/assets/links/1" +) + + +@pytest.fixture +def resp_list_links(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=links_url, + json=[link_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=link_id_url, + json=link_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url=links_url, + json=link_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_link(): + updated_content = dict(link_content) + updated_content["link_type"] = new_link_type + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url=link_id_url, + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_link(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url=link_id_url, + json=link_content, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_list_release_links(release, resp_list_links): + links = release.links.list() + assert isinstance(links, list) + assert isinstance(links[0], ProjectReleaseLink) + assert links[0].url == link_url + + +def test_get_release_link(release, resp_get_link): + link = release.links.get(1) + assert isinstance(link, ProjectReleaseLink) + assert link.url == link_url + + +def test_create_release_link(release, resp_create_link): + link = release.links.create({"url": link_url, "name": link_name}) + assert isinstance(link, ProjectReleaseLink) + assert link.url == link_url + + +def test_update_release_link(release, resp_update_link): + link = release.links.get(1, lazy=True) + link.link_type = new_link_type + link.save() + assert link.link_type == new_link_type + + +def test_delete_release_link(release, resp_delete_link): + link = release.links.get(1, lazy=True) + link.delete() diff --git a/tests/unit/objects/test_remote_mirrors.py b/tests/unit/objects/test_remote_mirrors.py new file mode 100644 index 0000000..1ac35a2 --- /dev/null +++ b/tests/unit/objects/test_remote_mirrors.py @@ -0,0 +1,72 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/remote_mirrors.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ProjectRemoteMirror + + +@pytest.fixture +def resp_remote_mirrors(): + content = { + "enabled": True, + "id": 1, + "last_error": None, + "last_successful_update_at": "2020-01-06T17:32:02.823Z", + "last_update_at": "2020-01-06T17:32:02.823Z", + "last_update_started_at": "2020-01-06T17:31:55.864Z", + "only_protected_branches": True, + "update_status": "none", + "url": "https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git", + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/remote_mirrors", + json=[content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/remote_mirrors", + json=content, + content_type="application/json", + status=200, + ) + + updated_content = dict(content) + updated_content["update_status"] = "finished" + + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/remote_mirrors/1", + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_remote_mirrors(project, resp_remote_mirrors): + mirrors = project.remote_mirrors.list() + assert isinstance(mirrors, list) + assert isinstance(mirrors[0], ProjectRemoteMirror) + assert mirrors[0].enabled + + +def test_create_project_remote_mirror(project, resp_remote_mirrors): + mirror = project.remote_mirrors.create({"url": "https://example.com"}) + assert isinstance(mirror, ProjectRemoteMirror) + assert mirror.update_status == "none" + + +def test_update_project_remote_mirror(project, resp_remote_mirrors): + mirror = project.remote_mirrors.create({"url": "https://example.com"}) + mirror.only_protected_branches = True + mirror.save() + assert mirror.update_status == "finished" + assert mirror.only_protected_branches diff --git a/tests/unit/objects/test_repositories.py b/tests/unit/objects/test_repositories.py new file mode 100644 index 0000000..7c4d77d --- /dev/null +++ b/tests/unit/objects/test_repositories.py @@ -0,0 +1,49 @@ +""" +GitLab API: +https://docs.gitlab.com/ee/api/repositories.html +https://docs.gitlab.com/ee/api/repository_files.html +""" +from urllib.parse import quote + +import pytest +import responses + +from gitlab.v4.objects import ProjectFile + +file_path = "app/models/key.rb" +ref = "main" + + +@pytest.fixture +def resp_get_repository_file(): + file_response = { + "file_name": "key.rb", + "file_path": file_path, + "size": 1476, + "encoding": "base64", + "content": "IyA9PSBTY2hlbWEgSW5mb3...", + "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481", + "ref": ref, + "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", + "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", + "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", + } + + # requests also encodes `.` + encoded_path = quote(file_path, safe="").replace(".", "%2E") + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=f"http://localhost/api/v4/projects/1/repository/files/{encoded_path}", + json=file_response, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_get_repository_file(project, resp_get_repository_file): + file = project.files.get(file_path, ref=ref) + assert isinstance(file, ProjectFile) + assert file.file_path == file_path diff --git a/tests/unit/objects/test_resource_label_events.py b/tests/unit/objects/test_resource_label_events.py new file mode 100644 index 0000000..deea8a0 --- /dev/null +++ b/tests/unit/objects/test_resource_label_events.py @@ -0,0 +1,105 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/resource_label_events.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ( + GroupEpicResourceLabelEvent, + ProjectIssueResourceLabelEvent, + ProjectMergeRequestResourceLabelEvent, +) + + +@pytest.fixture() +def resp_group_epic_request_label_events(): + epic_content = {"id": 1} + events_content = {"id": 1, "resource_type": "Epic"} + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/epics", + json=[epic_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/epics/1/resource_label_events", + json=[events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_merge_request_label_events(): + mr_content = {"iid": 1} + events_content = {"id": 1, "resource_type": "MergeRequest"} + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests", + json=[mr_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/resource_label_events", + json=[events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_project_issue_label_events(): + issue_content = {"iid": 1} + events_content = {"id": 1, "resource_type": "Issue"} + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues", + json=[issue_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues/1/resource_label_events", + json=[events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +def test_project_issue_label_events(project, resp_project_issue_label_events): + issue = project.issues.list()[0] + label_events = issue.resourcelabelevents.list() + assert isinstance(label_events, list) + label_event = label_events[0] + assert isinstance(label_event, ProjectIssueResourceLabelEvent) + assert label_event.resource_type == "Issue" + + +def test_merge_request_label_events(project, resp_merge_request_label_events): + mr = project.mergerequests.list()[0] + label_events = mr.resourcelabelevents.list() + assert isinstance(label_events, list) + label_event = label_events[0] + assert isinstance(label_event, ProjectMergeRequestResourceLabelEvent) + assert label_event.resource_type == "MergeRequest" + + +def test_group_epic_request_label_events(group, resp_group_epic_request_label_events): + epic = group.epics.list()[0] + label_events = epic.resourcelabelevents.list() + assert isinstance(label_events, list) + label_event = label_events[0] + assert isinstance(label_event, GroupEpicResourceLabelEvent) + assert label_event.resource_type == "Epic" diff --git a/tests/unit/objects/test_resource_milestone_events.py b/tests/unit/objects/test_resource_milestone_events.py new file mode 100644 index 0000000..99faeaa --- /dev/null +++ b/tests/unit/objects/test_resource_milestone_events.py @@ -0,0 +1,73 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/resource_milestone_events.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ( + ProjectIssueResourceMilestoneEvent, + ProjectMergeRequestResourceMilestoneEvent, +) + + +@pytest.fixture() +def resp_merge_request_milestone_events(): + mr_content = {"iid": 1} + events_content = {"id": 1, "resource_type": "MergeRequest"} + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests", + json=[mr_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/resource_milestone_events", + json=[events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_project_issue_milestone_events(): + issue_content = {"iid": 1} + events_content = {"id": 1, "resource_type": "Issue"} + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues", + json=[issue_content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues/1/resource_milestone_events", + json=[events_content], + content_type="application/json", + status=200, + ) + yield rsps + + +def test_project_issue_milestone_events(project, resp_project_issue_milestone_events): + issue = project.issues.list()[0] + milestone_events = issue.resourcemilestoneevents.list() + assert isinstance(milestone_events, list) + milestone_event = milestone_events[0] + assert isinstance(milestone_event, ProjectIssueResourceMilestoneEvent) + assert milestone_event.resource_type == "Issue" + + +def test_merge_request_milestone_events(project, resp_merge_request_milestone_events): + mr = project.mergerequests.list()[0] + milestone_events = mr.resourcemilestoneevents.list() + assert isinstance(milestone_events, list) + milestone_event = milestone_events[0] + assert isinstance(milestone_event, ProjectMergeRequestResourceMilestoneEvent) + assert milestone_event.resource_type == "MergeRequest" diff --git a/tests/unit/objects/test_resource_state_events.py b/tests/unit/objects/test_resource_state_events.py new file mode 100644 index 0000000..bf18193 --- /dev/null +++ b/tests/unit/objects/test_resource_state_events.py @@ -0,0 +1,104 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/resource_state_events.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ( + ProjectIssueResourceStateEvent, + ProjectMergeRequestResourceStateEvent, +) + +issue_event_content = {"id": 1, "resource_type": "Issue"} +mr_event_content = {"id": 1, "resource_type": "MergeRequest"} + + +@pytest.fixture() +def resp_list_project_issue_state_events(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues/1/resource_state_events", + json=[issue_event_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_get_project_issue_state_event(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/issues/1/resource_state_events/1", + json=issue_event_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_list_merge_request_state_events(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events", + json=[mr_event_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture() +def resp_get_merge_request_state_event(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/resource_state_events/1", + json=mr_event_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_issue_state_events( + project_issue, resp_list_project_issue_state_events +): + state_events = project_issue.resourcestateevents.list() + assert isinstance(state_events, list) + + state_event = state_events[0] + assert isinstance(state_event, ProjectIssueResourceStateEvent) + assert state_event.resource_type == "Issue" + + +def test_get_project_issue_state_event( + project_issue, resp_get_project_issue_state_event +): + state_event = project_issue.resourcestateevents.get(1) + assert isinstance(state_event, ProjectIssueResourceStateEvent) + assert state_event.resource_type == "Issue" + + +def test_list_merge_request_state_events( + project_merge_request, resp_list_merge_request_state_events +): + state_events = project_merge_request.resourcestateevents.list() + assert isinstance(state_events, list) + + state_event = state_events[0] + assert isinstance(state_event, ProjectMergeRequestResourceStateEvent) + assert state_event.resource_type == "MergeRequest" + + +def test_get_merge_request_state_event( + project_merge_request, resp_get_merge_request_state_event +): + state_event = project_merge_request.resourcestateevents.get(1) + assert isinstance(state_event, ProjectMergeRequestResourceStateEvent) + assert state_event.resource_type == "MergeRequest" diff --git a/tests/unit/objects/test_runners.py b/tests/unit/objects/test_runners.py new file mode 100644 index 0000000..686eec2 --- /dev/null +++ b/tests/unit/objects/test_runners.py @@ -0,0 +1,282 @@ +import re + +import pytest +import responses + +import gitlab + +runner_detail = { + "active": True, + "architecture": "amd64", + "description": "test-1-20150125", + "id": 6, + "ip_address": "127.0.0.1", + "is_shared": False, + "contacted_at": "2016-01-25T16:39:48.066Z", + "name": "test-runner", + "online": True, + "status": "online", + "platform": "linux", + "projects": [ + { + "id": 1, + "name": "GitLab Community Edition", + "name_with_namespace": "GitLab.org / GitLab Community Edition", + "path": "gitlab-foss", + "path_with_namespace": "gitlab-org/gitlab-foss", + } + ], + "revision": "5nj35", + "tag_list": ["ruby", "mysql"], + "version": "v13.0.0", + "access_level": "ref_protected", + "maximum_timeout": 3600, +} + +runner_shortinfo = { + "active": True, + "description": "test-1-20150125", + "id": 6, + "is_shared": False, + "ip_address": "127.0.0.1", + "name": "test-name", + "online": True, + "status": "online", +} + +runner_jobs = [ + { + "id": 6, + "ip_address": "127.0.0.1", + "status": "running", + "stage": "test", + "name": "test", + "ref": "master", + "tag": False, + "coverage": "99%", + "created_at": "2017-11-16T08:50:29.000Z", + "started_at": "2017-11-16T08:51:29.000Z", + "finished_at": "2017-11-16T08:53:29.000Z", + "duration": 120, + "user": { + "id": 1, + "name": "John Doe2", + "username": "user2", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user2", + "created_at": "2017-11-16T18:38:46.000Z", + "bio": None, + "location": None, + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": None, + }, + } +] + + +@pytest.fixture +def resp_get_runners_jobs(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/runners/6/jobs", + json=runner_jobs, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_runners_list(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=re.compile(r".*?(/runners(/all)?|/(groups|projects)/1/runners)"), + json=[runner_shortinfo], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_runner_detail(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?/runners/6") + rsps.add( + method=responses.GET, + url=pattern, + json=runner_detail, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.PUT, + url=pattern, + json=runner_detail, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_runner_register(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?/runners") + rsps.add( + method=responses.POST, + url=pattern, + json={"id": "6", "token": "6337ff461c94fd3fa32ba3b1ff4125"}, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_runner_enable(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?(projects|groups)/1/runners") + rsps.add( + method=responses.POST, + url=pattern, + json=runner_shortinfo, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_runner_delete(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?/runners/6") + rsps.add( + method=responses.GET, + url=pattern, + json=runner_detail, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.DELETE, + url=pattern, + status=204, + ) + yield rsps + + +@pytest.fixture +def resp_runner_disable(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?/(groups|projects)/1/runners/6") + rsps.add( + method=responses.DELETE, + url=pattern, + status=204, + ) + yield rsps + + +@pytest.fixture +def resp_runner_verify(): + with responses.RequestsMock() as rsps: + pattern = re.compile(r".*?/runners/verify") + rsps.add( + method=responses.POST, + url=pattern, + status=200, + ) + yield rsps + + +def test_owned_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): + runners = gl.runners.list() + assert runners[0].active is True + assert runners[0].id == 6 + assert runners[0].name == "test-name" + assert len(runners) == 1 + + +def test_project_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): + runners = gl.projects.get(1, lazy=True).runners.list() + assert runners[0].active is True + assert runners[0].id == 6 + assert runners[0].name == "test-name" + assert len(runners) == 1 + + +def test_group_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): + runners = gl.groups.get(1, lazy=True).runners.list() + assert runners[0].active is True + assert runners[0].id == 6 + assert runners[0].name == "test-name" + assert len(runners) == 1 + + +def test_all_runners_list(gl: gitlab.Gitlab, resp_get_runners_list): + runners = gl.runners.all() + assert runners[0].active is True + assert runners[0].id == 6 + assert runners[0].name == "test-name" + assert len(runners) == 1 + + +def test_create_runner(gl: gitlab.Gitlab, resp_runner_register): + runner = gl.runners.create({"token": "token"}) + assert runner.id == "6" + assert runner.token == "6337ff461c94fd3fa32ba3b1ff4125" + + +def test_get_update_runner(gl: gitlab.Gitlab, resp_runner_detail): + runner = gl.runners.get(6) + assert runner.active is True + runner.tag_list.append("new") + runner.save() + + +def test_remove_runner(gl: gitlab.Gitlab, resp_runner_delete): + runner = gl.runners.get(6) + runner.delete() + gl.runners.delete(6) + + +def test_disable_project_runner(gl: gitlab.Gitlab, resp_runner_disable): + gl.projects.get(1, lazy=True).runners.delete(6) + + +def test_disable_group_runner(gl: gitlab.Gitlab, resp_runner_disable): + gl.groups.get(1, lazy=True).runners.delete(6) + + +def test_enable_project_runner(gl: gitlab.Gitlab, resp_runner_enable): + runner = gl.projects.get(1, lazy=True).runners.create({"runner_id": 6}) + assert runner.active is True + assert runner.id == 6 + assert runner.name == "test-name" + + +def test_enable_group_runner(gl: gitlab.Gitlab, resp_runner_enable): + runner = gl.groups.get(1, lazy=True).runners.create({"runner_id": 6}) + assert runner.active is True + assert runner.id == 6 + assert runner.name == "test-name" + + +def test_verify_runner(gl: gitlab.Gitlab, resp_runner_verify): + gl.runners.verify("token") + + +def test_runner_jobs(gl: gitlab.Gitlab, resp_get_runners_jobs): + jobs = gl.runners.get(6, lazy=True).jobs.list() + assert jobs[0].duration == 120 + assert jobs[0].name == "test" + assert jobs[0].user.get("name") == "John Doe2" + assert len(jobs) == 1 diff --git a/tests/unit/objects/test_services.py b/tests/unit/objects/test_services.py new file mode 100644 index 0000000..5b2bcb8 --- /dev/null +++ b/tests/unit/objects/test_services.py @@ -0,0 +1,93 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/services.html +""" + +import pytest +import responses + +from gitlab.v4.objects import ProjectService + + +@pytest.fixture +def resp_service(): + content = { + "id": 100152, + "title": "Pipelines emails", + "slug": "pipelines-email", + "created_at": "2019-01-14T08:46:43.637+01:00", + "updated_at": "2019-07-01T14:10:36.156+02:00", + "active": True, + "commit_events": True, + "push_events": True, + "issues_events": True, + "confidential_issues_events": True, + "merge_requests_events": True, + "tag_push_events": True, + "note_events": True, + "confidential_note_events": True, + "pipeline_events": True, + "wiki_page_events": True, + "job_events": True, + "comment_on_event_enabled": True, + "project_id": 1, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/services", + json=[content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/services", + json=content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/services/pipelines-email", + json=content, + content_type="application/json", + status=200, + ) + updated_content = dict(content) + updated_content["issues_events"] = False + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/services/pipelines-email", + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_active_services(project, resp_service): + services = project.services.list() + assert isinstance(services, list) + assert isinstance(services[0], ProjectService) + assert services[0].active + assert services[0].push_events + + +def test_list_available_services(project, resp_service): + services = project.services.available() + assert isinstance(services, list) + assert isinstance(services[0], str) + + +def test_get_service(project, resp_service): + service = project.services.get("pipelines-email") + assert isinstance(service, ProjectService) + assert service.push_events is True + + +def test_update_service(project, resp_service): + service = project.services.get("pipelines-email") + service.issues_events = False + service.save() + assert service.issues_events is False diff --git a/tests/unit/objects/test_snippets.py b/tests/unit/objects/test_snippets.py new file mode 100644 index 0000000..2540fc3 --- /dev/null +++ b/tests/unit/objects/test_snippets.py @@ -0,0 +1,89 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/project_snippets.html + https://docs.gitlab.com/ee/api/snippets.html (todo) +""" + +import pytest +import responses + +title = "Example Snippet Title" +visibility = "private" +new_title = "new-title" + + +@pytest.fixture +def resp_snippet(): + content = { + "title": title, + "description": "More verbose snippet description", + "file_name": "example.txt", + "content": "source code with multiple lines", + "visibility": visibility, + } + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/snippets", + json=[content], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/snippets/1", + json=content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/projects/1/snippets", + json=content, + content_type="application/json", + status=200, + ) + + updated_content = dict(content) + updated_content["title"] = new_title + updated_content["visibility"] = visibility + + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/snippets", + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_list_project_snippets(project, resp_snippet): + snippets = project.snippets.list() + assert len(snippets) == 1 + assert snippets[0].title == title + assert snippets[0].visibility == visibility + + +def test_get_project_snippet(project, resp_snippet): + snippet = project.snippets.get(1) + assert snippet.title == title + assert snippet.visibility == visibility + + +def test_create_update_project_snippets(project, resp_snippet): + snippet = project.snippets.create( + { + "title": title, + "file_name": title, + "content": title, + "visibility": visibility, + } + ) + assert snippet.title == title + assert snippet.visibility == visibility + + snippet.title = new_title + snippet.save() + assert snippet.title == new_title + assert snippet.visibility == visibility diff --git a/tests/unit/objects/test_submodules.py b/tests/unit/objects/test_submodules.py new file mode 100644 index 0000000..69c1cd7 --- /dev/null +++ b/tests/unit/objects/test_submodules.py @@ -0,0 +1,46 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/repository_submodules.html +""" +import pytest +import responses + + +@pytest.fixture +def resp_update_submodule(): + content = { + "id": "ed899a2f4b50b4370feeea94676502b42383c746", + "short_id": "ed899a2f4b5", + "title": "Message", + "author_name": "Author", + "author_email": "author@example.com", + "committer_name": "Author", + "committer_email": "author@example.com", + "created_at": "2018-09-20T09:26:24.000-07:00", + "message": "Message", + "parent_ids": ["ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"], + "committed_date": "2018-09-20T09:26:24.000-07:00", + "authored_date": "2018-09-20T09:26:24.000-07:00", + "status": None, + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/projects/1/repository/submodules/foo%2Fbar", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_update_submodule(project, resp_update_submodule): + ret = project.update_submodule( + submodule="foo/bar", + branch="master", + commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664", + commit_message="Message", + ) + assert isinstance(ret, dict) + assert ret["message"] == "Message" + assert ret["id"] == "ed899a2f4b50b4370feeea94676502b42383c746" diff --git a/tests/unit/objects/test_todos.py b/tests/unit/objects/test_todos.py new file mode 100644 index 0000000..058fe33 --- /dev/null +++ b/tests/unit/objects/test_todos.py @@ -0,0 +1,62 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/todos.html +""" + +import json +import os + +import pytest +import responses + +from gitlab.v4.objects import Todo + +with open(os.path.dirname(__file__) + "/../data/todo.json", "r") as json_file: + todo_content = json_file.read() + json_content = json.loads(todo_content) + + +@pytest.fixture +def resp_todo(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/todos", + json=json_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/todos/102/mark_as_done", + json=json_content[0], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_mark_all_as_done(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/todos/mark_as_done", + json={}, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_todo(gl, resp_todo): + todo = gl.todos.list()[0] + assert isinstance(todo, Todo) + assert todo.id == 102 + assert todo.target_type == "MergeRequest" + assert todo.target["assignee"]["username"] == "root" + + todo.mark_as_done() + + +def test_todo_mark_all_as_done(gl, resp_mark_all_as_done): + gl.todos.mark_all_as_done() diff --git a/tests/unit/objects/test_users.py b/tests/unit/objects/test_users.py new file mode 100644 index 0000000..e46a315 --- /dev/null +++ b/tests/unit/objects/test_users.py @@ -0,0 +1,217 @@ +""" +GitLab API: https://docs.gitlab.com/ce/api/users.html +""" +import pytest +import responses + +from gitlab.v4.objects import User, UserMembership, UserStatus + + +@pytest.fixture +def resp_get_user(): + content = { + "name": "name", + "id": 1, + "password": "password", + "username": "username", + "email": "email", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/users/1", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_user_memberships(): + content = [ + { + "source_id": 1, + "source_name": "Project one", + "source_type": "Project", + "access_level": "20", + }, + { + "source_id": 3, + "source_name": "Group three", + "source_type": "Namespace", + "access_level": "20", + }, + ] + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/users/1/memberships", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_activate(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/users/1/activate", + json={}, + content_type="application/json", + status=201, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/users/1/deactivate", + json={}, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_get_user_status(): + content = { + "message": "test", + "message_html": "<h1>Message</h1>", + "emoji": "thumbsup", + } + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/users/1/status", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_user_identity(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/users/1/identities/test_provider", + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +@pytest.fixture +def resp_follow_unfollow(): + user = { + "id": 1, + "username": "john_smith", + "name": "John Smith", + "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "web_url": "http://localhost:3000/john_smith", + } + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/users/1/follow", + json=user, + content_type="application/json", + status=201, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/users/1/unfollow", + json=user, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_followers_following(): + content = [ + { + "id": 2, + "name": "Lennie Donnelly", + "username": "evette.kilback", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon", + "web_url": "http://127.0.0.1:3000/evette.kilback", + }, + { + "id": 4, + "name": "Serena Bradtke", + "username": "cammy", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon", + "web_url": "http://127.0.0.1:3000/cammy", + }, + ] + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/users/1/followers", + json=content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/users/1/following", + json=content, + content_type="application/json", + status=200, + ) + yield rsps + + +def test_get_user(gl, resp_get_user): + user = gl.users.get(1) + assert isinstance(user, User) + assert user.name == "name" + assert user.id == 1 + + +def test_user_memberships(user, resp_get_user_memberships): + memberships = user.memberships.list() + assert isinstance(memberships[0], UserMembership) + assert memberships[0].source_type == "Project" + + +def test_user_status(user, resp_get_user_status): + status = user.status.get() + assert isinstance(status, UserStatus) + assert status.message == "test" + assert status.emoji == "thumbsup" + + +def test_user_activate_deactivate(user, resp_activate): + user.activate() + user.deactivate() + + +def test_delete_user_identity(user, resp_delete_user_identity): + user.identityproviders.delete("test_provider") + + +def test_user_follow_unfollow(user, resp_follow_unfollow): + user.follow() + user.unfollow() + + +def test_list_followers(user, resp_followers_following): + followers = user.followers_users.list() + followings = user.following_users.list() + assert isinstance(followers[0], User) + assert followers[0].id == 2 + assert isinstance(followings[0], User) + assert followings[1].id == 4 diff --git a/tests/unit/objects/test_variables.py b/tests/unit/objects/test_variables.py new file mode 100644 index 0000000..fae37a8 --- /dev/null +++ b/tests/unit/objects/test_variables.py @@ -0,0 +1,192 @@ +""" +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 +""" + +import re + +import pytest +import responses + +from gitlab.v4.objects import GroupVariable, ProjectVariable, Variable + +key = "TEST_VARIABLE_1" +value = "TEST_1" +new_value = "TEST_2" + +variable_content = { + "key": key, + "variable_type": "env_var", + "value": value, + "protected": False, + "masked": True, +} +variables_url = re.compile( + r"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables" +) +variables_key_url = re.compile( + rf"http://localhost/api/v4/(((groups|projects)/1)|(admin/ci))/variables/{key}" +) + + +@pytest.fixture +def resp_list_variables(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=variables_url, + json=[variable_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_variable(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url=variables_key_url, + json=variable_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_variable(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url=variables_url, + json=variable_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_variable(): + updated_content = dict(variable_content) + updated_content["value"] = new_value + + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.PUT, + url=variables_key_url, + json=updated_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_variable(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url=variables_key_url, + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + +def test_list_instance_variables(gl, resp_list_variables): + variables = gl.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], Variable) + assert variables[0].value == value + + +def test_get_instance_variable(gl, resp_get_variable): + variable = gl.variables.get(key) + assert isinstance(variable, Variable) + assert variable.value == value + + +def test_create_instance_variable(gl, resp_create_variable): + variable = gl.variables.create({"key": key, "value": value}) + assert isinstance(variable, Variable) + assert variable.value == value + + +def test_update_instance_variable(gl, resp_update_variable): + variable = gl.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_instance_variable(gl, resp_delete_variable): + variable = gl.variables.get(key, lazy=True) + variable.delete() + + +def test_list_project_variables(project, resp_list_variables): + variables = project.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], ProjectVariable) + assert variables[0].value == value + + +def test_get_project_variable(project, resp_get_variable): + variable = project.variables.get(key) + assert isinstance(variable, ProjectVariable) + assert variable.value == value + + +def test_create_project_variable(project, resp_create_variable): + variable = project.variables.create({"key": key, "value": value}) + assert isinstance(variable, ProjectVariable) + assert variable.value == value + + +def test_update_project_variable(project, resp_update_variable): + variable = project.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_project_variable(project, resp_delete_variable): + variable = project.variables.get(key, lazy=True) + variable.delete() + + +def test_list_group_variables(group, resp_list_variables): + variables = group.variables.list() + assert isinstance(variables, list) + assert isinstance(variables[0], GroupVariable) + assert variables[0].value == value + + +def test_get_group_variable(group, resp_get_variable): + variable = group.variables.get(key) + assert isinstance(variable, GroupVariable) + assert variable.value == value + + +def test_create_group_variable(group, resp_create_variable): + variable = group.variables.create({"key": key, "value": value}) + assert isinstance(variable, GroupVariable) + assert variable.value == value + + +def test_update_group_variable(group, resp_update_variable): + variable = group.variables.get(key, lazy=True) + variable.value = new_value + variable.save() + assert variable.value == new_value + + +def test_delete_group_variable(group, resp_delete_variable): + variable = group.variables.get(key, lazy=True) + variable.delete() diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py new file mode 100644 index 0000000..b3a58fc --- /dev/null +++ b/tests/unit/test_base.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pickle + +import pytest + +from gitlab import base + + +class FakeGitlab(object): + pass + + +class FakeObject(base.RESTObject): + pass + + +class FakeManager(base.RESTManager): + _obj_cls = FakeObject + _path = "/tests" + + +@pytest.fixture +def fake_gitlab(): + return FakeGitlab() + + +@pytest.fixture +def fake_manager(fake_gitlab): + return FakeManager(fake_gitlab) + + +class TestRESTManager: + def test_computed_path_simple(self): + class MGR(base.RESTManager): + _path = "/tests" + _obj_cls = object + + mgr = MGR(FakeGitlab()) + assert mgr._computed_path == "/tests" + + def test_computed_path_with_parent(self): + class MGR(base.RESTManager): + _path = "/tests/%(test_id)s/cases" + _obj_cls = object + _from_parent_attrs = {"test_id": "id"} + + class Parent(object): + id = 42 + + mgr = MGR(FakeGitlab(), parent=Parent()) + assert mgr._computed_path == "/tests/42/cases" + + def test_path_property(self): + class MGR(base.RESTManager): + _path = "/tests" + _obj_cls = object + + mgr = MGR(FakeGitlab()) + assert mgr.path == "/tests" + + +class TestRESTObject: + def test_instantiate(self, fake_gitlab, fake_manager): + obj = FakeObject(fake_manager, {"foo": "bar"}) + + assert {"foo": "bar"} == obj._attrs + assert {} == obj._updated_attrs + assert obj._create_managers() is None + assert fake_manager == obj.manager + assert fake_gitlab == obj.manager.gitlab + + def test_picklability(self, fake_manager): + obj = FakeObject(fake_manager, {"foo": "bar"}) + original_obj_module = obj._module + pickled = pickle.dumps(obj) + unpickled = pickle.loads(pickled) + assert isinstance(unpickled, FakeObject) + assert hasattr(unpickled, "_module") + assert unpickled._module == original_obj_module + pickle.dumps(unpickled) + + def test_attrs(self, fake_manager): + obj = FakeObject(fake_manager, {"foo": "bar"}) + + assert "bar" == obj.foo + with pytest.raises(AttributeError): + getattr(obj, "bar") + + obj.bar = "baz" + assert "baz" == obj.bar + assert {"foo": "bar"} == obj._attrs + assert {"bar": "baz"} == obj._updated_attrs + + def test_get_id(self, fake_manager): + obj = FakeObject(fake_manager, {"foo": "bar"}) + obj.id = 42 + assert 42 == obj.get_id() + + obj.id = None + assert obj.get_id() is None + + def test_custom_id_attr(self, fake_manager): + class OtherFakeObject(FakeObject): + _id_attr = "foo" + + obj = OtherFakeObject(fake_manager, {"foo": "bar"}) + assert "bar" == obj.get_id() + + def test_update_attrs(self, fake_manager): + obj = FakeObject(fake_manager, {"foo": "bar"}) + obj.bar = "baz" + obj._update_attrs({"foo": "foo", "bar": "bar"}) + assert {"foo": "foo", "bar": "bar"} == obj._attrs + assert {} == obj._updated_attrs + + def test_update_attrs_deleted(self, fake_manager): + obj = FakeObject(fake_manager, {"foo": "foo", "bar": "bar"}) + obj.bar = "baz" + obj._update_attrs({"foo": "foo"}) + assert {"foo": "foo"} == obj._attrs + assert {} == obj._updated_attrs + + def test_dir_unique(self, fake_manager): + obj = FakeObject(fake_manager, {"manager": "foo"}) + assert len(dir(obj)) == len(set(dir(obj))) + + def test_create_managers(self, fake_gitlab, fake_manager): + class ObjectWithManager(FakeObject): + _managers = (("fakes", "FakeManager"),) + + obj = ObjectWithManager(fake_manager, {"foo": "bar"}) + obj.id = 42 + assert isinstance(obj.fakes, FakeManager) + assert obj.fakes.gitlab == fake_gitlab + assert obj.fakes._parent == obj + + def test_equality(self, fake_manager): + obj1 = FakeObject(fake_manager, {"id": "foo"}) + obj2 = FakeObject(fake_manager, {"id": "foo", "other_attr": "bar"}) + assert obj1 == obj2 + + def test_equality_custom_id(self, fake_manager): + class OtherFakeObject(FakeObject): + _id_attr = "foo" + + obj1 = OtherFakeObject(fake_manager, {"foo": "bar"}) + obj2 = OtherFakeObject(fake_manager, {"foo": "bar", "other_attr": "baz"}) + assert obj1 == obj2 + + def test_inequality(self, fake_manager): + obj1 = FakeObject(fake_manager, {"id": "foo"}) + obj2 = FakeObject(fake_manager, {"id": "bar"}) + assert obj1 != obj2 + + def test_inequality_no_id(self, fake_manager): + obj1 = FakeObject(fake_manager, {"attr1": "foo"}) + obj2 = FakeObject(fake_manager, {"attr1": "bar"}) + assert obj1 != obj2 diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..a9ca958 --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016-2017 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import io +import os +import tempfile +from contextlib import redirect_stderr # noqa: H302 + +import pytest + +from gitlab import cli + + +@pytest.mark.parametrize( + "what,expected_class", + [ + ("class", "Class"), + ("test-class", "TestClass"), + ("test-longer-class", "TestLongerClass"), + ("current-user-gpg-key", "CurrentUserGPGKey"), + ("user-gpg-key", "UserGPGKey"), + ("ldap-group", "LDAPGroup"), + ], +) +def test_what_to_cls(what, expected_class): + def _namespace(): + pass + + ExpectedClass = type(expected_class, (), {}) + _namespace.__dict__[expected_class] = ExpectedClass + + assert cli.what_to_cls(what, _namespace) == ExpectedClass + + +@pytest.mark.parametrize( + "class_name,expected_what", + [ + ("Class", "class"), + ("TestClass", "test-class"), + ("TestUPPERCASEClass", "test-uppercase-class"), + ("UPPERCASETestClass", "uppercase-test-class"), + ("CurrentUserGPGKey", "current-user-gpg-key"), + ("UserGPGKey", "user-gpg-key"), + ("LDAPGroup", "ldap-group"), + ], +) +def test_cls_to_what(class_name, expected_what): + TestClass = type(class_name, (), {}) + + assert cli.cls_to_what(TestClass) == expected_what + + +def test_die(): + fl = io.StringIO() + with redirect_stderr(fl): + with pytest.raises(SystemExit) as test: + cli.die("foobar") + assert fl.getvalue() == "foobar\n" + assert test.value.code == 1 + + +def test_parse_value(): + ret = cli._parse_value("foobar") + assert ret == "foobar" + + ret = cli._parse_value(True) + assert ret is True + + ret = cli._parse_value(1) + assert ret == 1 + + ret = cli._parse_value(None) + assert ret is None + + fd, temp_path = tempfile.mkstemp() + os.write(fd, b"content") + os.close(fd) + ret = cli._parse_value("@%s" % temp_path) + assert ret == "content" + os.unlink(temp_path) + + fl = io.StringIO() + with redirect_stderr(fl): + with pytest.raises(SystemExit) as exc: + cli._parse_value("@/thisfileprobablydoesntexist") + assert ( + fl.getvalue() == "[Errno 2] No such file or directory:" + " '/thisfileprobablydoesntexist'\n" + ) + assert exc.value.code == 1 + + +def test_base_parser(): + parser = cli._get_base_parser() + args = parser.parse_args(["-v", "-g", "gl_id", "-c", "foo.cfg", "-c", "bar.cfg"]) + assert args.verbose + assert args.gitlab == "gl_id" + assert args.config_file == ["foo.cfg", "bar.cfg"] + + +def test_v4_parse_args(): + parser = cli._get_parser() + args = parser.parse_args(["project", "list"]) + assert args.what == "project" + assert args.whaction == "list" + + +def test_v4_parser(): + parser = cli._get_parser() + subparsers = next( + action + for action in parser._actions + if isinstance(action, argparse._SubParsersAction) + ) + assert subparsers is not None + assert "project" in subparsers.choices + + user_subparsers = next( + action + for action in subparsers.choices["project"]._actions + if isinstance(action, argparse._SubParsersAction) + ) + assert user_subparsers is not None + assert "list" in user_subparsers.choices + assert "get" in user_subparsers.choices + assert "delete" in user_subparsers.choices + assert "update" in user_subparsers.choices + assert "create" in user_subparsers.choices + assert "archive" in user_subparsers.choices + assert "unarchive" in user_subparsers.choices + + actions = user_subparsers.choices["create"]._option_string_actions + assert not actions["--description"].required + + user_subparsers = next( + action + for action in subparsers.choices["group"]._actions + if isinstance(action, argparse._SubParsersAction) + ) + actions = user_subparsers.choices["create"]._option_string_actions + assert actions["--name"].required diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..cd61b8d --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2016-2017 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import io +import os +from textwrap import dedent + +import mock +import pytest + +from gitlab import config, USER_AGENT + +custom_user_agent = "my-package/1.0.0" + +valid_config = u"""[global] +default = one +ssl_verify = true +timeout = 2 + +[one] +url = http://one.url +private_token = ABCDEF + +[two] +url = https://two.url +private_token = GHIJKL +ssl_verify = false +timeout = 10 + +[three] +url = https://three.url +private_token = MNOPQR +ssl_verify = /path/to/CA/bundle.crt +per_page = 50 + +[four] +url = https://four.url +oauth_token = STUV +""" + +custom_user_agent_config = """[global] +default = one +user_agent = {} + +[one] +url = http://one.url +private_token = ABCDEF +""".format( + custom_user_agent +) + +no_default_config = u"""[global] +[there] +url = http://there.url +private_token = ABCDEF +""" + +missing_attr_config = u"""[global] +[one] +url = http://one.url + +[two] +private_token = ABCDEF + +[three] +meh = hem + +[four] +url = http://four.url +private_token = ABCDEF +per_page = 200 +""" + + +@mock.patch.dict(os.environ, {"PYTHON_GITLAB_CFG": "/some/path"}) +def test_env_config_present(): + assert ["/some/path"] == config._env_config() + + +@mock.patch.dict(os.environ, {}, clear=True) +def test_env_config_missing(): + assert [] == config._env_config() + + +@mock.patch("os.path.exists") +def test_missing_config(path_exists): + path_exists.return_value = False + with pytest.raises(config.GitlabConfigMissingError): + config.GitlabConfigParser("test") + + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +def test_invalid_id(m_open, path_exists): + fd = io.StringIO(no_default_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + path_exists.return_value = True + config.GitlabConfigParser("there") + with pytest.raises(config.GitlabIDError): + config.GitlabConfigParser() + + fd = io.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + with pytest.raises(config.GitlabDataError): + config.GitlabConfigParser(gitlab_id="not_there") + + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +def test_invalid_data(m_open, path_exists): + fd = io.StringIO(missing_attr_config) + fd.close = mock.Mock(return_value=None, side_effect=lambda: fd.seek(0)) + m_open.return_value = fd + path_exists.return_value = True + + config.GitlabConfigParser("one") + config.GitlabConfigParser("one") + with pytest.raises(config.GitlabDataError): + config.GitlabConfigParser(gitlab_id="two") + with pytest.raises(config.GitlabDataError): + config.GitlabConfigParser(gitlab_id="three") + with pytest.raises(config.GitlabDataError) as emgr: + config.GitlabConfigParser("four") + assert "Unsupported per_page number: 200" == emgr.value.args[0] + + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +def test_valid_data(m_open, path_exists): + fd = io.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + path_exists.return_value = True + + cp = config.GitlabConfigParser() + assert "one" == cp.gitlab_id + assert "http://one.url" == cp.url + assert "ABCDEF" == cp.private_token + assert cp.oauth_token is None + assert 2 == cp.timeout + assert cp.ssl_verify is True + assert cp.per_page is None + + fd = io.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + cp = config.GitlabConfigParser(gitlab_id="two") + assert "two" == cp.gitlab_id + assert "https://two.url" == cp.url + assert "GHIJKL" == cp.private_token + assert cp.oauth_token is None + assert 10 == cp.timeout + assert cp.ssl_verify is False + + fd = io.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + cp = config.GitlabConfigParser(gitlab_id="three") + assert "three" == cp.gitlab_id + assert "https://three.url" == cp.url + assert "MNOPQR" == cp.private_token + assert cp.oauth_token is None + assert 2 == cp.timeout + assert "/path/to/CA/bundle.crt" == cp.ssl_verify + assert 50 == cp.per_page + + fd = io.StringIO(valid_config) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + cp = config.GitlabConfigParser(gitlab_id="four") + assert "four" == cp.gitlab_id + assert "https://four.url" == cp.url + assert cp.private_token is None + assert "STUV" == cp.oauth_token + assert 2 == cp.timeout + assert cp.ssl_verify is True + + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +def test_data_from_helper(m_open, path_exists, tmp_path): + helper = tmp_path / "helper.sh" + helper.write_text( + dedent( + """\ + #!/bin/sh + echo "secret" + """ + ) + ) + helper.chmod(0o755) + + fd = io.StringIO( + dedent( + """\ + [global] + default = helper + + [helper] + url = https://helper.url + oauth_token = helper: %s + """ + ) + % helper + ) + + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + cp = config.GitlabConfigParser(gitlab_id="helper") + assert "helper" == cp.gitlab_id + assert "https://helper.url" == cp.url + assert cp.private_token is None + assert "secret" == cp.oauth_token + + +@mock.patch("os.path.exists") +@mock.patch("builtins.open") +@pytest.mark.parametrize( + "config_string,expected_agent", + [ + (valid_config, USER_AGENT), + (custom_user_agent_config, custom_user_agent), + ], +) +def test_config_user_agent(m_open, path_exists, config_string, expected_agent): + fd = io.StringIO(config_string) + fd.close = mock.Mock(return_value=None) + m_open.return_value = fd + + cp = config.GitlabConfigParser() + assert cp.user_agent == expected_agent diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py new file mode 100644 index 0000000..57b394b --- /dev/null +++ b/tests/unit/test_exceptions.py @@ -0,0 +1,18 @@ +import pytest + +from gitlab import exceptions + + +def test_error_raises_from_http_error(): + """Methods decorated with @on_http_error should raise from GitlabHttpError.""" + + class TestError(Exception): + pass + + @exceptions.on_http_error(TestError) + def raise_error_from_http_error(): + raise exceptions.GitlabHttpError + + with pytest.raises(TestError) as context: + raise_error_from_http_error() + assert isinstance(context.value.__cause__, exceptions.GitlabHttpError) diff --git a/tests/unit/test_gitlab.py b/tests/unit/test_gitlab.py new file mode 100644 index 0000000..acb8752 --- /dev/null +++ b/tests/unit/test_gitlab.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 Mika Mäenpää <mika.j.maenpaa@tut.fi>, +# Tampere University of Technology +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or` +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import pickle + +import pytest +from httmock import HTTMock, response, urlmatch, with_httmock # noqa + +from gitlab import Gitlab, GitlabList, USER_AGENT +from gitlab.v4.objects import CurrentUser + +username = "username" +user_id = 1 + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/user", method="get") +def resp_get_user(url, request): + headers = {"content-type": "application/json"} + content = '{{"id": {0:d}, "username": "{1:s}"}}'.format(user_id, username).encode( + "utf-8" + ) + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/tests", method="get") +def resp_page_1(url, request): + headers = { + "content-type": "application/json", + "X-Page": 1, + "X-Next-Page": 2, + "X-Per-Page": 1, + "X-Total-Pages": 2, + "X-Total": 2, + "Link": ("<http://localhost/api/v4/tests?per_page=1&page=2>;" ' rel="next"'), + } + content = '[{"a": "b"}]' + return response(200, content, headers, None, 5, request) + + +@urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/tests", + method="get", + query=r".*page=2", +) +def resp_page_2(url, request): + headers = { + "content-type": "application/json", + "X-Page": 2, + "X-Next-Page": 2, + "X-Per-Page": 1, + "X-Total-Pages": 2, + "X-Total": 2, + } + content = '[{"c": "d"}]' + return response(200, content, headers, None, 5, request) + + +def test_gitlab_build_list(gl): + with HTTMock(resp_page_1): + obj = gl.http_list("/tests", as_list=False) + assert len(obj) == 2 + assert obj._next_url == "http://localhost/api/v4/tests?per_page=1&page=2" + assert obj.current_page == 1 + assert obj.prev_page is None + assert obj.next_page == 2 + assert obj.per_page == 1 + assert obj.total_pages == 2 + assert obj.total == 2 + + with HTTMock(resp_page_2): + test_list = list(obj) + assert len(test_list) == 2 + assert test_list[0]["a"] == "b" + assert test_list[1]["c"] == "d" + + +@with_httmock(resp_page_1, resp_page_2) +def test_gitlab_all_omitted_when_as_list(gl): + result = gl.http_list("/tests", as_list=False, all=True) + assert isinstance(result, GitlabList) + + +def test_gitlab_strip_base_url(gl_trailing): + assert gl_trailing.url == "http://localhost" + + +def test_gitlab_strip_api_url(gl_trailing): + assert gl_trailing.api_url == "http://localhost/api/v4" + + +def test_gitlab_build_url(gl_trailing): + r = gl_trailing._build_url("/projects") + assert r == "http://localhost/api/v4/projects" + + +def test_gitlab_pickability(gl): + original_gl_objects = gl._objects + pickled = pickle.dumps(gl) + unpickled = pickle.loads(pickled) + assert isinstance(unpickled, Gitlab) + assert hasattr(unpickled, "_objects") + assert unpickled._objects == original_gl_objects + + +@with_httmock(resp_get_user) +def test_gitlab_token_auth(gl, callback=None): + gl.auth() + assert gl.user.username == username + assert gl.user.id == user_id + assert isinstance(gl.user, CurrentUser) + + +def test_gitlab_from_config(default_config): + config_path = default_config + Gitlab.from_config("one", [config_path]) + + +def test_gitlab_subclass_from_config(default_config): + class MyGitlab(Gitlab): + pass + + config_path = default_config + gl = MyGitlab.from_config("one", [config_path]) + assert isinstance(gl, MyGitlab) + + +@pytest.mark.parametrize( + "kwargs,expected_agent", + [ + ({}, USER_AGENT), + ({"user_agent": "my-package/1.0.0"}, "my-package/1.0.0"), + ], +) +def test_gitlab_user_agent(kwargs, expected_agent): + gl = Gitlab("http://localhost", **kwargs) + assert gl.headers["User-Agent"] == expected_agent diff --git a/tests/unit/test_gitlab_auth.py b/tests/unit/test_gitlab_auth.py new file mode 100644 index 0000000..314fbed --- /dev/null +++ b/tests/unit/test_gitlab_auth.py @@ -0,0 +1,85 @@ +import pytest +import requests + +from gitlab import Gitlab + + +def test_invalid_auth_args(): + with pytest.raises(ValueError): + Gitlab( + "http://localhost", + api_version="4", + private_token="private_token", + oauth_token="bearer", + ) + with pytest.raises(ValueError): + Gitlab( + "http://localhost", + api_version="4", + oauth_token="bearer", + http_username="foo", + http_password="bar", + ) + with pytest.raises(ValueError): + Gitlab( + "http://localhost", + api_version="4", + private_token="private_token", + http_password="bar", + ) + with pytest.raises(ValueError): + Gitlab( + "http://localhost", + api_version="4", + private_token="private_token", + http_username="foo", + ) + + +def test_private_token_auth(): + gl = Gitlab("http://localhost", private_token="private_token", api_version="4") + assert gl.private_token == "private_token" + assert gl.oauth_token is None + assert gl.job_token is None + assert gl._http_auth is None + assert "Authorization" not in gl.headers + assert gl.headers["PRIVATE-TOKEN"] == "private_token" + assert "JOB-TOKEN" not in gl.headers + + +def test_oauth_token_auth(): + gl = Gitlab("http://localhost", oauth_token="oauth_token", api_version="4") + assert gl.private_token is None + assert gl.oauth_token == "oauth_token" + assert gl.job_token is None + assert gl._http_auth is None + assert gl.headers["Authorization"] == "Bearer oauth_token" + assert "PRIVATE-TOKEN" not in gl.headers + assert "JOB-TOKEN" not in gl.headers + + +def test_job_token_auth(): + gl = Gitlab("http://localhost", job_token="CI_JOB_TOKEN", api_version="4") + assert gl.private_token is None + assert gl.oauth_token is None + assert gl.job_token == "CI_JOB_TOKEN" + assert gl._http_auth is None + assert "Authorization" not in gl.headers + assert "PRIVATE-TOKEN" not in gl.headers + assert gl.headers["JOB-TOKEN"] == "CI_JOB_TOKEN" + + +def test_http_auth(): + gl = Gitlab( + "http://localhost", + private_token="private_token", + http_username="foo", + http_password="bar", + api_version="4", + ) + assert gl.private_token == "private_token" + assert gl.oauth_token is None + assert gl.job_token is None + assert isinstance(gl._http_auth, requests.auth.HTTPBasicAuth) + assert gl.headers["PRIVATE-TOKEN"] == "private_token" + assert "Authorization" not in gl.headers diff --git a/tests/unit/test_gitlab_http_methods.py b/tests/unit/test_gitlab_http_methods.py new file mode 100644 index 0000000..f1bc9cd --- /dev/null +++ b/tests/unit/test_gitlab_http_methods.py @@ -0,0 +1,234 @@ +import pytest +import requests +from httmock import HTTMock, response, urlmatch + +from gitlab import GitlabHttpError, GitlabList, GitlabParsingError + + +def test_build_url(gl): + r = gl._build_url("http://localhost/api/v4") + assert r == "http://localhost/api/v4" + r = gl._build_url("https://localhost/api/v4") + assert r == "https://localhost/api/v4" + r = gl._build_url("/projects") + assert r == "http://localhost/api/v4/projects" + + +def test_http_request(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '[{"name": "project1"}]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + http_r = gl.http_request("get", "/projects") + http_r.json() + assert http_r.status_code == 200 + + +def test_http_request_404(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") + def resp_cont(url, request): + content = {"Here is wh it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_request("get", "/not_there") + + +def test_get_request(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '{"name": "project1"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_get("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + +def test_get_request_raw(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/octet-stream"} + content = "content" + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_get("/projects") + assert result.content.decode("utf-8") == "content" + + +def test_get_request_404(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") + def resp_cont(url, request): + content = {"Here is wh it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_get("/not_there") + + +def test_get_request_invalid_data(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '["name": "project1"]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabParsingError): + gl.http_get("/projects") + + +def test_list_request(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/json", "X-Total": 1} + content = '[{"name": "project1"}]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_list("/projects", as_list=True) + assert isinstance(result, list) + assert len(result) == 1 + + with HTTMock(resp_cont): + result = gl.http_list("/projects", as_list=False) + assert isinstance(result, GitlabList) + assert len(result) == 1 + + with HTTMock(resp_cont): + result = gl.http_list("/projects", all=True) + assert isinstance(result, list) + assert len(result) == 1 + + +def test_list_request_404(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="get") + def resp_cont(url, request): + content = {"Here is why it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_list("/not_there") + + +def test_list_request_invalid_data(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '["name": "project1"]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabParsingError): + gl.http_list("/projects") + + +def test_post_request(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '{"name": "project1"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_post("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + +def test_post_request_404(gl): + @urlmatch( + scheme="http", netloc="localhost", path="/api/v4/not_there", method="post" + ) + def resp_cont(url, request): + content = {"Here is wh it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_post("/not_there") + + +def test_post_request_invalid_data(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="post") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '["name": "project1"]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabParsingError): + gl.http_post("/projects") + + +def test_put_request(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '{"name": "project1"}' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_put("/projects") + assert isinstance(result, dict) + assert result["name"] == "project1" + + +def test_put_request_404(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/not_there", method="put") + def resp_cont(url, request): + content = {"Here is wh it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_put("/not_there") + + +def test_put_request_invalid_data(gl): + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="put") + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = '["name": "project1"]' + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabParsingError): + gl.http_put("/projects") + + +def test_delete_request(gl): + @urlmatch( + scheme="http", netloc="localhost", path="/api/v4/projects", method="delete" + ) + def resp_cont(url, request): + headers = {"content-type": "application/json"} + content = "true" + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_cont): + result = gl.http_delete("/projects") + assert isinstance(result, requests.Response) + assert result.json() is True + + +def test_delete_request_404(gl): + @urlmatch( + scheme="http", netloc="localhost", path="/api/v4/not_there", method="delete" + ) + def resp_cont(url, request): + content = {"Here is wh it failed"} + return response(404, content, {}, None, 5, request) + + with HTTMock(resp_cont): + with pytest.raises(GitlabHttpError): + gl.http_delete("/not_there") diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py new file mode 100644 index 0000000..a2e5ff5 --- /dev/null +++ b/tests/unit/test_types.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from gitlab import types + + +def test_gitlab_attribute_get(): + o = types.GitlabAttribute("whatever") + assert o.get() == "whatever" + + o.set_from_cli("whatever2") + assert o.get() == "whatever2" + assert o.get_for_api() == "whatever2" + + o = types.GitlabAttribute() + assert o._value is None + + +def test_list_attribute_input(): + o = types.ListAttribute() + o.set_from_cli("foo,bar,baz") + assert o.get() == ["foo", "bar", "baz"] + + o.set_from_cli("foo") + assert o.get() == ["foo"] + + +def test_list_attribute_empty_input(): + o = types.ListAttribute() + o.set_from_cli("") + assert o.get() == [] + + o.set_from_cli(" ") + assert o.get() == [] + + +def test_list_attribute_get_for_api_from_cli(): + o = types.ListAttribute() + o.set_from_cli("foo,bar,baz") + assert o.get_for_api() == "foo,bar,baz" + + +def test_list_attribute_get_for_api_from_list(): + o = types.ListAttribute(["foo", "bar", "baz"]) + assert o.get_for_api() == "foo,bar,baz" + + +def test_list_attribute_get_for_api_from_int_list(): + o = types.ListAttribute([1, 9, 7]) + assert o.get_for_api() == "1,9,7" + + +def test_list_attribute_does_not_split_string(): + o = types.ListAttribute("foo") + assert o.get_for_api() == "foo" + + +def test_lowercase_string_attribute_get_for_api(): + o = types.LowercaseStringAttribute("FOO") + assert o.get_for_api() == "foo" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..dbe0838 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019 Gauvain Pocentek <gauvain@pocentek.net> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from gitlab import utils + + +def test_clean_str_id(): + src = "nothing_special" + dest = "nothing_special" + assert dest == utils.clean_str_id(src) + + src = "foo#bar/baz/" + dest = "foo%23bar%2Fbaz%2F" + assert dest == utils.clean_str_id(src) + + src = "foo%bar/baz/" + dest = "foo%25bar%2Fbaz%2F" + assert dest == utils.clean_str_id(src) + + +def test_sanitized_url(): + src = "http://localhost/foo/bar" + dest = "http://localhost/foo/bar" + assert dest == utils.sanitized_url(src) + + src = "http://localhost/foo.bar.baz" + dest = "http://localhost/foo%2Ebar%2Ebaz" + assert dest == utils.sanitized_url(src) |