diff --git a/docs/gl_objects/users.rst b/docs/gl_objects/users.rst index 5ebfa296b..c44262f0c 100644 --- a/docs/gl_objects/users.rst +++ b/docs/gl_objects/users.rst @@ -435,6 +435,53 @@ Delete an email for a user:: # or email.delete() +Current user applications +========================= + +References +---------- + +You can manage OAuth applications owned by the current user (GitLab 19.0 and +later). To manage instance-wide applications, see :doc:`applications`. + +* v4 API: + + + :class:`gitlab.v4.objects.CurrentUserApplication` + + :class:`gitlab.v4.objects.CurrentUserApplicationManager` + + :attr:`gitlab.v4.objects.CurrentUser.applications` + +* GitLab API: https://docs.gitlab.com/api/user_applications + +Examples +-------- + +List the current user's applications:: + + applications = gl.user.applications.list(get_all=True) + +Get an application:: + + application = gl.user.applications.get(app_id) + +Create an application:: + + application = gl.user.applications.create({ + 'name': 'your_app', + 'redirect_uri': 'http://application.url', + 'scopes': 'read_user openid profile email', + }) + +Update an application:: + + application.name = 'new_name' + application.save() + +Delete an application:: + + gl.user.applications.delete(app_id) + # or + application.delete() + Users activities ================ diff --git a/gitlab/v4/objects/users.py b/gitlab/v4/objects/users.py index 3978b8aba..0c9c99375 100644 --- a/gitlab/v4/objects/users.py +++ b/gitlab/v4/objects/users.py @@ -33,6 +33,8 @@ from .personal_access_tokens import UserPersonalAccessTokenManager # noqa: F401 __all__ = [ + "CurrentUserApplication", + "CurrentUserApplicationManager", "CurrentUserEmail", "CurrentUserEmailManager", "CurrentUserGPGKey", @@ -73,6 +75,19 @@ ] +class CurrentUserApplication(SaveMixin, ObjectDeleteMixin, RESTObject): + _repr_attr = "application_name" + + +class CurrentUserApplicationManager(CRUDMixin[CurrentUserApplication]): + _path = "/user/applications" + _obj_cls = CurrentUserApplication + _create_attrs = RequiredOptional( + required=("name", "redirect_uri", "scopes"), optional=("confidential",) + ) + _update_attrs = RequiredOptional(optional=("name", "scopes")) + + class CurrentUserEmail(ObjectDeleteMixin, RESTObject): _repr_attr = "email" @@ -157,6 +172,7 @@ class CurrentUser(RESTObject): _id_attr = None _repr_attr = "username" + applications: CurrentUserApplicationManager emails: CurrentUserEmailManager gpgkeys: CurrentUserGPGKeyManager keys: CurrentUserKeyManager diff --git a/tests/unit/objects/test_current_user_applications.py b/tests/unit/objects/test_current_user_applications.py new file mode 100644 index 000000000..117e93ee3 --- /dev/null +++ b/tests/unit/objects/test_current_user_applications.py @@ -0,0 +1,137 @@ +""" +GitLab API: https://docs.gitlab.com/api/user_applications +""" + +import pytest +import responses + +from gitlab.v4.objects import CurrentUserApplication + +application_content = { + "id": 1, + "application_id": "5832fc6e14300a0d962240a8144466eef4ee93ef", + "application_name": "MyApplication", + "callback_url": "http://redirect.uri", + "confidential": True, +} +updated_content = {**application_content, "application_name": "UpdatedApplication"} + + +@pytest.fixture +def resp_create_application(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/user/applications", + json={**application_content, "secret": "secret"}, + content_type="application/json", + status=201, + match=[ + responses.matchers.json_params_matcher( + { + "name": "MyApplication", + "redirect_uri": "http://redirect.uri", + "scopes": "api read_user", + "confidential": True, + } + ) + ], + ) + yield rsps + + +@pytest.fixture +def resp_list_application(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/user/applications", + json=[application_content], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_application(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/user/applications/1", + json=application_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_update_application(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/user/applications/1", + json=application_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/user/applications/1", + json=updated_content, + content_type="application/json", + status=200, + match=[ + responses.matchers.json_params_matcher({"name": "UpdatedApplication"}) + ], + ) + yield rsps + + +@pytest.fixture +def resp_delete_application(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/user/applications/1", + status=204, + ) + yield rsps + + +def test_create_current_user_application(current_user, resp_create_application): + application = current_user.applications.create( + { + "name": "MyApplication", + "redirect_uri": "http://redirect.uri", + "scopes": "api read_user", + "confidential": True, + } + ) + assert isinstance(application, CurrentUserApplication) + assert application.id == 1 + assert application.secret == "secret" + + +def test_list_current_user_applications(current_user, resp_list_application): + applications = current_user.applications.list() + assert isinstance(applications[0], CurrentUserApplication) + assert applications[0].application_name == "MyApplication" + + +def test_get_current_user_application(current_user, resp_get_application): + application = current_user.applications.get(1) + assert isinstance(application, CurrentUserApplication) + assert application.callback_url == "http://redirect.uri" + + +def test_update_current_user_application(current_user, resp_update_application): + application = current_user.applications.get(1) + application.name = "UpdatedApplication" + application.save() + assert application.application_name == "UpdatedApplication" + + +def test_delete_current_user_application(current_user, resp_delete_application): + current_user.applications.delete(1)