From 65e4209d5e1e3af2c0973808596eaf286c096893 Mon Sep 17 00:00:00 2001 From: Aviad Lichtenstadt Date: Fri, 14 Aug 2026 01:38:15 +0300 Subject: [PATCH 1/2] feat(mgmt): add failOnConflict parameter to updateEmail/updatePhone (#360) 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: Aviad Lichtenstadt 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, From ed1cb76bff8d6fd8b2f21bcc7d74fbb30dd1d446 Mon Sep 17 00:00:00 2001 From: "descope[bot]" <107609351+descope[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:57:36 +0000 Subject: [PATCH 2/2] fix(deps): update dependency org.apache.httpcomponents.client5:httpclient5 to v5.6.3 [security] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 81acad37..92fb4654 100644 --- a/pom.xml +++ b/pom.xml @@ -141,7 +141,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.6.2 + 5.6.3 jackson-databind