diff --git a/README.md b/README.md index 1b24b654..7ab0b641 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,21 @@ try { ``` +To deliver the enchanted link by SMS, use the `WithPhone` variants, which take a phone +number as the login ID and return a `PhoneEnchantedLinkResponse` carrying `maskedPhone`. +The SMS carries only the correct link, so there is nothing for the user to choose. + +```java +PhoneEnchantedLinkResponse res = null; +try { + String uri = "http://myapp.com/verify-enchanted-link"; + res = els.signUpOrInWithPhone(phoneNumber, uri); +} catch (DescopeException de) { + // Handle the error +} + +``` + After sending the link, you must poll to receive a valid session using the `PendingRef` from the previous step. A valid session will be returned only after the user clicks the right link. diff --git a/src/main/java/com/descope/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index 00800e26..e55b2def 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -38,6 +38,7 @@ public static class AuthEndPoints { public static final String VERIFY_ENCHANTED_LINK = "/v1/auth/enchantedlink/verify"; public static final String ENCHANTED_LINK_SESSION = "/v1/auth/enchantedlink/pending-session"; public static final String UPDATE_EMAIL_ENCHANTED_LINK = "/v1/auth/enchantedlink/update/email"; + public static final String UPDATE_PHONE_ENCHANTED_LINK = "/v1/auth/enchantedlink/update/phone"; // TOTP public static final String SIGN_UP_TOTP_LINK = "/v1/auth/totp/signup"; diff --git a/src/main/java/com/descope/model/enchantedlink/PhoneEnchantedLinkResponse.java b/src/main/java/com/descope/model/enchantedlink/PhoneEnchantedLinkResponse.java new file mode 100644 index 00000000..7e5a0a0f --- /dev/null +++ b/src/main/java/com/descope/model/enchantedlink/PhoneEnchantedLinkResponse.java @@ -0,0 +1,14 @@ +package com.descope.model.enchantedlink; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PhoneEnchantedLinkResponse { + private String pendingRef; + private String linkId; + private String maskedPhone; +} diff --git a/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java b/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java index 26e29163..7f72a6d1 100644 --- a/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java +++ b/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java @@ -20,4 +20,10 @@ public class SignUpRequest { @JsonProperty("URI") private String uri; private SignUpOptions loginOptions; + private String phone; + + public SignUpRequest(String email, String loginId, User user, String uri, + SignUpOptions loginOptions) { + this(email, loginId, user, uri, loginOptions, null); + } } diff --git a/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java b/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java index f51e671b..b5d9e092 100644 --- a/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java +++ b/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java @@ -4,6 +4,7 @@ import com.descope.model.auth.AuthenticationInfo; import com.descope.model.auth.UpdateOptions; import com.descope.model.enchantedlink.EnchantedLinkResponse; +import com.descope.model.enchantedlink.PhoneEnchantedLinkResponse; import com.descope.model.magiclink.LoginOptions; import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; @@ -27,6 +28,23 @@ EnchantedLinkResponse signIn( LoginOptions loginOptions) throws DescopeException; + /** + * Use to login a user based on an enchanted link that will be sent by SMS. + * + * @param loginId - User login ID + * @param uri - Base URI + * @param token - when doing step-up or mfa then we need current session token + * @param loginOptions - {@link LoginOptions LoginOptions} + * @return pendingRef, linkId and masked phone + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse signInWithPhone( + String loginId, + String uri, + String token, + LoginOptions loginOptions) + throws DescopeException; + /** * Use to create a new user based on the given loginID either email or a phone. * @@ -52,6 +70,33 @@ EnchantedLinkResponse signUp(String loginId, String uri, User user) EnchantedLinkResponse signUp(String loginId, String uri, User user, SignUpOptions signupOptions) throws DescopeException; + /** + * Use to create a new user with a phone number as the loginID, verified by an enchanted link + * sent by SMS. + * + * @param loginId - User login ID, a phone number + * @param uri - Base URI + * @param user - {@link User User} + * @return pendingRef, linkId and masked phone + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse signUpWithPhone(String loginId, String uri, User user) + throws DescopeException; + + /** + * Use to create a new user with a phone number as the loginID, verified by an enchanted link + * sent by SMS. + * + * @param loginId - User login ID, a phone number + * @param uri - Base URI + * @param user - {@link User User} + * @param signupOptions - optional claims and template strings + * @return pendingRef, linkId and masked phone + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse signUpWithPhone(String loginId, String uri, User user, + SignUpOptions signupOptions) throws DescopeException; + /** * Use to login in using loginID, if user does not exist, a new user will be created. * @@ -63,6 +108,18 @@ EnchantedLinkResponse signUp(String loginId, String uri, User user, SignUpOption EnchantedLinkResponse signUpOrIn(String loginId, String uri) throws DescopeException; + /** + * Use to login in using a phone number as the loginID, if user does not exist, a new user will + * be created. The enchanted link is sent by SMS. + * + * @param loginId - User login ID, a phone number + * @param uri - Base URI + * @return pendingRef, linkId and masked phone + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse signUpOrInWithPhone(String loginId, String uri) + throws DescopeException; + /** * Use to get a session that was generated by SignIn/SignUp request. * @@ -110,4 +167,35 @@ EnchantedLinkResponse updateUserEmail(String loginId, String email, String uri, */ EnchantedLinkResponse updateUserEmail(String loginId, String email, String uri, String refreshToken, UpdateOptions updateOptions, Map templateOptions) throws DescopeException; + + /** + * Use to update phone and validate via enchanted link sent by SMS. + * + * @param loginId - User login ID + * @param phone - User phone number + * @param uri - Base URI + * @param refreshToken - refresh token to perform the update + * @param updateOptions - update options for the update + * @return {@link PhoneEnchantedLinkResponse} including masked address where the link was sent + * (phone), link to chose and link to retrieve new session from + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse updateUserPhone(String loginId, String phone, String uri, String refreshToken, + UpdateOptions updateOptions) throws DescopeException; + + /** + * Use to update phone and validate via enchanted link sent by SMS. + * + * @param loginId - User login ID + * @param phone - User phone number + * @param uri - Base URI + * @param refreshToken - refresh token to perform the update + * @param updateOptions - update options for the update + * @param templateOptions - optional parameters for template + * @return {@link PhoneEnchantedLinkResponse} including masked address where the link was sent + * (phone), link to chose and link to retrieve new session from + * @throws DescopeException - error upon failure + */ + PhoneEnchantedLinkResponse updateUserPhone(String loginId, String phone, String uri, String refreshToken, + UpdateOptions updateOptions, Map templateOptions) throws DescopeException; } diff --git a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java index cfb211b7..b649af4f 100644 --- a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java @@ -1,14 +1,18 @@ package com.descope.sdk.auth.impl; import static com.descope.enums.DeliveryMethod.EMAIL; +import static com.descope.enums.DeliveryMethod.SMS; import static com.descope.literals.Routes.AuthEndPoints.ENCHANTED_LINK_SESSION; import static com.descope.literals.Routes.AuthEndPoints.SIGN_IN_ENCHANTED_LINK; import static com.descope.literals.Routes.AuthEndPoints.SIGN_UP_ENCHANTED_LINK; import static com.descope.literals.Routes.AuthEndPoints.SIGN_UP_OR_IN_ENCHANTED_LINK; import static com.descope.literals.Routes.AuthEndPoints.UPDATE_EMAIL_ENCHANTED_LINK; +import static com.descope.literals.Routes.AuthEndPoints.UPDATE_PHONE_ENCHANTED_LINK; import static com.descope.literals.Routes.AuthEndPoints.VERIFY_ENCHANTED_LINK; import static com.descope.utils.PatternUtils.EMAIL_PATTERN; +import static com.descope.utils.PatternUtils.PHONE_PATTERN; +import com.descope.enums.DeliveryMethod; import com.descope.exception.DescopeException; import com.descope.exception.ServerCommonException; import com.descope.model.auth.AuthenticationInfo; @@ -17,12 +21,14 @@ import com.descope.model.enchantedlink.EmptyResponse; import com.descope.model.enchantedlink.EnchantedLinkResponse; import com.descope.model.enchantedlink.EnchantedLinkSessionBody; +import com.descope.model.enchantedlink.PhoneEnchantedLinkResponse; import com.descope.model.jwt.response.JWTResponse; import com.descope.model.magiclink.LoginOptions; import com.descope.model.magiclink.SignUpOptions; import com.descope.model.magiclink.request.SignInRequest; import com.descope.model.magiclink.request.SignUpRequest; import com.descope.model.magiclink.request.UpdateEmailRequest; +import com.descope.model.magiclink.request.UpdatePhoneRequest; import com.descope.model.magiclink.request.VerifyRequest; import com.descope.model.user.User; import com.descope.proxy.ApiProxy; @@ -41,10 +47,21 @@ class EnchantedLinkServiceImpl extends AuthenticationServiceImpl implements Ench @Override public EnchantedLinkResponse signIn(String loginId, String uri, String token, LoginOptions loginOptions) throws DescopeException { + return signInByDeliveryMethod(EMAIL, loginId, uri, token, loginOptions, EnchantedLinkResponse.class); + } + + @Override + public PhoneEnchantedLinkResponse signInWithPhone(String loginId, String uri, String token, + LoginOptions loginOptions) throws DescopeException { + return signInByDeliveryMethod(SMS, loginId, uri, token, loginOptions, PhoneEnchantedLinkResponse.class); + } + + private R signInByDeliveryMethod(DeliveryMethod deliveryMethod, String loginId, String uri, String token, + LoginOptions loginOptions, Class responseClass) throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } - URI enchantedLink = composeEnchantedLinkSignInURL(); + URI enchantedLink = composeEnchantedLinkSignInURL(deliveryMethod); SignInRequest signInRequest = new SignInRequest(uri, loginId, loginOptions); ApiProxy apiProxy; if (JwtUtils.isJWTRequired(loginOptions)) { @@ -55,7 +72,7 @@ public EnchantedLinkResponse signIn(String loginId, String uri, String token, Lo } else { apiProxy = getApiProxy(); } - return apiProxy.post(enchantedLink, signInRequest, EnchantedLinkResponse.class); + return apiProxy.post(enchantedLink, signInRequest, responseClass); } @Override @@ -67,32 +84,67 @@ public EnchantedLinkResponse signUp(String loginId, String uri, User user) @Override public EnchantedLinkResponse signUp(String loginId, String uri, User user, SignUpOptions signupOptions) throws DescopeException { + return signUpByDeliveryMethod(EMAIL, loginId, uri, user, signupOptions, EnchantedLinkResponse.class); + } + + @Override + public PhoneEnchantedLinkResponse signUpWithPhone(String loginId, String uri, User user) + throws DescopeException { + return signUpWithPhone(loginId, uri, user, null); + } + + @Override + public PhoneEnchantedLinkResponse signUpWithPhone(String loginId, String uri, User user, + SignUpOptions signupOptions) throws DescopeException { + return signUpByDeliveryMethod(SMS, loginId, uri, user, signupOptions, PhoneEnchantedLinkResponse.class); + } + + private R signUpByDeliveryMethod(DeliveryMethod deliveryMethod, String loginId, String uri, User user, + SignUpOptions signupOptions, Class responseClass) throws DescopeException { if (user == null) { user = new User(); } - URI enchantedLinkSignUpURL = composeEnchantedLinkSignUpURL(); + URI enchantedLinkSignUpURL = composeEnchantedLinkSignUpURL(deliveryMethod); SignUpRequest.SignUpRequestBuilder signUpRequestBuilder = - SignUpRequest.builder().loginId(loginId).uri(uri).user(user).email(loginId); - if (StringUtils.isBlank(user.getEmail())) { - user.setEmail(loginId); + SignUpRequest.builder().loginId(loginId).uri(uri); + if (deliveryMethod == SMS) { + signUpRequestBuilder.phone(loginId); + if (StringUtils.isBlank(user.getPhone())) { + user.setPhone(loginId); + } + } else { + signUpRequestBuilder.email(loginId); + if (StringUtils.isBlank(user.getEmail())) { + user.setEmail(loginId); + } } if (signupOptions != null) { signUpRequestBuilder.loginOptions(signupOptions); } SignUpRequest signUpRequest = signUpRequestBuilder.user(user).build(); ApiProxy apiProxy = getApiProxy(); - return apiProxy.post(enchantedLinkSignUpURL, signUpRequest, EnchantedLinkResponse.class); + return apiProxy.post(enchantedLinkSignUpURL, signUpRequest, responseClass); } @Override public EnchantedLinkResponse signUpOrIn(String loginId, String uri) throws DescopeException { + return signUpOrInByDeliveryMethod(EMAIL, loginId, uri, EnchantedLinkResponse.class); + } + + @Override + public PhoneEnchantedLinkResponse signUpOrInWithPhone(String loginId, String uri) throws DescopeException { + return signUpOrInByDeliveryMethod(SMS, loginId, uri, PhoneEnchantedLinkResponse.class); + } + + private R signUpOrInByDeliveryMethod(DeliveryMethod deliveryMethod, String loginId, String uri, + Class responseClass) throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } - URI magicLinkSignUpOrInURL = composeEnchantedLinkSignUpOrInURL(); + URI enchantedLinkSignUpOrInURL = composeEnchantedLinkSignUpOrInURL(deliveryMethod); SignInRequest signInRequest = new SignInRequest(uri, loginId, null); ApiProxy apiProxy = getApiProxy(); - return apiProxy.post(magicLinkSignUpOrInURL, signInRequest, EnchantedLinkResponse.class); + return apiProxy.post(enchantedLinkSignUpOrInURL, signInRequest, responseClass); } @Override @@ -152,20 +204,61 @@ public EnchantedLinkResponse updateUserEmail(String loginId, String email, Strin return apiProxy.post(magicLinkUpdateUserEmail, updateEmailRequest, EnchantedLinkResponse.class); } + @Override + public PhoneEnchantedLinkResponse updateUserPhone(String loginId, String phone, String uri, String refreshToken, + UpdateOptions updateOptions) throws DescopeException { + return updateUserPhone(loginId, phone, uri, refreshToken, updateOptions, null); + } + + @Override + public PhoneEnchantedLinkResponse updateUserPhone(String loginId, String phone, String uri, String refreshToken, + UpdateOptions updateOptions, Map templateOptions) throws DescopeException { + if (StringUtils.isBlank(loginId)) { + throw ServerCommonException.invalidArgument("Login ID"); + } + if (StringUtils.isBlank(phone) || !PHONE_PATTERN.matcher(phone).matches()) { + throw ServerCommonException.invalidArgument("Phone"); + } + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.invalidArgument("Refresh Token"); + } + URI enchantedLinkUpdateUserPhone = composeUpdateUserPhoneEnchantedLink(); + if (updateOptions == null) { + updateOptions = new UpdateOptions(); + } + UpdatePhoneRequest updatePhoneRequest = + UpdatePhoneRequest.builder() + .phone(phone) + .uri(uri) + .loginId(loginId) + .crossDevice(false) + .addToLoginIds(updateOptions.isAddToLoginIds()) + .onMergeUseExisting(updateOptions.isOnMergeUseExisting()) + .templateOptions(templateOptions) + .build(); + + ApiProxy apiProxy = getApiProxy(refreshToken); + return apiProxy.post(enchantedLinkUpdateUserPhone, updatePhoneRequest, PhoneEnchantedLinkResponse.class); + } + private URI composeUpdateUserEmailEnchantedLink() { return getUri(UPDATE_EMAIL_ENCHANTED_LINK); } - private URI composeEnchantedLinkSignInURL() { - return composeURI(SIGN_IN_ENCHANTED_LINK, EMAIL.getValue()); + private URI composeUpdateUserPhoneEnchantedLink() { + return composeURI(UPDATE_PHONE_ENCHANTED_LINK, SMS.getValue()); + } + + private URI composeEnchantedLinkSignInURL(DeliveryMethod deliveryMethod) { + return composeURI(SIGN_IN_ENCHANTED_LINK, deliveryMethod.getValue()); } - private URI composeEnchantedLinkSignUpURL() { - return composeURI(SIGN_UP_ENCHANTED_LINK, EMAIL.getValue()); + private URI composeEnchantedLinkSignUpURL(DeliveryMethod deliveryMethod) { + return composeURI(SIGN_UP_ENCHANTED_LINK, deliveryMethod.getValue()); } - private URI composeEnchantedLinkSignUpOrInURL() { - return composeURI(SIGN_UP_OR_IN_ENCHANTED_LINK, EMAIL.getValue()); + private URI composeEnchantedLinkSignUpOrInURL(DeliveryMethod deliveryMethod) { + return composeURI(SIGN_UP_OR_IN_ENCHANTED_LINK, deliveryMethod.getValue()); } private URI composeVerifyEnchantedLinkURL() { diff --git a/src/test/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImplTest.java b/src/test/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImplTest.java index 2e510564..9aa879ff 100644 --- a/src/test/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImplTest.java +++ b/src/test/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImplTest.java @@ -4,6 +4,8 @@ import static com.descope.sdk.TestUtils.MOCK_EMAIL; import static com.descope.sdk.TestUtils.MOCK_JWT_RESPONSE; import static com.descope.sdk.TestUtils.MOCK_MASKED_EMAIL; +import static com.descope.sdk.TestUtils.MOCK_MASKED_PHONE; +import static com.descope.sdk.TestUtils.MOCK_PHONE; import static com.descope.sdk.TestUtils.MOCK_REFRESH_TOKEN; import static com.descope.sdk.TestUtils.MOCK_SIGNING_KEY; import static com.descope.sdk.TestUtils.MOCK_TOKEN; @@ -19,6 +21,8 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.descope.exception.RateLimitExceededException; import com.descope.exception.ServerCommonException; @@ -26,8 +30,12 @@ import com.descope.model.client.Client; import com.descope.model.enchantedlink.EmptyResponse; import com.descope.model.enchantedlink.EnchantedLinkResponse; +import com.descope.model.enchantedlink.PhoneEnchantedLinkResponse; import com.descope.model.jwt.Token; import com.descope.model.jwt.response.SigningKeysResponse; +import com.descope.model.magiclink.request.SignInRequest; +import com.descope.model.magiclink.request.SignUpRequest; +import com.descope.model.magiclink.request.UpdatePhoneRequest; import com.descope.model.user.User; import com.descope.model.user.request.UserRequest; import com.descope.model.user.response.EnchantedLinkTestUserResponse; @@ -40,12 +48,14 @@ import com.descope.sdk.mgmt.impl.ManagementServiceBuilder; import com.descope.utils.JwtUtils; import com.descope.utils.UriUtils; +import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; 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 EnchantedLinkServiceImplTest { @@ -170,6 +180,120 @@ void testUpdateUserEmailForSuccess() { } } + @Test + void signUpWithPhoneSendsPhoneBodyToSmsRoute() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(new PhoneEnchantedLinkResponse(MOCK_URL, MOCK_URL, MOCK_MASKED_PHONE)) + .when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + + PhoneEnchantedLinkResponse response = + enchantedLinkService.signUpWithPhone(MOCK_PHONE, MOCK_DOMAIN, null); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(SignUpRequest.class); + verify(apiProxy, times(1)).post(uriCaptor.capture(), bodyCaptor.capture(), + eq(PhoneEnchantedLinkResponse.class)); + assertThat(uriCaptor.getValue().getPath()).isEqualTo("/v1/auth/enchantedlink/signup/sms"); + SignUpRequest sent = bodyCaptor.getValue(); + assertThat(sent.getPhone()).isEqualTo(MOCK_PHONE); + assertThat(sent.getLoginId()).isEqualTo(MOCK_PHONE); + assertThat(sent.getEmail()).isNull(); + assertThat(sent.getUri()).isEqualTo(MOCK_DOMAIN); + assertThat(sent.getUser().getPhone()).isEqualTo(MOCK_PHONE); + assertThat(response.getMaskedPhone()).isEqualTo(MOCK_MASKED_PHONE); + } + } + + @Test + void signInWithPhoneSendsLoginIdToSmsRoute() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(new PhoneEnchantedLinkResponse(MOCK_URL, MOCK_URL, MOCK_MASKED_PHONE)) + .when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + + PhoneEnchantedLinkResponse response = + enchantedLinkService.signInWithPhone(MOCK_PHONE, MOCK_DOMAIN, null, null); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(SignInRequest.class); + verify(apiProxy, times(1)).post(uriCaptor.capture(), bodyCaptor.capture(), + eq(PhoneEnchantedLinkResponse.class)); + assertThat(uriCaptor.getValue().getPath()).isEqualTo("/v1/auth/enchantedlink/signin/sms"); + SignInRequest sent = bodyCaptor.getValue(); + assertThat(sent.getLoginId()).isEqualTo(MOCK_PHONE); + assertThat(sent.getUri()).isEqualTo(MOCK_DOMAIN); + assertThat(response.getMaskedPhone()).isEqualTo(MOCK_MASKED_PHONE); + } + } + + @Test + void signUpOrInWithPhoneSendsLoginIdToSmsRoute() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(new PhoneEnchantedLinkResponse(MOCK_URL, MOCK_URL, MOCK_MASKED_PHONE)) + .when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + + PhoneEnchantedLinkResponse response = + enchantedLinkService.signUpOrInWithPhone(MOCK_PHONE, MOCK_DOMAIN); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(SignInRequest.class); + verify(apiProxy, times(1)).post(uriCaptor.capture(), bodyCaptor.capture(), + eq(PhoneEnchantedLinkResponse.class)); + assertThat(uriCaptor.getValue().getPath()).isEqualTo("/v1/auth/enchantedlink/signup-in/sms"); + SignInRequest sent = bodyCaptor.getValue(); + assertThat(sent.getLoginId()).isEqualTo(MOCK_PHONE); + assertThat(sent.getUri()).isEqualTo(MOCK_DOMAIN); + assertThat(response.getMaskedPhone()).isEqualTo(MOCK_MASKED_PHONE); + } + } + + @Test + void updateUserPhoneSendsPhoneBodyToSmsRoute() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(new PhoneEnchantedLinkResponse(MOCK_URL, MOCK_URL, MOCK_MASKED_PHONE)) + .when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + + PhoneEnchantedLinkResponse response = enchantedLinkService.updateUserPhone( + MOCK_EMAIL, MOCK_PHONE, MOCK_DOMAIN, MOCK_REFRESH_TOKEN, null); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(UpdatePhoneRequest.class); + verify(apiProxy, times(1)).post(uriCaptor.capture(), bodyCaptor.capture(), + eq(PhoneEnchantedLinkResponse.class)); + assertThat(uriCaptor.getValue().getPath()) + .isEqualTo("/v1/auth/enchantedlink/update/phone/sms"); + UpdatePhoneRequest sent = bodyCaptor.getValue(); + assertThat(sent.getPhone()).isEqualTo(MOCK_PHONE); + assertThat(sent.getLoginId()).isEqualTo(MOCK_EMAIL); + assertThat(sent.getUri()).isEqualTo(MOCK_DOMAIN); + assertThat(response.getMaskedPhone()).isEqualTo(MOCK_MASKED_PHONE); + } + } + + @Test + void updateUserPhoneRejectsInvalidArguments() { + assertEquals("The Login ID argument is invalid", assertThrows(ServerCommonException.class, + () -> enchantedLinkService.updateUserPhone("", MOCK_PHONE, MOCK_DOMAIN, MOCK_REFRESH_TOKEN, null)) + .getMessage()); + assertEquals("The Phone argument is invalid", assertThrows(ServerCommonException.class, + () -> enchantedLinkService.updateUserPhone(MOCK_EMAIL, "abc", MOCK_DOMAIN, MOCK_REFRESH_TOKEN, null)) + .getMessage()); + assertEquals("The Refresh Token argument is invalid", assertThrows(ServerCommonException.class, + () -> enchantedLinkService.updateUserPhone(MOCK_EMAIL, MOCK_PHONE, MOCK_DOMAIN, "", null)) + .getMessage()); + } + @Test void testGetSession() { ApiProxy apiProxy = mock(ApiProxy.class);