From a4b718a0777a097a9f7f987ddbfc7041cbbdf0ce Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 12 Aug 2026 15:59:34 -0700 Subject: [PATCH 01/22] Re-enable Native Auth SSPR tests with Mail.tm Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- common | 2 +- ...AuthPublicClientApplicationAbstractTest.kt | 5 +- .../e2e/tests/network/nativeauth/SSPRTest.kt | 112 ++++++++---------- .../network/nativeauth/SignInEmailOTPTest.kt | 8 +- .../tests/network/nativeauth/SignInJITTest.kt | 18 +-- .../tests/network/nativeauth/SignInMFATest.kt | 6 +- .../SignUpEmailOTPAttributesTest.kt | 12 +- .../network/nativeauth/SignUpEmailOTPTest.kt | 18 +-- .../SignUpEmailPasswordAttributesTest.kt | 12 +- .../nativeauth/SignUpEmailPasswordTest.kt | 24 ++-- 10 files changed, 109 insertions(+), 108 deletions(-) diff --git a/common b/common index ecb5b71ef5..90a68a7054 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit ecb5b71ef58463e268a1119adec6aafc6b018486 +Subproject commit 90a68a70543c309660dd467232eef910082d7f3f diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index 354d235100..ccf18d27b5 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -35,6 +35,7 @@ import com.microsoft.identity.common.internal.controllers.CommandDispatcherHelpe import com.microsoft.identity.common.java.nativeauth.BuildValues import com.microsoft.identity.internal.testutils.TestUtils import com.microsoft.identity.internal.testutils.nativeauth.ConfigType +import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.labapi.utilities.BuildConfig import com.microsoft.identity.labapi.utilities.authentication.LabApiAuthenticationClient @@ -76,6 +77,7 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl private lateinit var context: Context private lateinit var activity: Activity + protected val tempEmailApi = TemporaryEmailService(BuildValues.getEmailProviderPassword()) // Remove default Coroutine test timeout of 10 seconds. private val testDispatcher = StandardTestDispatcher() @@ -161,8 +163,7 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl authFlow() shouldRetry = false // authFlow() has succeeded, so we don't need to retry. } catch (e: Exception) { - //1secmail occasionally has a delay for emails to arrive / return from the API, or throws an internal server error, which causes tests to fail - //In this case, retry the test + // Retry transient end-to-end flow failures. if (retryCount >= maxRetries) { Assert.fail(e.message) shouldRetry = false diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt index 4d26af3e85..a5d578d047 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthResetPasswordParameters @@ -42,8 +41,6 @@ import org.junit.Test class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -55,7 +52,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Verify email with email OTP first and then reset password. * (hero scenario 8 & 17, use case 3.1.1) */ - @Ignore("Retrieving OTP code failure") @Test fun testSSPRSuccess() { config = getConfig(defaultConfigType) @@ -63,21 +59,20 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - retryOperation { - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - result = application.resetPassword(param) - assertResult(result) - - val otp = tempEmailApi.retrieveCodeFromInbox(user) - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) - assertResult(submitCodeResult) - - val password = getSafePassword() - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) - } + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp = tempEmailApi.retrieveCodeFromInbox(user) + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) + assertResult(submitCodeResult) + + val password = getSafePassword() + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) } } @@ -85,7 +80,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * New password being set doesn’t meet password complexity requirements set on portal * (use case 3.1.3) */ - @Ignore("Retrieving OTP code failure.") @Test fun testErrorInvalidPasswordFormat() { config = getConfig(defaultConfigType) @@ -93,22 +87,21 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - retryOperation { - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - result = application.resetPassword(param) - assertResult(result) - - val otp = tempEmailApi.retrieveCodeFromInbox(user) - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) - assertResult(submitCodeResult) - - val password = INVALID_PASSWORD - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError) - Assert.assertTrue((submitPasswordResult as ResetPasswordSubmitPasswordError).isInvalidPassword()) - } + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp = tempEmailApi.retrieveCodeFromInbox(user) + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) + assertResult(submitCodeResult) + + val password = INVALID_PASSWORD + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError) + Assert.assertTrue((submitPasswordResult as ResetPasswordSubmitPasswordError).isInvalidPassword()) } } @@ -116,7 +109,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Resend Code. * (use case 3.1.4) */ - @Ignore("Retrieving OTP code failure.") @Test fun testResendCode() { config = getConfig(defaultConfigType) @@ -124,28 +116,28 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - retryOperation { - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - result = application.resetPassword(param) - assertResult(result) - - val otp1 = tempEmailApi.retrieveCodeFromInbox(user) - val codeRequiredState = (result as ResetPasswordStartResult.CodeRequired).nextState - val resendCodeResult = codeRequiredState.resendCode() - assertResult(resendCodeResult) - - val otp2 = tempEmailApi.retrieveCodeFromInbox(user) - Assert.assertNotEquals(otp1, otp2) - - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp2) - assertResult(submitCodeResult) - - val password = getSafePassword() - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) - } + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp1 = tempEmailApi.retrieveCodeFromInbox(user) + val codeRequiredState = (result as ResetPasswordStartResult.CodeRequired).nextState + tempEmailApi.markCheckpoint(user) + val resendCodeResult = codeRequiredState.resendCode() + assertResult(resendCodeResult) + + val otp2 = tempEmailApi.retrieveCodeFromInbox(user) + Assert.assertNotEquals(otp1, otp2) + + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp2) + assertResult(submitCodeResult) + + val password = getSafePassword() + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) } } @@ -159,7 +151,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) runBlocking { - val username = tempEmailApi.generateRandomEmailAddressLocally() + val username = tempEmailApi.generateRandomUnregisteredEmailAddress() val param = NativeAuthResetPasswordParameters(username = username) val result = application.resetPassword(param) Assert.assertTrue(result is ResetPasswordError) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInEmailOTPTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInEmailOTPTest.kt index 5db7671f5c..c9245cdf29 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInEmailOTPTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInEmailOTPTest.kt @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthSignInParameters @@ -40,8 +39,6 @@ import org.junit.Test class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -63,6 +60,7 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { runBlocking { val user = config.email val param = NativeAuthSignInParameters(username = user) + tempEmailApi.markCheckpoint(user) val signInResult = application.signIn(param) assertResult(signInResult) val otp = tempEmailApi.retrieveCodeFromInbox(user) @@ -83,7 +81,7 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { - val username = tempEmailApi.generateRandomEmailAddressLocally() + val username = tempEmailApi.generateRandomUnregisteredEmailAddress() val param = NativeAuthSignInParameters(username = username) val signInResult = application.signIn(param) Assert.assertTrue(signInResult is SignInError) @@ -146,11 +144,13 @@ class SignInEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { runBlocking { val user = config.email val param = NativeAuthSignInParameters(username = user) + tempEmailApi.markCheckpoint(user) val signInResult = application.signIn(param) assertResult(signInResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(user) val codeRequiredState = (signInResult as SignInResult.CodeRequired).nextState + tempEmailApi.markCheckpoint(user) val resendCodeResult = codeRequiredState.resendCode() assertResult(resendCodeResult) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt index 09d90c9fea..8f92db5680 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInJITTest.kt @@ -26,7 +26,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.claims.ClaimsRequest import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthChallengeAuthMethodParameters @@ -47,8 +46,6 @@ import org.junit.Test class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - private lateinit var resources: List lateinit var application: INativeAuthPublicClientApplication @@ -80,9 +77,10 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // SignUp a new user with username and password - val username = tempEmailApi.generateRandomEmailAddressLocally() + val username = tempEmailApi.createRandomEmailAddress() val signUpParams = NativeAuthSignUpParameters(username) signUpParams.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(username) val signUpResult = application.signUp(signUpParams) assertResult(signUpResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(username) @@ -97,12 +95,13 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { assertResult(signInResult) val authMethod = (signInResult as SignInResult.StrongAuthMethodRegistrationRequired).authMethods[0] - val contact = tempEmailApi.generateRandomEmailAddressLocally() + val contact = tempEmailApi.createRandomEmailAddress() // Specify a different email as verification contact. val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod, contact) // Complete JIT. Verification email should be sent to the second email. + tempEmailApi.markCheckpoint(contact) val challengeResult = signInResult.nextState.challengeAuthMethod(authMethodParams) val otp2 = tempEmailApi.retrieveCodeFromInbox(contact) val submitChallengeResult = (challengeResult as RegisterStrongAuthChallengeResult.VerificationRequired).result.getNextState().submitChallenge(otp2) @@ -141,9 +140,10 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // SignUp a new user with username and password. - val username = tempEmailApi.generateRandomEmailAddressLocally() + val username = tempEmailApi.createRandomEmailAddress() val signUpParams = NativeAuthSignUpParameters(username) signUpParams.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(username) val signUpResult = application.signUp(signUpParams) assertResult(signUpResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(username) @@ -159,12 +159,13 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { assertResult(signWithContinuationResult) val authMethod = (signWithContinuationResult as SignInResult.StrongAuthMethodRegistrationRequired).authMethods[0] - val contact = tempEmailApi.generateRandomEmailAddressLocally() + val contact = tempEmailApi.createRandomEmailAddress() // Specify a different email as verification contact. val authMethodParams = NativeAuthChallengeAuthMethodParameters(authMethod, contact) // Complete JIT. Verification email should be sent to the second email. + tempEmailApi.markCheckpoint(contact) val challengeResult = signWithContinuationResult.nextState.challengeAuthMethod(authMethodParams) val otp2 = tempEmailApi.retrieveCodeFromInbox(contact) val submitChallengeResult = (challengeResult as RegisterStrongAuthChallengeResult.VerificationRequired).result.getNextState().submitChallenge(otp2) @@ -204,9 +205,10 @@ class SignInJITTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // SignUp a new user with username and password - val username = tempEmailApi.generateRandomEmailAddressLocally() + val username = tempEmailApi.createRandomEmailAddress() val signUpParams = NativeAuthSignUpParameters(username) signUpParams.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(username) val signUpResult = application.signUp(signUpParams) assertResult(signUpResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(username) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt index fdb0d595b4..919dda78e1 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignInMFATest.kt @@ -26,7 +26,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.claims.ClaimsRequest import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthGetAccessTokenParameters @@ -46,8 +45,6 @@ import java.util.Base64 class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - private lateinit var resources: List lateinit var application: INativeAuthPublicClientApplication @@ -90,6 +87,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { assertResult(result) // Initiate challenge, send code to email + tempEmailApi.markCheckpoint(username) val sendChallengeResult = (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) @@ -104,6 +102,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { assertTrue((submitIncorrectChallengeResult as MFASubmitChallengeError).isInvalidChallenge()) // Request new challenge + tempEmailApi.markCheckpoint(username) val requestNewChallengeResult = sendChallengeResult.nextState.requestChallenge(result.authMethods.first()) assertResult(requestNewChallengeResult) (requestNewChallengeResult as MFARequiredResult.VerificationRequired) @@ -159,6 +158,7 @@ class SignInMFATest : NativeAuthPublicClientApplicationAbstractTest() { assertResult(result) // Initiate challenge, send code to email + tempEmailApi.markCheckpoint(username) val sendChallengeResult = (result as SignInResult.MFARequired).nextState.requestChallenge(result.authMethods.first()) assertResult(sendChallengeResult) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPAttributesTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPAttributesTest.kt index bd471cf9b2..28a61f994c 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPAttributesTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPAttributesTest.kt @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.UserAttributes @@ -39,8 +38,6 @@ import org.junit.Test class SignUpEmailOTPAttributesTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -64,12 +61,13 @@ class SignUpEmailOTPAttributesTest : NativeAuthPublicClientApplicationAbstractTe fun testSuccessAttributesFirst() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val attributes = UserAttributes.Builder().country("Ireland").city("Dublin").build() val param = NativeAuthSignUpParameters(username = user) param.attributes = attributes + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -90,9 +88,10 @@ class SignUpEmailOTPAttributesTest : NativeAuthPublicClientApplicationAbstractTe fun testSuccessAttributesLastSameScreen() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -121,8 +120,9 @@ class SignUpEmailOTPAttributesTest : NativeAuthPublicClientApplicationAbstractTe fun testSuccessAttributesLastMultipleScreens() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPTest.kt index 1c52142755..6e5501a541 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailOTPTest.kt @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthSignUpParameters @@ -40,8 +39,6 @@ import org.junit.Test class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -61,8 +58,9 @@ class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -85,12 +83,14 @@ class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(user) val codeRequiredState = (signUpResult as SignUpResult.CodeRequired).nextState + tempEmailApi.markCheckpoint(user) val resendCodeResult = codeRequiredState.resendCode() assertResult(resendCodeResult) val otp2 = tempEmailApi.retrieveCodeFromInbox(user) @@ -183,8 +183,9 @@ class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) val otp = tempEmailApi.retrieveCodeFromInbox(user) @@ -207,7 +208,7 @@ class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { application = setupPCA(config, listOf("oob"), defaultCapabilities) runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.generateRandomUnregisteredEmailAddress() val param = NativeAuthSignUpParameters(username = user) val signUpResult = application.signUp(param) Assert.assertTrue(signUpResult is SignUpError) @@ -227,8 +228,9 @@ class SignUpEmailOTPTest : NativeAuthPublicClientApplicationAbstractTest() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) val otp = tempEmailApi.retrieveCodeFromInbox(user) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordAttributesTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordAttributesTest.kt index 3cf10cf745..489b953e08 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordAttributesTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordAttributesTest.kt @@ -26,7 +26,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.common.java.nativeauth.commands.parameters.SignUpStartCommandParameters import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.UserAttributes @@ -40,8 +39,6 @@ import org.junit.Test class SignUpEmailPasswordAttributesTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -67,13 +64,14 @@ class SignUpEmailPasswordAttributesTest : NativeAuthPublicClientApplicationAbstr fun testEmailPasswordAttributesOnSameScreen() { retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val attributes = UserAttributes.Builder().country("Ireland").city("Dublin").flatUsername("flatusername").build() val param = NativeAuthSignUpParameters(username = user) param.password = getSafePassword().toCharArray() param.attributes = attributes + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -97,8 +95,9 @@ class SignUpEmailPasswordAttributesTest : NativeAuthPublicClientApplicationAbstr fun testSeparateEmailPasswordAndAttributesOnSameScreen() { retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -135,8 +134,9 @@ class SignUpEmailPasswordAttributesTest : NativeAuthPublicClientApplicationAbstr fun testSeparateEmailPasswordAndAttributesOnMultipleScreens() { retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordTest.kt index 5115b8c765..6e0b4ee251 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SignUpEmailPasswordTest.kt @@ -25,7 +25,6 @@ package com.microsoft.identity.client.e2e.tests.network.nativeauth import com.microsoft.identity.client.e2e.utils.assertResult import com.microsoft.identity.internal.testutils.nativeauth.ConfigType -import com.microsoft.identity.internal.testutils.nativeauth.api.TemporaryEmailService import com.microsoft.identity.internal.testutils.nativeauth.api.models.NativeAuthTestConfig import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication import com.microsoft.identity.nativeauth.parameters.NativeAuthSignUpParameters @@ -40,8 +39,6 @@ import org.junit.Test class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() { - private val tempEmailApi = TemporaryEmailService() - lateinit var application: INativeAuthPublicClientApplication lateinit var config: NativeAuthTestConfig.Config @@ -58,7 +55,7 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.generateRandomUnregisteredEmailAddress() val param = NativeAuthSignUpParameters(username = user) param.password = INVALID_PASSWORD.toCharArray() val result = application.signUp(param) @@ -80,9 +77,10 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) param.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -105,13 +103,15 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) param.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) val otp1 = tempEmailApi.retrieveCodeFromInbox(user) val codeRequiredState = (signUpResult as SignUpResult.CodeRequired).nextState + tempEmailApi.markCheckpoint(user) val resendCodeResult = codeRequiredState.resendCode() assertResult(resendCodeResult) val otp2 = tempEmailApi.retrieveCodeFromInbox(user) @@ -132,8 +132,9 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) @@ -159,11 +160,13 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) + tempEmailApi.markCheckpoint(user) val resendCodeResult = (signUpResult as SignUpResult.CodeRequired).nextState.resendCode() assertResult(resendCodeResult) @@ -246,7 +249,7 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() application = setupPCA(config, defaultChallengeTypes, defaultCapabilities) runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.generateRandomUnregisteredEmailAddress() val param = NativeAuthSignUpParameters(username = user) param.password = INVALID_PASSWORD.toCharArray() val signUpResult = application.signUp(param) @@ -267,9 +270,10 @@ class SignUpEmailPasswordTest : NativeAuthPublicClientApplicationAbstractTest() retryOperation { runBlocking { // Running with runBlocking to avoid default 10 second execution timeout. - val user = tempEmailApi.generateRandomEmailAddressLocally() + val user = tempEmailApi.createRandomEmailAddress() val param = NativeAuthSignUpParameters(username = user) param.password = getSafePassword().toCharArray() + tempEmailApi.markCheckpoint(user) val signUpResult = application.signUp(param) assertResult(signUpResult) val otp = tempEmailApi.retrieveCodeFromInbox(user) From 4ad73aa5725c73cbed5beb244c79d0a678cdd48b Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 12 Aug 2026 16:20:09 -0700 Subject: [PATCH 02/22] Update Common after dev rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index 90a68a7054..da8d58503c 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 90a68a70543c309660dd467232eef910082d7f3f +Subproject commit da8d58503c5a635e4800600aeecd8ac4be88562e From e9d60598b51b433f9fc5eba196e1ec10e1490946 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 12 Aug 2026 16:52:58 -0700 Subject: [PATCH 03/22] Document Native Auth E2E CI design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-08-12-native-auth-e2e-ci-design.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md diff --git a/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md b/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md new file mode 100644 index 0000000000..982ec1b507 --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md @@ -0,0 +1,34 @@ +# Native Auth E2E CI Design + +## Goal + +Run all MSAL Native Auth network end-to-end tests on the pull-request branch without enabling unrelated MSAL network tests or changing the existing unit-test and coverage behavior. + +## Pipeline Design + +Keep the existing `msal:jacocoTestReport` step unchanged. Add a separate Gradle step to the PR-branch testing job in `azure-pipelines/pull-request-validation/pr-msal.yml`. + +The new step will: + +- run `msal:cleanTestLocalDebugUnitTest` followed by `msal:testLocalDebugUnitTest`; +- pass `-Plabtest` so Gradle does not exclude network E2E tests; +- filter execution to `com.microsoft.identity.client.e2e.tests.network.nativeauth.*`; +- pass `-PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING)`; +- pass `-PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD)`; and +- use the `LabAuth` certificate already installed by `automation-cert.yml`. + +The filter prevents other network E2E packages from running. Cleaning the test task output ensures the filtered invocation executes even though the existing coverage step previously ran the same underlying unit-test task with different inputs. + +## Secret Configuration + +Create `EMAIL_PROVIDER_PASSWORD` as a secret Azure DevOps pipeline variable. Its value must be the password used by the Mail.tm accounts referenced by the Native Auth test configuration. The value must never be committed or printed. + +The existing `NATIVE_AUTH_CONFIG_STRING` pipeline variable supplies tenant and account configuration. The existing `automation-cert.yml` step obtains `LabAuth` from the `msidlabs` Key Vault and installs it into the agent's Windows certificate store. + +## Failure Behavior + +The new pipeline step must fail when any Native Auth network E2E test fails or when required configuration is absent. It must not silently skip the package or treat missing credentials as success. + +## Verification + +The Azure DevOps test results must contain test cases from the `com.microsoft.identity.client.e2e.tests.network.nativeauth` package, including the re-enabled `SSPRTest` methods. The pipeline log must show that `testLocalDebugUnitTest` ran with `labtest` enabled and the Native Auth package filter. From dd6b5cedc99b284451dc1119d179a0094c481deb Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 12 Aug 2026 16:54:27 -0700 Subject: [PATCH 04/22] Correct Native Auth E2E task design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md b/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md index 982ec1b507..930f22a371 100644 --- a/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md +++ b/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md @@ -10,14 +10,14 @@ Keep the existing `msal:jacocoTestReport` step unchanged. Add a separate Gradle The new step will: -- run `msal:cleanTestLocalDebugUnitTest` followed by `msal:testLocalDebugUnitTest`; +- run `msal:testLocalDebugUnitTest` in a separate Gradle invocation; - pass `-Plabtest` so Gradle does not exclude network E2E tests; - filter execution to `com.microsoft.identity.client.e2e.tests.network.nativeauth.*`; - pass `-PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING)`; - pass `-PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD)`; and - use the `LabAuth` certificate already installed by `automation-cert.yml`. -The filter prevents other network E2E packages from running. Cleaning the test task output ensures the filtered invocation executes even though the existing coverage step previously ran the same underlying unit-test task with different inputs. +The filter prevents other network E2E packages from running. The `labtest` property and test include pattern change the Gradle test task inputs from the preceding coverage invocation, causing the filtered invocation to execute separately. ## Secret Configuration From ce9290208a8c4d12d78677dbbdb62feba4fa9b71 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 12 Aug 2026 18:40:11 -0700 Subject: [PATCH 05/22] Run SSPR E2E tests in PR validation Add a dedicated PR-branch test invocation with the Native Auth and Mail.tm inputs required by SSPRTest. Use a deterministic too-short password for the invalid-format case and update Common for Mail.tm HTTP/1.1 compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/pull-request-validation/pr-msal.yml | 9 +++++++++ common | 2 +- .../client/e2e/tests/network/nativeauth/SSPRTest.kt | 9 +++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 4218ab2f69..2d12faef20 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -75,6 +75,15 @@ stages: tasks: msal:jacocoTestReport -PcodeCoverageEnabled=true -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PmockApiUrl=$(MOCK_API_URL) -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 + - task: Gradle@2 + displayName: Run Native Auth SSPR E2E Tests + inputs: + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests "com.microsoft.identity.client.e2e.tests.network.nativeauth.SSPRTest" + javaHomeSelection: $(BuildParameters.javaHomeSelection) + jdkVersion: 1.17 + publishJUnitResults: true + testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml' + testRunTitle: Native Auth SSPR E2E Tests (PR Branch) - publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml artifact: jacocoReport displayName: 'Publish JaCoCo Report Artifact (PR Branch)' diff --git a/common b/common index da8d58503c..8885260133 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit da8d58503c5a635e4800600aeecd8ac4be88562e +Subproject commit 8885260133c5d3037f38c37f65146fd640af976a diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt index a5d578d047..084d4207ed 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt @@ -47,6 +47,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { private val defaultConfigType = ConfigType.SSPR private val defaultChallengeTypes = listOf("password", "oob") private val defaultCapabilities = listOf("mfa_required", "registration_required") + private val invalidPasswordTooShort = "a" /** * Verify email with email OTP first and then reset password. @@ -98,10 +99,14 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) assertResult(submitCodeResult) - val password = INVALID_PASSWORD + val password = invalidPasswordTooShort val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError) - Assert.assertTrue((submitPasswordResult as ResetPasswordSubmitPasswordError).isInvalidPassword()) + val error = submitPasswordResult as ResetPasswordSubmitPasswordError + Assert.assertTrue( + "Expected invalid password error, but received errorType=${error.errorType}, error=${error.error}, subError=${error.subError}", + error.isInvalidPassword() + ) } } From a6aba489f0d17ca2e02c75d819f5470cb4d69104 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Thu, 13 Aug 2026 21:24:28 -0700 Subject: [PATCH 06/22] Delete docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md --- .../2026-08-12-native-auth-e2e-ci-design.md | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md diff --git a/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md b/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md deleted file mode 100644 index 930f22a371..0000000000 --- a/docs/superpowers/specs/2026-08-12-native-auth-e2e-ci-design.md +++ /dev/null @@ -1,34 +0,0 @@ -# Native Auth E2E CI Design - -## Goal - -Run all MSAL Native Auth network end-to-end tests on the pull-request branch without enabling unrelated MSAL network tests or changing the existing unit-test and coverage behavior. - -## Pipeline Design - -Keep the existing `msal:jacocoTestReport` step unchanged. Add a separate Gradle step to the PR-branch testing job in `azure-pipelines/pull-request-validation/pr-msal.yml`. - -The new step will: - -- run `msal:testLocalDebugUnitTest` in a separate Gradle invocation; -- pass `-Plabtest` so Gradle does not exclude network E2E tests; -- filter execution to `com.microsoft.identity.client.e2e.tests.network.nativeauth.*`; -- pass `-PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING)`; -- pass `-PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD)`; and -- use the `LabAuth` certificate already installed by `automation-cert.yml`. - -The filter prevents other network E2E packages from running. The `labtest` property and test include pattern change the Gradle test task inputs from the preceding coverage invocation, causing the filtered invocation to execute separately. - -## Secret Configuration - -Create `EMAIL_PROVIDER_PASSWORD` as a secret Azure DevOps pipeline variable. Its value must be the password used by the Mail.tm accounts referenced by the Native Auth test configuration. The value must never be committed or printed. - -The existing `NATIVE_AUTH_CONFIG_STRING` pipeline variable supplies tenant and account configuration. The existing `automation-cert.yml` step obtains `LabAuth` from the `msidlabs` Key Vault and installs it into the agent's Windows certificate store. - -## Failure Behavior - -The new pipeline step must fail when any Native Auth network E2E test fails or when required configuration is absent. It must not silently skip the package or treat missing credentials as success. - -## Verification - -The Azure DevOps test results must contain test cases from the `com.microsoft.identity.client.e2e.tests.network.nativeauth` package, including the re-enabled `SSPRTest` methods. The pipeline log must show that `testLocalDebugUnitTest` ran with `labtest` enabled and the Native Auth package filter. From a90fc176c9f47d32d94d8bdfd58ed4d97a0b7c2f Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Fri, 14 Aug 2026 00:33:45 -0400 Subject: [PATCH 07/22] Run all Native Auth E2E tests in PR validation Update Common to the latest mail-tm-native-auth-e2e commit and broaden the dedicated PR test filter to the complete Native Auth E2E package, including SSPR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/pull-request-validation/pr-msal.yml | 6 +++--- common | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 2d12faef20..c55ef6b067 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -76,14 +76,14 @@ stages: javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 - task: Gradle@2 - displayName: Run Native Auth SSPR E2E Tests + displayName: Run Native Auth E2E Tests inputs: - tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests "com.microsoft.identity.client.e2e.tests.network.nativeauth.SSPRTest" + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests "com.microsoft.identity.client.e2e.tests.network.nativeauth.*" javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 publishJUnitResults: true testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml' - testRunTitle: Native Auth SSPR E2E Tests (PR Branch) + testRunTitle: Native Auth E2E Tests (PR Branch) - publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml artifact: jacocoReport displayName: 'Publish JaCoCo Report Artifact (PR Branch)' diff --git a/common b/common index 8885260133..76a87431e1 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 8885260133c5d3037f38c37f65146fd640af976a +Subproject commit 76a87431e1234762162728c094cab94d8908c3ed From f56e84448fb991b5e989dc89407498c6b40f5979 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Thu, 13 Aug 2026 22:00:21 -0700 Subject: [PATCH 08/22] Update pr-msal.yml --- azure-pipelines/pull-request-validation/pr-msal.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index c55ef6b067..5da67856c1 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -78,7 +78,7 @@ stages: - task: Gradle@2 displayName: Run Native Auth E2E Tests inputs: - tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests "com.microsoft.identity.client.e2e.tests.network.nativeauth.*" + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 publishJUnitResults: true From e94d3212d9eb0bbd0e559a743651822a43c22175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:59:28 +0000 Subject: [PATCH 09/22] Fix AB ID extraction in PR workflow Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- .github/workflows/validate-pr-ab-id.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-pr-ab-id.yml b/.github/workflows/validate-pr-ab-id.yml index 8f2ed1888a..b2ef20a45d 100644 --- a/.github/workflows/validate-pr-ab-id.yml +++ b/.github/workflows/validate-pr-ab-id.yml @@ -36,7 +36,7 @@ jobs: env: PR_BODY: ${{ github.event.pull_request.body }} run: | - result=$(echo "$PR_BODY" | grep -oP '(?<=#)\d+') + result=$(echo "$PR_BODY" | grep -oiP 'AB#\K\d+' | head -n 1) echo "id=${result}" >> "$GITHUB_OUTPUT" # Get Pull request ID From 3c4a8e3950f68e1dbb08ce31680de9abd80668fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:02:20 +0000 Subject: [PATCH 10/22] Restore retries for SSPR E2E tests Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- .../e2e/tests/network/nativeauth/SSPRTest.kt | 116 +++++++++--------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt index 084d4207ed..fb2f5d48f1 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt @@ -60,20 +60,22 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - tempEmailApi.markCheckpoint(user) - result = application.resetPassword(param) - assertResult(result) - - val otp = tempEmailApi.retrieveCodeFromInbox(user) - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) - assertResult(submitCodeResult) - - val password = getSafePassword() - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) + retryOperation { + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp = tempEmailApi.retrieveCodeFromInbox(user) + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) + assertResult(submitCodeResult) + + val password = getSafePassword() + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) + } } } @@ -88,25 +90,27 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - tempEmailApi.markCheckpoint(user) - result = application.resetPassword(param) - assertResult(result) - - val otp = tempEmailApi.retrieveCodeFromInbox(user) - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) - assertResult(submitCodeResult) - - val password = invalidPasswordTooShort - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError) - val error = submitPasswordResult as ResetPasswordSubmitPasswordError - Assert.assertTrue( - "Expected invalid password error, but received errorType=${error.errorType}, error=${error.error}, subError=${error.subError}", - error.isInvalidPassword() - ) + retryOperation { + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp = tempEmailApi.retrieveCodeFromInbox(user) + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp) + assertResult(submitCodeResult) + + val password = invalidPasswordTooShort + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordSubmitPasswordError) + val error = submitPasswordResult as ResetPasswordSubmitPasswordError + Assert.assertTrue( + "Expected invalid password error, but received errorType=${error.errorType}, error=${error.error}, subError=${error.subError}", + error.isInvalidPassword() + ) + } } } @@ -121,28 +125,30 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { var result: ResetPasswordStartResult - runBlocking { - val user = config.email - val param = NativeAuthResetPasswordParameters(username = user) - tempEmailApi.markCheckpoint(user) - result = application.resetPassword(param) - assertResult(result) - - val otp1 = tempEmailApi.retrieveCodeFromInbox(user) - val codeRequiredState = (result as ResetPasswordStartResult.CodeRequired).nextState - tempEmailApi.markCheckpoint(user) - val resendCodeResult = codeRequiredState.resendCode() - assertResult(resendCodeResult) - - val otp2 = tempEmailApi.retrieveCodeFromInbox(user) - Assert.assertNotEquals(otp1, otp2) - - val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp2) - assertResult(submitCodeResult) - - val password = getSafePassword() - val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) - Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) + retryOperation { + runBlocking { + val user = config.email + val param = NativeAuthResetPasswordParameters(username = user) + tempEmailApi.markCheckpoint(user) + result = application.resetPassword(param) + assertResult(result) + + val otp1 = tempEmailApi.retrieveCodeFromInbox(user) + val codeRequiredState = (result as ResetPasswordStartResult.CodeRequired).nextState + tempEmailApi.markCheckpoint(user) + val resendCodeResult = codeRequiredState.resendCode() + assertResult(resendCodeResult) + + val otp2 = tempEmailApi.retrieveCodeFromInbox(user) + Assert.assertNotEquals(otp1, otp2) + + val submitCodeResult = (result as ResetPasswordStartResult.CodeRequired).nextState.submitCode(otp2) + assertResult(submitCodeResult) + + val password = getSafePassword() + val submitPasswordResult = (submitCodeResult as ResetPasswordSubmitCodeResult.PasswordRequired).nextState.submitPassword(password.toCharArray()) + Assert.assertTrue(submitPasswordResult is ResetPasswordResult.Complete) + } } } From e1124b589f454a7522d105abe79294b73ef5e662 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:03:37 +0000 Subject: [PATCH 11/22] Pass email password through pipeline env Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- azure-pipelines/pull-request-validation/pr-msal.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 5da67856c1..5c78c706a7 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -78,12 +78,14 @@ stages: - task: Gradle@2 displayName: Run Native Auth E2E Tests inputs: - tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 publishJUnitResults: true testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml' testRunTitle: Native Auth E2E Tests (PR Branch) + env: + EMAIL_PROVIDER_PASSWORD: $(EMAIL_PROVIDER_PASSWORD) - publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml artifact: jacocoReport displayName: 'Publish JaCoCo Report Artifact (PR Branch)' From 97dcc4b6d75d2d5cbfb29599c76c8ea602e52dc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:32:19 +0000 Subject: [PATCH 12/22] Restore email password Gradle property Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- azure-pipelines/pull-request-validation/pr-msal.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 5c78c706a7..5da67856c1 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -78,14 +78,12 @@ stages: - task: Gradle@2 displayName: Run Native Auth E2E Tests inputs: - tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 publishJUnitResults: true testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml' testRunTitle: Native Auth E2E Tests (PR Branch) - env: - EMAIL_PROVIDER_PASSWORD: $(EMAIL_PROVIDER_PASSWORD) - publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml artifact: jacocoReport displayName: 'Publish JaCoCo Report Artifact (PR Branch)' From f2592958b5ab3d6bc331f9709be79f26568dda76 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:02:24 +0000 Subject: [PATCH 13/22] Skip rate-limited SSPR OTP E2E scenarios Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- .../identity/client/e2e/tests/network/nativeauth/SSPRTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt index fb2f5d48f1..e7eb3d0229 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt @@ -53,6 +53,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Verify email with email OTP first and then reset password. * (hero scenario 8 & 17, use case 3.1.1) */ + @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testSSPRSuccess() { config = getConfig(defaultConfigType) @@ -83,6 +84,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * New password being set doesn’t meet password complexity requirements set on portal * (use case 3.1.3) */ + @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testErrorInvalidPasswordFormat() { config = getConfig(defaultConfigType) @@ -118,6 +120,7 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Resend Code. * (use case 3.1.4) */ + @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testResendCode() { config = getConfig(defaultConfigType) From 97f1df6dc44953c961ef6185e1bef70bdba74424 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:07:58 +0000 Subject: [PATCH 14/22] Retry rate-limited Native Auth E2E tests Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- ...AuthPublicClientApplicationAbstractTest.kt | 27 ++++++++++++------- .../e2e/tests/network/nativeauth/SSPRTest.kt | 3 --- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index ccf18d27b5..8e595c3d76 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -156,24 +156,31 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl authFlow: () -> T ) { var retryCount = 0 - var shouldRetry = true - while (shouldRetry) { + while (true) { try { authFlow() - shouldRetry = false // authFlow() has succeeded, so we don't need to retry. - } catch (e: Exception) { - // Retry transient end-to-end flow failures. - if (retryCount >= maxRetries) { - Assert.fail(e.message) - shouldRetry = false - } else { - retryCount++ + return + } catch (e: AssertionError) { + if (!e.message.orEmpty().contains("AADSTS701014")) { + throw e } + retryOrFail(e, retryCount++, maxRetries) + } catch (e: Exception) { + retryOrFail(e, retryCount++, maxRetries) } } } + private fun retryOrFail(error: Throwable, retryCount: Int, maxRetries: Int) { + if (retryCount >= maxRetries) { + Assert.fail(error.message) + } + + // Avoid repeatedly requesting OTPs while the Native Auth test tenant is throttling them. + Thread.sleep(5_000L * (1L shl retryCount)) + } + private fun readConfigFile(filePath: String): String { val sb = StringBuilder() try { diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt index e7eb3d0229..fb2f5d48f1 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/SSPRTest.kt @@ -53,7 +53,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Verify email with email OTP first and then reset password. * (hero scenario 8 & 17, use case 3.1.1) */ - @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testSSPRSuccess() { config = getConfig(defaultConfigType) @@ -84,7 +83,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * New password being set doesn’t meet password complexity requirements set on portal * (use case 3.1.3) */ - @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testErrorInvalidPasswordFormat() { config = getConfig(defaultConfigType) @@ -120,7 +118,6 @@ class SSPRTest : NativeAuthPublicClientApplicationAbstractTest() { * Resend Code. * (use case 3.1.4) */ - @Ignore("OTP generation is rate limited in the test tenant.") @Test fun testResendCode() { config = getConfig(defaultConfigType) From 41b12b83c5e91c9c1dfc4223bb95fad2964f11eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:09:05 +0000 Subject: [PATCH 15/22] Preserve Native Auth retry failure cause Co-authored-by: djanardhan98 <307255345+djanardhan98@users.noreply.github.com> --- .../nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index 8e595c3d76..a6a108a82a 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -174,7 +174,7 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl private fun retryOrFail(error: Throwable, retryCount: Int, maxRetries: Int) { if (retryCount >= maxRetries) { - Assert.fail(error.message) + throw AssertionError(error.message).apply { initCause(error) } } // Avoid repeatedly requesting OTPs while the Native Auth test tenant is throttling them. From 9686b508646f2f6dbbc0edfef7568dfffa88014d Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Fri, 14 Aug 2026 13:51:37 -0500 Subject: [PATCH 16/22] Update Common Mail.tm integration Point the Common submodule at the latest commit from AzureAD/microsoft-authentication-library-common-for-android#3219. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index 76a87431e1..40ad51c8ae 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 76a87431e1234762162728c094cab94d8908c3ed +Subproject commit 40ad51c8ae1f0c5995440a244198db685efd7a68 From e356e84bc7cbca851c6079bea2c8a8e713efee0a Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Mon, 17 Aug 2026 12:37:56 -0500 Subject: [PATCH 17/22] Update Common runtime email credential integration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83947e30-5bc9-4ec7-a8ed-c03d4415da92 --- azure-pipelines/pull-request-validation/pr-msal.yml | 4 +++- common | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 5da67856c1..5c78c706a7 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -78,12 +78,14 @@ stages: - task: Gradle@2 displayName: Run Native Auth E2E Tests inputs: - tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) -PemailProviderPassword=$(EMAIL_PROVIDER_PASSWORD) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* + tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 publishJUnitResults: true testResultsFiles: '**/build/test-results/testLocalDebugUnitTest/TEST-*.xml' testRunTitle: Native Auth E2E Tests (PR Branch) + env: + EMAIL_PROVIDER_PASSWORD: $(EMAIL_PROVIDER_PASSWORD) - publish: $(Build.SourcesDirectory)/msal/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml artifact: jacocoReport displayName: 'Publish JaCoCo Report Artifact (PR Branch)' diff --git a/common b/common index 40ad51c8ae..c109bf8e31 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 40ad51c8ae1f0c5995440a244198db685efd7a68 +Subproject commit c109bf8e3108456184b335716fd0064a06f7401d From a59934587239f1cff742e3c7b27ed9f847e3ce96 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Mon, 17 Aug 2026 15:19:21 -0500 Subject: [PATCH 18/22] Fix Native Auth email service initialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83947e30-5bc9-4ec7-a8ed-c03d4415da92 --- .../nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index a6a108a82a..a553b35b55 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -77,7 +77,7 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl private lateinit var context: Context private lateinit var activity: Activity - protected val tempEmailApi = TemporaryEmailService(BuildValues.getEmailProviderPassword()) + protected val tempEmailApi = TemporaryEmailService() // Remove default Coroutine test timeout of 10 seconds. private val testDispatcher = StandardTestDispatcher() From 4bd47e532e6fb4d534af8a0ac1397a0eabdb39e6 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Tue, 18 Aug 2026 00:47:54 -0500 Subject: [PATCH 19/22] Revert PR AB ID workflow change Restore validate-pr-ab-id.yml to the dev branch version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4f268d2a-b8c4-4a22-826e-de2d9b9f59a6 --- .github/workflows/validate-pr-ab-id.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-pr-ab-id.yml b/.github/workflows/validate-pr-ab-id.yml index b2ef20a45d..8f2ed1888a 100644 --- a/.github/workflows/validate-pr-ab-id.yml +++ b/.github/workflows/validate-pr-ab-id.yml @@ -36,7 +36,7 @@ jobs: env: PR_BODY: ${{ github.event.pull_request.body }} run: | - result=$(echo "$PR_BODY" | grep -oiP 'AB#\K\d+' | head -n 1) + result=$(echo "$PR_BODY" | grep -oP '(?<=#)\d+') echo "id=${result}" >> "$GITHUB_OUTPUT" # Get Pull request ID From 62183f70675a67dc506e8757cb79305628de9924 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Mon, 24 Aug 2026 09:40:39 -0700 Subject: [PATCH 20/22] Update Common to latest dev Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common b/common index c109bf8e31..6cbee72fb8 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit c109bf8e3108456184b335716fd0064a06f7401d +Subproject commit 6cbee72fb88d4b58c77164608c861c27e5dbdf84 From a1be871d4d79d66145b77dedacf3dc478d0b9480 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Tue, 25 Aug 2026 11:09:21 -0700 Subject: [PATCH 21/22] Address review feedback on Native Auth E2E tests Responds to review comments on PR #2553, aligning with the equivalent iOS/ObjC implementation (microsoft-authentication-library-for-objc). - pr-msal.yml: add a "skip-native-auth-e2e-tests" label opt-out so a PR is not blocked by live mail.tm / tenant availability. The check fail-safes to skipping when the labels API is unreachable or the build is not a PR build. Also bound the E2E task with timeoutInMinutes: 30. - Replace the brittle AADSTS701014 substring match with a typed classifier (NativeAuthEmailOTPErrorClassifier), mirroring iOS MSALNativeAuthEmailOTPErrorClassifier. AssertUtils now throws NativeAuthAssertionError carrying the typed Error, so the OTP throttle is detected from Error.errorCodes first, with the description substring retained only for failures that never carry typed codes. Adds 12 unit tests for the classifier. - Reduce throttle retry cost: 5 retries with uncapped exponential backoff (~155s) becomes 3 retries with a 20s cap (~35s worst case). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8ace3950-0a5f-47cf-bc7e-33687e701f5f --- .../pull-request-validation/pr-msal.yml | 42 ++++ ...AuthPublicClientApplicationAbstractTest.kt | 29 ++- .../identity/client/e2e/utils/AssertUtils.kt | 27 ++- .../NativeAuthEmailOTPErrorClassifier.kt | 94 ++++++++ .../NativeAuthEmailOTPErrorClassifierTest.kt | 209 ++++++++++++++++++ 5 files changed, 394 insertions(+), 7 deletions(-) create mode 100644 msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifier.kt create mode 100644 msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifierTest.kt diff --git a/azure-pipelines/pull-request-validation/pr-msal.yml b/azure-pipelines/pull-request-validation/pr-msal.yml index 5c78c706a7..c37d617efa 100644 --- a/azure-pipelines/pull-request-validation/pr-msal.yml +++ b/azure-pipelines/pull-request-validation/pr-msal.yml @@ -75,8 +75,50 @@ stages: tasks: msal:jacocoTestReport -PcodeCoverageEnabled=true -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PmockApiUrl=$(MOCK_API_URL) -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) javaHomeSelection: $(BuildParameters.javaHomeSelection) jdkVersion: 1.17 + - bash: | + set -euo pipefail + SKIP_LABEL="skip-native-auth-e2e-tests" + # AGENT_TEMPDIRECTORY is a Windows path on the windows-2022 pool; normalise the + # separators so curl/grep never see a backslash path under Git Bash/MSYS. + TEMP_DIR="${AGENT_TEMPDIRECTORY:-.}" + LABELS_FILE="${TEMP_DIR//\\//}/native-auth-pr-labels.json" + + if [ -z "${PR_NUMBER:-}" ]; then + echo "No PR number available (not a PR build). Skipping Native Auth E2E tests." + echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true" + exit 0 + fi + + LABELS_URL="https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/labels" + echo "Fetching PR labels from ${LABELS_URL}" + # Capture the body and the HTTP status separately, so that a failed API call is + # distinguishable from a successful call that simply returned no matching label. + HTTP_STATUS=$(curl -sS -o "$LABELS_FILE" -w "%{http_code}" "$LABELS_URL" || echo "000") + + if [ "$HTTP_STATUS" != "200" ]; then + echo "GitHub labels API returned HTTP ${HTTP_STATUS}. Skipping Native Auth E2E tests as a preventive measure (fail-safe to skipping, never to blocking)." + echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true" + exit 0 + fi + + # jq is not reliably available on the windows-2022 pool, so match on the raw JSON. + if grep -q "\"name\"[[:space:]]*:[[:space:]]*\"${SKIP_LABEL}\"" "$LABELS_FILE"; then + echo "Label '${SKIP_LABEL}' is present on PR #${PR_NUMBER}. Skipping Native Auth E2E tests." + echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]true" + else + echo "Label '${SKIP_LABEL}' is not present on PR #${PR_NUMBER}. Running Native Auth E2E tests." + echo "##vso[task.setvariable variable=SKIP_NATIVE_AUTH_E2E]false" + fi + displayName: Check for skip-native-auth-e2e-tests label + env: + PR_NUMBER: $(System.PullRequest.PullRequestNumber) + REPO: $(Build.Repository.Name) - task: Gradle@2 displayName: Run Native Auth E2E Tests + # Mirrors iOS (microsoft-authentication-library-for-objc) pr-validation.yml, which guards its + # native auth E2E jobs with the same opt-out label and a 30 minute timeout. + condition: and(succeeded(), ne(variables['SKIP_NATIVE_AUTH_E2E'], 'true')) + timeoutInMinutes: 30 inputs: tasks: msal:testLocalDebugUnitTest -Plabtest -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}} -PnativeAuthConfigString=$(NATIVE_AUTH_CONFIG_STRING) --tests com.microsoft.identity.client.e2e.tests.network.nativeauth.* javaHomeSelection: $(BuildParameters.javaHomeSelection) diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index a553b35b55..4e98f9ca30 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -30,6 +30,7 @@ import com.google.gson.reflect.TypeToken import com.microsoft.identity.client.PublicClientApplication import com.microsoft.identity.client.e2e.shadows.ShadowAndroidSdkStorageEncryptionManager import com.microsoft.identity.client.e2e.tests.IPublicClientApplicationTest +import com.microsoft.identity.client.e2e.utils.NativeAuthEmailOTPErrorClassifier import com.microsoft.identity.client.exception.MsalException import com.microsoft.identity.common.internal.controllers.CommandDispatcherHelper import com.microsoft.identity.common.java.nativeauth.BuildValues @@ -70,6 +71,23 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl const val INVALID_PASSWORD = "password" const val INCORRECT_CODE = "00000000" + /** + * Retry budget for OTP-throttled (AADSTS701014) auth flows. + * + * Each retry re-runs the whole flow (new temporary inbox + signup + OTP request), so + * attempts are expensive in wall-clock time on top of the backoff itself. With 3 retries + * and the delay capped at [MAX_RETRY_DELAY_MILLIS], a persistently throttled test sleeps at + * most 5s + 10s + 20s = 35s across 4 attempts. + * + * iOS keeps 5 attempts only because XCTest's plan-level `maximumTestRepetitions` / + * `retryOnFailure` costs no extra wall-clock on success, and because its throttle path + * raises `XCTSkip` (via `skipIfEmailOTPThrottled`) instead of retrying into the throttle. + * This hand-rolled loop has neither property, hence the tighter budget. + */ + const val MAX_THROTTLE_RETRIES = 3 + private const val RETRY_BASE_DELAY_MILLIS = 5_000L + private const val MAX_RETRY_DELAY_MILLIS = 20_000L + private val labApiAuthenticationClient: LabApiAuthenticationClient = LabApiAuthenticationClient(BuildConfig.LAB_CLIENT_SECRET) val labClient: LabClient = LabClient(labApiAuthenticationClient) @@ -152,7 +170,7 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl } fun retryOperation( - maxRetries: Int = 5, + maxRetries: Int = MAX_THROTTLE_RETRIES, authFlow: () -> T ) { var retryCount = 0 @@ -162,7 +180,10 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl authFlow() return } catch (e: AssertionError) { - if (!e.message.orEmpty().contains("AADSTS701014")) { + // Classification (typed errorCodes first, assertion message as unconditional + // fallback) lives in the classifier so it is unit-testable without standing up + // this Robolectric-backed abstract test. + if (!NativeAuthEmailOTPErrorClassifier.isThrottleError(e)) { throw e } retryOrFail(e, retryCount++, maxRetries) @@ -178,7 +199,9 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl } // Avoid repeatedly requesting OTPs while the Native Auth test tenant is throttling them. - Thread.sleep(5_000L * (1L shl retryCount)) + Thread.sleep( + minOf(RETRY_BASE_DELAY_MILLIS * (1L shl retryCount), MAX_RETRY_DELAY_MILLIS) + ) } private fun readConfigFile(filePath: String): String { diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/utils/AssertUtils.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/AssertUtils.kt index 7615037a0d..6de2b6f762 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/utils/AssertUtils.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/AssertUtils.kt @@ -23,15 +23,34 @@ package com.microsoft.identity.client.e2e.utils import com.microsoft.identity.nativeauth.statemachine.errors.Error -import org.junit.Assert + +/** + * An [AssertionError] that additionally carries the structured Native Auth [Error] that caused the + * assertion to fail, so callers (e.g. the throttle-aware retry loop in + * `NativeAuthPublicClientApplicationAbstractTest`) can classify the failure from typed data such as + * [Error.errorCodes] instead of re-parsing the formatted assertion message. + * + * It remains an [AssertionError] subclass so JUnit continues to report it as a test failure exactly + * as `Assert.fail` did. + * + * Note: [Error] here is [com.microsoft.identity.nativeauth.statemachine.errors.Error], not + * [kotlin.Error]. + * + * @param message the assertion message, unchanged from the previous `Assert.fail` message format. + * @param nativeAuthError the Native Auth error that failed the assertion, or null if the actual + * value was not a Native Auth [Error]. + */ +class NativeAuthAssertionError(message: String, val nativeAuthError: Error?) : AssertionError(message) + inline fun assertResult(actual: Any) { val condition = actual is ExpectedType if (!condition) { - val assertMessage: String = if (actual is Error) { - "Type comparison failed. Expected: ${ExpectedType::class.java}, actual: ${actual.javaClass}. Error: ${actual.error} - ${actual.errorMessage}" + val nativeAuthError: Error? = actual as? Error + val assertMessage: String = if (nativeAuthError != null) { + "Type comparison failed. Expected: ${ExpectedType::class.java}, actual: ${actual.javaClass}. Error: ${nativeAuthError.error} - ${nativeAuthError.errorMessage}" } else { "Type comparison failed. Expected: ${ExpectedType::class.java}, actual: ${actual.javaClass}" } - Assert.fail(assertMessage) + throw NativeAuthAssertionError(assertMessage, nativeAuthError) } } \ No newline at end of file diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifier.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifier.kt new file mode 100644 index 0000000000..502c6542d2 --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifier.kt @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.e2e.utils + +/** + * Test-scope classifier for the Entra/CIAM email one-time-passcode throttle error. + * + * This is the Android port of the iOS classifier + * `MSAL/test/unit/native_auth/utils/MSALNativeAuthEmailOTPErrorClassifier.swift` + * (microsoft-authentication-library-for-objc), and is kept intentionally behaviour-compatible + * with it: prefer the typed error codes, fall back to a description substring match. + */ +object NativeAuthEmailOTPErrorClassifier { + + /** + * AADSTS701014 - the service could not generate another email one-time passcode yet, because + * OTP issuance for this identity/tenant is currently being throttled. + * + * iOS phrases the equivalent skip message as + * "AADSTS701014: CIAM could not generate another email OTP." + * + * It is a transient, service-side condition: the correct reaction in an E2E test is to back + * off and retry rather than to fail the test. + */ + const val THROTTLE_ERROR_CODE = 701014 + + /** + * Substring fallback marker, matched against an error's description/assertion text when the + * typed [com.microsoft.identity.nativeauth.statemachine.errors.Error.errorCodes] list does not + * itself carry [THROTTLE_ERROR_CODE]. + */ + const val THROTTLE_ERROR_DESCRIPTION_MARKER = "AADSTS$THROTTLE_ERROR_CODE" + + /** + * Returns true when the supplied error represents the email OTP throttle. + * + * The typed [errorCodes] list is the primary, contract-based signal and is checked first, so + * that classification does not depend on how an error was formatted into a string. The + * [errorDescription] substring match is the fallback for the cases the typed list cannot cover. + * + * @param errorCodes the error codes returned by the authentication server, if available. + * @param errorDescription the error message/description text, if available. + */ + @JvmStatic + fun isThrottleError(errorCodes: List?, errorDescription: String?): Boolean { + if (errorCodes?.contains(THROTTLE_ERROR_CODE) == true) { + return true + } + return errorDescription?.contains(THROTTLE_ERROR_DESCRIPTION_MARKER) ?: false + } + + /** + * Returns true when the supplied assertion failure represents the email OTP throttle. + * + * The typed [com.microsoft.identity.nativeauth.statemachine.errors.Error.errorCodes] list + * carried by a [NativeAuthAssertionError] is the primary, contract-based signal. + * + * [Throwable.message] is the fallback and is consulted unconditionally rather than only when + * the typed error is absent: [assertResult] embeds both the server's `error` and `errorMessage` + * into the assertion message, so the message text is a strict superset of `errorMessage` for + * the typed case and is the only available text for the untyped case. Using it unconditionally + * keeps this fallback from ever being narrower than the plain string match it replaced. + * + * @param error the assertion failure thrown by the auth flow under test. + */ + @JvmStatic + fun isThrottleError(error: AssertionError): Boolean { + val nativeAuthError = (error as? NativeAuthAssertionError)?.nativeAuthError + return isThrottleError( + errorCodes = nativeAuthError?.errorCodes, + errorDescription = error.message + ) + } +} diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifierTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifierTest.kt new file mode 100644 index 0000000000..9e24ddca0f --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/utils/NativeAuthEmailOTPErrorClassifierTest.kt @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.e2e.utils + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import com.microsoft.identity.nativeauth.statemachine.errors.Error as NativeAuthError + +/** + * Pure JVM tests for [NativeAuthEmailOTPErrorClassifier], mirroring the iOS + * `MSALNativeAuthEmailOTPErrorClassifierTests` coverage. + */ +class NativeAuthEmailOTPErrorClassifierTest { + + @Test + fun testThrottleCodePresentInErrorCodesReturnsTrue() { + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = listOf(NativeAuthEmailOTPErrorClassifier.THROTTLE_ERROR_CODE), + errorDescription = null + ) + ) + } + + @Test + fun testThrottleCodePresentAmongOtherErrorCodesReturnsTrue() { + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = listOf(50034, NativeAuthEmailOTPErrorClassifier.THROTTLE_ERROR_CODE), + errorDescription = "some unrelated description" + ) + ) + } + + @Test + fun testThrottleCodeAbsentFromErrorCodesReturnsFalse() { + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = listOf(50034, 50126), + errorDescription = "AADSTS50034: The user account does not exist." + ) + ) + } + + @Test + fun testDescriptionOnlyMatchReturnsTrue() { + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = null, + errorDescription = "AADSTS50034: The user account does not exist." + ) + ) + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = null, + errorDescription = "AADSTS701014: CIAM could not generate another email OTP." + ) + ) + } + + @Test + fun testEmptyErrorCodesFallsBackToDescription() { + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = emptyList(), + errorDescription = "AADSTS701014: CIAM could not generate another email OTP." + ) + ) + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = emptyList(), + errorDescription = "some unrelated description" + ) + ) + } + + @Test + fun testNullErrorCodesAndNullDescriptionReturnsFalse() { + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + errorCodes = null, + errorDescription = null + ) + ) + } + + @Test + fun testThrottleErrorCodeConstantMatchesServiceContract() { + assertEquals(701014, NativeAuthEmailOTPErrorClassifier.THROTTLE_ERROR_CODE) + assertEquals("AADSTS701014", NativeAuthEmailOTPErrorClassifier.THROTTLE_ERROR_DESCRIPTION_MARKER) + } + + private fun nativeAuthError( + error: String?, + errorMessage: String?, + errorCodes: List? + ) = NativeAuthError( + error = error, + errorMessage = errorMessage, + correlationId = "UNSET", + errorCodes = errorCodes + ) + + @Test + fun testAssertionErrorWithTypedThrottleCodeIsThrottled() { + val error = nativeAuthError( + error = "invalid_grant", + errorMessage = "Something the marker does not appear in", + errorCodes = listOf(NativeAuthEmailOTPErrorClassifier.THROTTLE_ERROR_CODE) + ) + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + NativeAuthAssertionError("Type comparison failed.", error) + ) + ) + } + + /** + * Regression pin: when the carried error has no typed codes, classification must still fall + * back to the assertion message. Reading only `Error.errorMessage` here would be narrower than + * the plain `e.message.contains("AADSTS701014")` check this classifier replaced. + */ + @Test + fun testAssertionErrorWithoutTypedCodesFallsBackToAssertionMessage() { + val error = nativeAuthError(error = null, errorMessage = null, errorCodes = null) + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + NativeAuthAssertionError( + "Type comparison failed. Error: null - AADSTS701014: CIAM could not generate another email OTP.", + error + ) + ) + ) + } + + /** + * Regression pin: the marker may arrive in the server's `error` field rather than in + * `errorMessage`. [assertResult] embeds both into the assertion message, so classifying off the + * message keeps this case retryable. + */ + @Test + fun testAssertionErrorWithMarkerOnlyInErrorFieldIsThrottled() { + val error = nativeAuthError( + error = "AADSTS701014", + errorMessage = "Request throttled.", + errorCodes = listOf(50034) + ) + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + NativeAuthAssertionError( + "Type comparison failed. Error: AADSTS701014 - Request throttled.", + error + ) + ) + ) + } + + @Test + fun testNonThrottleAssertionErrorIsNotThrottled() { + val error = nativeAuthError( + error = "invalid_grant", + errorMessage = "AADSTS50034: The user account does not exist.", + errorCodes = listOf(50034) + ) + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + NativeAuthAssertionError( + "Type comparison failed. Error: invalid_grant - AADSTS50034: The user account does not exist.", + error + ) + ) + ) + } + + @Test + fun testPlainAssertionErrorIsClassifiedFromItsMessage() { + assertTrue( + NativeAuthEmailOTPErrorClassifier.isThrottleError( + AssertionError("AADSTS701014: CIAM could not generate another email OTP.") + ) + ) + assertFalse( + NativeAuthEmailOTPErrorClassifier.isThrottleError(AssertionError("expected:<1> but was:<2>")) + ) + assertFalse(NativeAuthEmailOTPErrorClassifier.isThrottleError(AssertionError())) + } +} From 03d18c5fa9fd24f01992bf0e319d4541b2118171 Mon Sep 17 00:00:00 2001 From: Disha Janardhan Date: Wed, 26 Aug 2026 12:44:25 -0700 Subject: [PATCH 22/22] Stop retrying non-throttle Native Auth failures Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 556e2a2c-f4a4-4c93-8aa6-e1e414b4dd8e --- ...AuthPublicClientApplicationAbstractTest.kt | 2 - ...PublicClientApplicationAbstractTestTest.kt | 48 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTestTest.kt diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt index 4e98f9ca30..8d4a3b5846 100644 --- a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTest.kt @@ -187,8 +187,6 @@ abstract class NativeAuthPublicClientApplicationAbstractTest : IPublicClientAppl throw e } retryOrFail(e, retryCount++, maxRetries) - } catch (e: Exception) { - retryOrFail(e, retryCount++, maxRetries) } } } diff --git a/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTestTest.kt b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTestTest.kt new file mode 100644 index 0000000000..ecb1e79389 --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/e2e/tests/network/nativeauth/NativeAuthPublicClientApplicationAbstractTestTest.kt @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.e2e.tests.network.nativeauth + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Assert.assertThrows +import org.junit.Test + +class NativeAuthPublicClientApplicationAbstractTestTest : + NativeAuthPublicClientApplicationAbstractTest() { + + @Test + fun retryOperationDoesNotRetryNonThrottleExceptions() { + val expected = IllegalStateException("Non-throttle failure") + var attempts = 0 + + val actual = assertThrows(IllegalStateException::class.java) { + retryOperation(maxRetries = 1) { + attempts++ + throw expected + } + } + + assertSame(expected, actual) + assertEquals(1, attempts) + } +}