From 24590fdcf50e40566e8eee980345cc96a9bb310b Mon Sep 17 00:00:00 2001 From: Aviad Lichtenstadt Date: Thu, 13 Aug 2026 21:28:01 +0000 Subject: [PATCH] feat(mgmt): add failOnConflict parameter to updateEmail/updatePhone The backend silently merges and deletes the conflicting user when failOnConflict is omitted, matching the option already exposed by the Node, Python, and Go SDKs. New overloads preserve backward compatibility for existing 3-arg callers. Co-Authored-By: Claude Sonnet 5 --- .../com/descope/sdk/mgmt/UserService.java | 32 +++++++++++++++++++ .../sdk/mgmt/impl/UserServiceImpl.java | 14 ++++++++ .../sdk/mgmt/impl/UserServiceImplTest.java | 31 ++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index e096f281..65a2edd2 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -236,6 +236,22 @@ public interface UserService { UserResponseDetails updateEmail(String loginId, String email, Boolean isVerified) throws DescopeException; + /** + * Update the email address for an existing user. + * + * @param loginId The loginID is required. + * @param email The email parameter can be empty in which case the email will be removed. + * @param isVerified The isVerified flag must be true for the user to be able to login with the + * email address. + * @param failOnConflict If true, the call will fail with an error instead of silently deleting + * and merging another user that already has this email address. + * @return {@link UserResponseDetails UserResponseDetails} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + UserResponseDetails updateEmail(String loginId, String email, Boolean isVerified, Boolean failOnConflict) + throws DescopeException; + /** * Update the email address for an existing user. * @@ -250,6 +266,22 @@ UserResponseDetails updateEmail(String loginId, String email, Boolean isVerified UserResponseDetails updatePhone(String loginId, String phone, Boolean isVerified) throws DescopeException; + /** + * Update the email address for an existing user. + * + * @param loginId The loginID is required. + * @param phone The phone parameter can be empty in which case the phone will be removed. + * @param isVerified The isVerified flag must be true for the user to be able to login with the + * email address. + * @param failOnConflict If true, the call will fail with an error instead of silently deleting + * and merging another user that already has this phone number. + * @return {@link UserResponseDetails UserResponseDetails} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + UserResponseDetails updatePhone(String loginId, String phone, Boolean isVerified, Boolean failOnConflict) + throws DescopeException; + /** * Update an existing user's display name (i.e., their full name). * diff --git a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java index 47cc8159..a73109de 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -382,22 +382,36 @@ public UserResponseDetails deactivate(String loginId) throws DescopeException { @Override public UserResponseDetails updateEmail(String loginId, String email, Boolean isVerified) throws DescopeException { + return updateEmail(loginId, email, isVerified, null); + } + + @Override + public UserResponseDetails updateEmail(String loginId, String email, Boolean isVerified, Boolean failOnConflict) + throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } URI updateEmailUri = composeUpdateEmailUri(); Map request = mapOf("loginId", loginId, "email", email, "verified", isVerified); + addIfNotNull(request, "failOnConflict", failOnConflict); ApiProxy apiProxy = getApiProxy(); return apiProxy.post(updateEmailUri, request, UserResponseDetails.class); } @Override public UserResponseDetails updatePhone(String loginId, String phone, Boolean isVerified) throws DescopeException { + return updatePhone(loginId, phone, isVerified, null); + } + + @Override + public UserResponseDetails updatePhone(String loginId, String phone, Boolean isVerified, Boolean failOnConflict) + throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } URI updatePhoneUri = composeUpdatePhoneUri(); Map request = mapOf("loginId", loginId, "phone", phone, "verified", isVerified); + addIfNotNull(request, "failOnConflict", failOnConflict); ApiProxy apiProxy = getApiProxy(); return apiProxy.post(updatePhoneUri, request, UserResponseDetails.class); } diff --git a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java index 6d6400d0..bbb82b4b 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java @@ -72,6 +72,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.RetryingTest; +import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; public class UserServiceImplTest { @@ -367,6 +368,21 @@ void testUpdateEmailForSuccess() { } } + @Test + void testUpdateEmailWithFailOnConflictForSuccess() { + UserResponseDetails userResponseDetails = mock(UserResponseDetails.class); + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(userResponseDetails).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + UserResponseDetails response = userService.updateEmail("someLoginId", "someEmail", false, true); + Assertions.assertThat(response).isNotNull(); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(apiProxy).post(any(), captor.capture(), any()); + assertEquals(true, captor.getValue().get("failOnConflict")); + } + } + @Test void testUpdatePhoneForEmptyLoginId() { ServerCommonException thrown = assertThrows(ServerCommonException.class, @@ -387,6 +403,21 @@ void testUpdatePhoneForSuccess() { } } + @Test + void testUpdatePhoneWithFailOnConflictForSuccess() { + UserResponseDetails userResponseDetails = mock(UserResponseDetails.class); + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(userResponseDetails).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + UserResponseDetails response = userService.updatePhone("someLoginId", "1234567890", false, true); + Assertions.assertThat(response).isNotNull(); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Map.class); + verify(apiProxy).post(any(), captor.capture(), any()); + assertEquals(true, captor.getValue().get("failOnConflict")); + } + } + @Test void testUpdateDisplayNameForEmptyLoginId() { ServerCommonException thrown = assertThrows(ServerCommonException.class,