Skip to content

Commit 29e54a3

Browse files
@W-21933885: [MSDK Android] App Attestation Implementation (Increase Test Code Coverage For AppAttestationClient)
1 parent 6990e6f commit 29e54a3

2 files changed

Lines changed: 142 additions & 25 deletions

File tree

libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AppAttestationClient.kt

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ import java.util.Base64
5454
* @param deviceId The device id, usually provided by the Salesforce SDK Manager
5555
* @param googleCloudProjectId The Google Cloud Project ID used with Google Play
5656
* Integrity API
57+
* @param integrityManager The Google Play App Integrity API Integrity Manager.
58+
* This parameter is intended for testing purposes only. Defaults to a new
59+
* instance
5760
* @param remoteAccessConsumerKey The Salesforce Connected App (CA) or External
5861
* Client App (ECA)remote access consumer key, usually provided by the boot
5962
* config
@@ -65,13 +68,11 @@ class AppAttestationClient(
6568
val apiHostName: String,
6669
val deviceId: String,
6770
val googleCloudProjectId: Long,
71+
val integrityManager: StandardIntegrityManager = createStandard(context),
6872
val remoteAccessConsumerKey: String,
69-
val restClient: RestClient
73+
val restClient: RestClient,
7074
) {
7175

72-
/** The Google Play Integrity Manager and Token Provider */
73-
private val integrityManager = createStandard(context)
74-
7576

7677
/** The Google Play Integrity API Token Provider */
7778
@VisibleForTesting
@@ -92,13 +93,9 @@ class AppAttestationClient(
9293
* prior to requesting the Integrity Token via
9394
* [createSalesforceOAuthAuthorizationAppAttestation] reduces the latency of
9495
* the request.
95-
* @param integrityManager The Google Play Integrity API integrity manager.
96-
* This parameter is intended for testing purposes only
9796
*/
9897
@VisibleForTesting
99-
internal fun prepareIntegrityTokenProvider(
100-
integrityManager: StandardIntegrityManager = this.integrityManager
101-
) = integrityManager.prepareIntegrityToken(
98+
internal fun prepareIntegrityTokenProvider() = integrityManager.prepareIntegrityToken(
10299
PrepareIntegrityTokenRequest.builder()
103100
.setCloudProjectNumber(googleCloudProjectId)
104101
.build()
@@ -133,19 +130,17 @@ class AppAttestationClient(
133130
* fetched using the "Challenge" as the Request Hash. The resulting token is
134131
* encoded into a value usable as the "attestation" parameter in the
135132
* Salesforce OAuth authorization request.
136-
* @param integrityManager The Google Play Integrity API integrity manager.
137-
* This parameter is intended for testing purposes only
138133
* @param integrityTokenProvider The Google Play App Integrity API Integrity
139134
* Token Provider. This parameter is intended for testing purposes only
140135
* @return The "attestation" value usable in Salesforce OAuth authorization
141136
* and token refresh requests or null if the value cannot be created
142137
*/
143138
suspend fun createSalesforceOAuthAuthorizationAppAttestation(
144-
integrityManager: StandardIntegrityManager = this.integrityManager,
139+
// TODO: Coverage needed. ECJ20260416
145140
integrityTokenProvider: StandardIntegrityTokenProvider? = this.integrityTokenProvider,
146141
): String? {
147142
// Guard to ensure the Google Play Integrity API Integrity Provider was asynchronously resolved or do so synchronously now
148-
val integrityTokenProviderResolved = integrityTokenProvider ?: prepareIntegrityTokenProvider(integrityManager).result
143+
val integrityTokenProviderResolved = integrityTokenProvider ?: prepareIntegrityTokenProvider().result
149144

150145
// Fetch the Salesforce Mobile App Attestation Challenge.
151146
val salesforceAppAttestationChallenge = fetchSalesforceMobileAppAttestationChallenge()
@@ -179,9 +174,9 @@ class AppAttestationClient(
179174
).toBase64String()
180175
}.getOrElse { e ->
181176
// If the Google Play Integrity API failed due to the Integrity Token Provider being expired, re-prepare it once for an inline retry.
177+
// TODO: Coverage needed. ECJ20260416
182178
if ((e as? IntegrityServiceException)?.errorCode == INTEGRITY_TOKEN_PROVIDER_INVALID) {
183179
createSalesforceOAuthAuthorizationAppAttestation(
184-
integrityManager = integrityManager,
185180
integrityTokenProvider = null
186181
)
187182
} else {
@@ -195,6 +190,7 @@ class AppAttestationClient(
195190
* [createSalesforceOAuthAuthorizationAppAttestation]
196191
*/
197192
@JvmName("createSalesforceOAuthAuthorizationAppAttestationBlocking")
193+
// TODO: Coverage needed. ECJ20260416
198194
fun createSalesforceOAuthAuthorizationAppAttestationBlocking() = runBlocking { createSalesforceOAuthAuthorizationAppAttestation() }
199195

200196
/**

libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/AppAttestationClientTest.kt

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.google.android.play.core.integrity.StandardIntegrityManager
3434
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityToken
3535
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
3636
import com.google.android.play.core.integrity.model.StandardIntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID
37+
import com.google.android.play.core.integrity.model.StandardIntegrityErrorCode.INTERNAL_ERROR
3738
import com.salesforce.androidsdk.rest.RestClient
3839
import com.salesforce.androidsdk.rest.RestResponse
3940
import io.mockk.coEvery
@@ -45,6 +46,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
4546
import kotlinx.coroutines.tasks.await
4647
import kotlinx.coroutines.test.advanceUntilIdle
4748
import kotlinx.coroutines.test.runTest
49+
import kotlinx.serialization.json.Json
4850
import org.junit.Assert.assertEquals
4951
import org.junit.Assert.assertNull
5052
import org.junit.Test
@@ -68,19 +70,16 @@ class AppAttestationClientTest {
6870
val integrityManager = mockk<StandardIntegrityManager>(relaxed = true)
6971
every { integrityManager.prepareIntegrityToken(any()) } returns integrityTokenProviderTask
7072

71-
val appAttestationClient = AppAttestationClient(
73+
AppAttestationClient(
7274
apiHostName = "login.example.com",
7375
context = context,
7476
deviceId = deviceId,
7577
googleCloudProjectId = googleCloudProjectId,
78+
integrityManager = integrityManager,
7679
remoteAccessConsumerKey = remoteAccessConsumerKey,
7780
restClient = restClient
7881
)
7982

80-
appAttestationClient.prepareIntegrityTokenProvider(
81-
integrityManager = integrityManager
82-
)
83-
8483
verify(exactly = 1) {
8584
integrityManager.prepareIntegrityToken(match {
8685
it.toString().contains("cloudProjectNumber=654321")
@@ -175,6 +174,9 @@ class AppAttestationClientTest {
175174
restClient = restClient
176175
)
177176

177+
// TODO: Consider refactoring this statement once it proves coverage for AppAttestationClient#145 ECJ20260416
178+
// appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation() // TODO: This won't run without mocks. ECJ20260416
179+
178180
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation(
179181
integrityTokenProvider = integrityTokenProvider
180182
)
@@ -229,12 +231,12 @@ class AppAttestationClientTest {
229231
context = context,
230232
deviceId = deviceId,
231233
googleCloudProjectId = googleCloudProjectId,
234+
integrityManager = integrityManager,
232235
remoteAccessConsumerKey = remoteAccessConsumerKey,
233-
restClient = restClient
236+
restClient = restClient,
234237
)
235238

236239
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation(
237-
integrityManager = integrityManager,
238240
integrityTokenProvider = throwingIntegrityTokenProvider
239241
)
240242

@@ -243,6 +245,65 @@ class AppAttestationClientTest {
243245
assertEquals("eyJhdHRlc3RhdGlvbklkIjoiMTIzNDU2IiwiYXR0ZXN0YXRpb25EYXRhIjoiWDE5VVJWTlVYMGxPVkVWSFVrbFVXVjlVVDB0RlRsOWYifQ==", result)
244246
}
245247

248+
@OptIn(ExperimentalCoroutinesApi::class)
249+
@Test
250+
fun appAttestationClient_createSalesforceOAuthAuthorizationAppAttestationThrowingForUnknownIntegrityServiceException_returnsSuccessfully() = runTest {
251+
252+
val context = mockk<Context>(relaxed = true)
253+
val deviceId = "123456"
254+
val googleCloudProjectId = 654321L
255+
val remoteAccessConsumerKey = "13579"
256+
val restResponse = mockk<RestResponse>(relaxed = true)
257+
every { restResponse.asString() } returns "__TEST_CHALLENGE_VALUE__"
258+
every { restResponse.isSuccess } returns true
259+
val restClient = mockk<RestClient>(relaxed = true)
260+
every { restClient.sendSync(any()) } returns restResponse
261+
262+
val integrityToken = mockk<StandardIntegrityToken>(relaxed = true)
263+
every { integrityToken.token() } returns "__TEST_INTEGRITY_TOKEN__"
264+
val throwingIntegrityTokenTask = mockk<Task<StandardIntegrityToken>>(relaxed = true)
265+
every { throwingIntegrityTokenTask.addOnFailureListener(any()) } returns throwingIntegrityTokenTask
266+
every { throwingIntegrityTokenTask.getResult() } returns integrityToken
267+
mockkStatic("kotlinx.coroutines.tasks.TasksKt")
268+
val integrityServiceException = mockk<IntegrityServiceException>(relaxed = true)
269+
every { integrityServiceException.errorCode } returns INTERNAL_ERROR
270+
coEvery { throwingIntegrityTokenTask.await() } throws integrityServiceException
271+
val throwingIntegrityTokenProvider = mockk<StandardIntegrityTokenProvider>(relaxed = true)
272+
every { throwingIntegrityTokenProvider.request(any()) } returns throwingIntegrityTokenTask
273+
274+
val successfulIntegrityTokenTask = mockk<Task<StandardIntegrityToken>>(relaxed = true)
275+
every { successfulIntegrityTokenTask.addOnFailureListener(any()) } returns successfulIntegrityTokenTask
276+
every { successfulIntegrityTokenTask.getResult() } returns integrityToken
277+
coEvery { successfulIntegrityTokenTask.await() } returns integrityToken
278+
val successfulIntegrityTokenProvider = mockk<StandardIntegrityTokenProvider>(relaxed = true)
279+
every { successfulIntegrityTokenProvider.request(any()) } returns successfulIntegrityTokenTask
280+
281+
val integrityTokenProviderTask = mockk<Task<StandardIntegrityTokenProvider>>(relaxed = true)
282+
every { integrityTokenProviderTask.addOnSuccessListener(any()) } returns integrityTokenProviderTask
283+
every { integrityTokenProviderTask.addOnFailureListener(any()) } returns integrityTokenProviderTask
284+
coEvery { integrityTokenProviderTask.result } returns successfulIntegrityTokenProvider
285+
val integrityManager = mockk<StandardIntegrityManager>(relaxed = true)
286+
every { integrityManager.prepareIntegrityToken(any()) } returns integrityTokenProviderTask
287+
288+
val appAttestationClient = AppAttestationClient(
289+
apiHostName = "login.example.com",
290+
context = context,
291+
deviceId = deviceId,
292+
googleCloudProjectId = googleCloudProjectId,
293+
integrityManager = integrityManager,
294+
remoteAccessConsumerKey = remoteAccessConsumerKey,
295+
restClient = restClient,
296+
)
297+
298+
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation(
299+
integrityTokenProvider = throwingIntegrityTokenProvider
300+
)
301+
302+
advanceUntilIdle()
303+
304+
assertNull(result)
305+
}
306+
246307
@OptIn(ExperimentalCoroutinesApi::class)
247308
@Test
248309
fun appAttestationClient_createSalesforceOAuthAuthorizationAppAttestationThrowingUnknownException_returnsNull() = runTest {
@@ -286,12 +347,12 @@ class AppAttestationClientTest {
286347
context = context,
287348
deviceId = deviceId,
288349
googleCloudProjectId = googleCloudProjectId,
350+
integrityManager = integrityManager,
289351
remoteAccessConsumerKey = remoteAccessConsumerKey,
290-
restClient = restClient
352+
restClient = restClient,
291353
)
292354

293355
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation(
294-
integrityManager = integrityManager,
295356
integrityTokenProvider = throwingIntegrityTokenProvider
296357
)
297358

@@ -337,17 +398,77 @@ class AppAttestationClientTest {
337398
context = context,
338399
deviceId = deviceId,
339400
googleCloudProjectId = googleCloudProjectId,
401+
integrityManager = integrityManager,
340402
remoteAccessConsumerKey = remoteAccessConsumerKey,
341-
restClient = restClient
403+
restClient = restClient,
342404
)
343405

344406
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestation(
345-
integrityManager = integrityManager,
346407
integrityTokenProvider = null
347408
)
348409

349410
advanceUntilIdle()
350411

351412
assertEquals("eyJhdHRlc3RhdGlvbklkIjoiMTIzNDU2IiwiYXR0ZXN0YXRpb25EYXRhIjoiWDE5VVJWTlVYMGxPVkVWSFVrbFVXVjlVVDB0RlRsOWYifQ==", result)
352413
}
414+
415+
@OptIn(ExperimentalCoroutinesApi::class)
416+
@Test
417+
fun appAttestationClient_createSalesforceOAuthAuthorizationAppAttestationBlocking_returnsSuccessfully() = runTest {
418+
419+
val context = mockk<Context>(relaxed = true)
420+
val deviceId = "123456"
421+
val googleCloudProjectId = 654321L
422+
val remoteAccessConsumerKey = "13579"
423+
val restResponse = mockk<RestResponse>(relaxed = true)
424+
every { restResponse.asString() } returns "__TEST_CHALLENGE_VALUE__"
425+
every { restResponse.isSuccess } returns true
426+
val restClient = mockk<RestClient>(relaxed = true)
427+
every { restClient.sendSync(any()) } returns restResponse
428+
429+
val integrityToken = mockk<StandardIntegrityToken>(relaxed = true)
430+
every { integrityToken.token() } returns "__TEST_INTEGRITY_TOKEN__"
431+
val integrityTokenTask = mockk<Task<StandardIntegrityToken>>(relaxed = true)
432+
every { integrityTokenTask.addOnFailureListener(any()) } returns integrityTokenTask
433+
every { integrityTokenTask.getResult() } returns integrityToken
434+
mockkStatic("kotlinx.coroutines.tasks.TasksKt")
435+
coEvery { integrityTokenTask.await() } returns integrityToken
436+
val integrityTokenProvider = mockk<StandardIntegrityTokenProvider>(relaxed = true)
437+
every { integrityTokenProvider.request(any()) } returns integrityTokenTask
438+
439+
val integrityTokenProviderTask = mockk<Task<StandardIntegrityTokenProvider>>(relaxed = true)
440+
every { integrityTokenProviderTask.addOnSuccessListener(any()) } returns integrityTokenProviderTask
441+
every { integrityTokenProviderTask.addOnFailureListener(any()) } returns integrityTokenProviderTask
442+
coEvery { integrityTokenProviderTask.result } returns integrityTokenProvider
443+
val integrityManager = mockk<StandardIntegrityManager>(relaxed = true)
444+
every { integrityManager.prepareIntegrityToken(any()) } returns integrityTokenProviderTask
445+
446+
val appAttestationClient = AppAttestationClient(
447+
apiHostName = "login.example.com",
448+
context = context,
449+
deviceId = deviceId,
450+
googleCloudProjectId = googleCloudProjectId,
451+
integrityManager = integrityManager,
452+
remoteAccessConsumerKey = remoteAccessConsumerKey,
453+
restClient = restClient
454+
)
455+
456+
val result = appAttestationClient.createSalesforceOAuthAuthorizationAppAttestationBlocking()
457+
458+
advanceUntilIdle()
459+
460+
assertEquals("eyJhdHRlc3RhdGlvbklkIjoiMTIzNDU2IiwiYXR0ZXN0YXRpb25EYXRhIjoiWDE5VVJWTlVYMGxPVkVWSFVrbFVXVjlVVDB0RlRsOWYifQ==", result)
461+
}
462+
463+
@Test
464+
fun oAuthAuthorizationAttestationData_encode_returnsSuccessfully() {
465+
466+
val result = Json.decodeFromString(
467+
OAuthAuthorizationAttestation.serializer(),
468+
"{ \"attestationId\": \"123456\", \"attestationData\": \"W19VVlJTVVhNbExPVkVWSFVrbFVXVjlVVDB0RlRsOWYifQ==\" }"
469+
)
470+
471+
assertEquals("123456", result.attestationId)
472+
assertEquals("W19VVlJTVVhNbExPVkVWSFVrbFVXVjlVVDB0RlRsOWYifQ==", result.attestationData)
473+
}
353474
}

0 commit comments

Comments
 (0)