@@ -41,6 +41,7 @@ import com.salesforce.androidsdk.config.OAuthConfig
4141import com.salesforce.androidsdk.security.SalesforceKeyGenerator.getSHA256Hash
4242import com.salesforce.androidsdk.ui.LoginActivity.Companion.ABOUT_BLANK
4343import com.salesforce.androidsdk.ui.LoginViewModel
44+ import io.mockk.coEvery
4445import io.mockk.coVerify
4546import io.mockk.every
4647import io.mockk.mockk
@@ -69,6 +70,10 @@ import java.net.URI
6970private const val FAKE_SERVER_URL = " shouldMatchNothing.salesforce.com"
7071private const val FAKE_JWT = " 1234"
7172private const val FAKE_JWT_FLOW_AUTH = " 5678"
73+ private const val TEST_ATTESTATION_SERVER = " test.salesforce.com"
74+ private const val TEST_CHALLENGE_VALUE = " __TEST_CHALLENGE_VALUE__"
75+ private const val TEST_APP_ATTESTATION = " __TEST_APP_ATTESTATION__"
76+ private const val ATTESTATION_QUERY_PARAM_PREFIX = " attestation="
7277
7378@OptIn(ExperimentalCoroutinesApi ::class )
7479@RunWith(AndroidJUnit4 ::class )
@@ -373,6 +378,7 @@ class LoginViewModelTest {
373378 scopes = listOf (" api" ),
374379 )
375380 }
381+ every { sdkManagerMock.appAttestationClient } returns null
376382 val debugConsumerKey = " debug_override_key_789"
377383 val debugRedirectUri = " debug://redirect"
378384 val debugScopes = listOf (" api" , " debug_scope" )
@@ -418,6 +424,7 @@ class LoginViewModelTest {
418424 scopes = listOf (" api" ),
419425 )
420426 }
427+ every { sdkManagerMock.appAttestationClient } returns null
421428 val debugConsumerKey = " debug_override_key_789"
422429 val debugRedirectUri = " debug://redirect"
423430 val debugScopes = listOf (" api" , " debug_scope" )
@@ -674,6 +681,55 @@ class LoginViewModelTest {
674681 }
675682 }
676683
684+ @Test
685+ fun getAuthorizationUrl_WithNullAppAttestationClient_OmitsAttestationParam () = runBlocking {
686+ val sdkManagerMock = createSdkManagerMockForAttestation(appAttestationClient = null )
687+ val freshViewModel = LoginViewModel (bootConfig)
688+
689+ val loginUrl = freshViewModel.getAuthorizationUrl(TEST_ATTESTATION_SERVER , sdkManagerMock)
690+
691+ assertFalse(
692+ " URL should NOT contain an attestation parameter but was '$loginUrl '." ,
693+ loginUrl.contains(ATTESTATION_QUERY_PARAM_PREFIX ),
694+ )
695+ }
696+
697+ @Test
698+ fun getAuthorizationUrl_WithAppAttestationClient_IncludesAttestationParam () = runBlocking {
699+ val appAttestationClient = createMockAppAttestationClient(attestation = TEST_APP_ATTESTATION )
700+ val sdkManagerMock = createSdkManagerMockForAttestation(appAttestationClient = appAttestationClient)
701+ val freshViewModel = LoginViewModel (bootConfig)
702+
703+ val loginUrl = freshViewModel.getAuthorizationUrl(TEST_ATTESTATION_SERVER , sdkManagerMock)
704+
705+ assertTrue(
706+ " URL should contain '$ATTESTATION_QUERY_PARAM_PREFIX$TEST_APP_ATTESTATION ' but was '$loginUrl '." ,
707+ loginUrl.contains(" $ATTESTATION_QUERY_PARAM_PREFIX$TEST_APP_ATTESTATION " ),
708+ )
709+ coVerify(exactly = 1 ) {
710+ appAttestationClient.fetchMobileAppAttestationChallenge()
711+ appAttestationClient.createAppAttestation(appAttestationChallenge = TEST_CHALLENGE_VALUE )
712+ }
713+ }
714+
715+ @Test
716+ fun getAuthorizationUrl_WhenCreateAppAttestationReturnsNull_OmitsAttestationParam () = runBlocking {
717+ val appAttestationClient = createMockAppAttestationClient(attestation = null )
718+ val sdkManagerMock = createSdkManagerMockForAttestation(appAttestationClient = appAttestationClient)
719+ val freshViewModel = LoginViewModel (bootConfig)
720+
721+ val loginUrl = freshViewModel.getAuthorizationUrl(TEST_ATTESTATION_SERVER , sdkManagerMock)
722+
723+ assertFalse(
724+ " URL should NOT contain an attestation parameter but was '$loginUrl '." ,
725+ loginUrl.contains(ATTESTATION_QUERY_PARAM_PREFIX ),
726+ )
727+ coVerify(exactly = 1 ) {
728+ appAttestationClient.fetchMobileAppAttestationChallenge()
729+ appAttestationClient.createAppAttestation(appAttestationChallenge = TEST_CHALLENGE_VALUE )
730+ }
731+ }
732+
677733 @Test
678734 fun loginViewModel_applyPendingLoginServer_returns_onNullPendingLoginServer () {
679735
@@ -1272,6 +1328,25 @@ class LoginViewModelTest {
12721328 assertEquals(result, viewModel.getValidServerUrl(value))
12731329 }
12741330
1331+ private fun createSdkManagerMockForAttestation (
1332+ appAttestationClient : AppAttestationClient ? ,
1333+ ): SalesforceSDKManager = mockk<SalesforceSDKManager >(relaxed = true ).also { mock ->
1334+ every { mock.useHybridAuthentication } returns false
1335+ every { mock.isDebugBuild } returns false
1336+ every { mock.debugOverrideAppConfig } returns null
1337+ every { mock.appConfigForLoginHost } returns { _ -> null }
1338+ every { mock.appAttestationClient } returns appAttestationClient
1339+ }
1340+
1341+ private fun createMockAppAttestationClient (
1342+ attestation : String? ,
1343+ ): AppAttestationClient = mockk<AppAttestationClient >(relaxed = true ).also { client ->
1344+ every { client.fetchMobileAppAttestationChallenge() } returns TEST_CHALLENGE_VALUE
1345+ coEvery {
1346+ client.createAppAttestation(appAttestationChallenge = TEST_CHALLENGE_VALUE )
1347+ } returns attestation
1348+ }
1349+
12751350 private fun generateExpectedAuthorizationUrl (
12761351 server : String ,
12771352 codeChallenge : String ,
0 commit comments