From 4c496785cbc61f879a6f12e8a15671e65ec2b314 Mon Sep 17 00:00:00 2001 From: antn Date: Tue, 18 Nov 2025 01:44:34 -0800 Subject: [PATCH] Add ability to resend invitations --- tests/test_user_management.py | 56 +++++++++++++++++++++++++++++++++++ workos/user_management.py | 26 ++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/tests/test_user_management.py b/tests/test_user_management.py index eab3dee9..5bcb34fa 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -1194,3 +1194,59 @@ def test_revoke_invitation( ) assert request_kwargs["method"] == "post" assert isinstance(invitation, Invitation) + + def test_resend_invitation( + self, capture_and_mock_http_client_request, mock_invitation + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_invitation, 200 + ) + + invitation = syncify(self.user_management.resend_invitation("invitation_ABCDE")) + + assert request_kwargs["url"].endswith( + "user_management/invitations/invitation_ABCDE/resend" + ) + assert request_kwargs["method"] == "post" + assert isinstance(invitation, Invitation) + assert invitation.id == "invitation_ABCDE" + + def test_resend_invitation_not_found(self, capture_and_mock_http_client_request): + error_response = { + "message": "Invitation not found", + "code": "not_found", + } + capture_and_mock_http_client_request(self.http_client, error_response, 404) + + with pytest.raises(Exception): + syncify(self.user_management.resend_invitation("invitation_nonexistent")) + + def test_resend_invitation_expired(self, capture_and_mock_http_client_request): + error_response = { + "message": "Invite has expired.", + "code": "invite_expired", + } + capture_and_mock_http_client_request(self.http_client, error_response, 400) + + with pytest.raises(Exception): + syncify(self.user_management.resend_invitation("invitation_expired")) + + def test_resend_invitation_revoked(self, capture_and_mock_http_client_request): + error_response = { + "message": "Invite has been revoked.", + "code": "invite_revoked", + } + capture_and_mock_http_client_request(self.http_client, error_response, 400) + + with pytest.raises(Exception): + syncify(self.user_management.resend_invitation("invitation_revoked")) + + def test_resend_invitation_accepted(self, capture_and_mock_http_client_request): + error_response = { + "message": "Invite has already been accepted.", + "code": "invite_accepted", + } + capture_and_mock_http_client_request(self.http_client, error_response, 400) + + with pytest.raises(Exception): + syncify(self.user_management.resend_invitation("invitation_accepted")) diff --git a/workos/user_management.py b/workos/user_management.py index e5558726..13a50607 100644 --- a/workos/user_management.py +++ b/workos/user_management.py @@ -94,6 +94,7 @@ INVITATION_DETAIL_PATH = "user_management/invitations/{0}" INVITATION_DETAIL_BY_TOKEN_PATH = "user_management/invitations/by_token/{0}" INVITATION_REVOKE_PATH = "user_management/invitations/{0}/revoke" +INVITATION_RESEND_PATH = "user_management/invitations/{0}/resend" PASSWORD_RESET_PATH = "user_management/password_reset" PASSWORD_RESET_DETAIL_PATH = "user_management/password_reset/{0}" @@ -896,6 +897,17 @@ def revoke_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: """ ... + def resend_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: + """Resends an existing Invitation. + + Args: + invitation_id (str): The unique ID of the Invitation. + + Returns: + Invitation: Invitation response from WorkOS. + """ + ... + class UserManagement(UserManagementModule): _http_client: SyncHTTPClient @@ -1584,6 +1596,13 @@ def revoke_invitation(self, invitation_id: str) -> Invitation: return Invitation.model_validate(response) + def resend_invitation(self, invitation_id: str) -> Invitation: + response = self._http_client.request( + INVITATION_RESEND_PATH.format(invitation_id), method=REQUEST_METHOD_POST + ) + + return Invitation.model_validate(response) + class AsyncUserManagement(UserManagementModule): _http_client: AsyncHTTPClient @@ -2286,3 +2305,10 @@ async def revoke_invitation(self, invitation_id: str) -> Invitation: ) return Invitation.model_validate(response) + + async def resend_invitation(self, invitation_id: str) -> Invitation: + response = await self._http_client.request( + INVITATION_RESEND_PATH.format(invitation_id), method=REQUEST_METHOD_POST + ) + + return Invitation.model_validate(response)