Skip to content

Commit 9919599

Browse files
@W-21933885: [MSDK Android] App Attestation Implementation (Significant Update To Avoid mockkStatic In IDPAuthCodeHelperTest.kt)
1 parent 5f968fa commit 9919599

3 files changed

Lines changed: 137 additions & 84 deletions

File tree

CLAUDE.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,52 @@ See [README.md](README.md) for basic setup. Commands below are for contributors
126126
- **Test data cleanup**: Every test must clean up created soups, user accounts, and cached data. Use `@Before`/`@After` rigorously.
127127
- **Test credentials**: Tests requiring authentication need `test_credentials.json` in `shared/test/`.
128128

129+
### Firebase Test Lab Considerations
130+
131+
**CRITICAL: MockK `mockkStatic()` Does Not Work on Firebase Test Lab**
132+
133+
Firebase Test Lab silently disables MockK's static mocking, causing tests to execute real implementations. Tests pass locally but timeout (60s) on Firebase with `UncompletedCoroutinesError` as real network/Google Play Services calls execute.
134+
135+
**Root Cause:** Firebase's SELinux policies block MockK bytecode instrumentation, APK re-signing breaks inline mocking agent, and real Google Play Services execute unexpected calls.
136+
137+
**DO NOT USE:**
138+
```kotlin
139+
mockkStatic(OAuth2::class) // ❌ Fails silently on Firebase
140+
```
141+
142+
**REQUIRED ALTERNATIVES:**
143+
144+
1. **Parameter Injection (Simplest):**
145+
```kotlin
146+
// Add parameter with default value pointing to static method
147+
fun myFunction(
148+
helper: (String) -> Int = { StaticClass.staticMethod(it) }
149+
) {
150+
val result = helper("input")
151+
}
152+
153+
// Test injects mock lambda
154+
myFunction(helper = { "mock result" })
155+
```
156+
157+
2. **TypeAlias for Complex Static Methods:**
158+
```kotlin
159+
// Define function type
160+
typealias GetAuthUrl = (Boolean, URI, String, String) -> URI?
161+
162+
// Use as parameter with default calling static method
163+
suspend fun authorize(
164+
getAuthUrl: GetAuthUrl = { a, b, c, d -> OAuth2.getAuthUrl(a, b, c, d) }
165+
) { /* ... */ }
166+
167+
// Test injects simple mock
168+
authorize(getAuthUrl = { _, _, _, _ -> URI("mock://url") })
169+
```
170+
171+
**Rule:** Never use `mockkStatic()` in instrumented tests. Always use parameter injection to enable mocking.
172+
173+
---
174+
129175
### What to Test for Each Library
130176

131177
**SalesforceSDK (Core)**:

libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ import android.webkit.WebResourceRequest
3131
import android.webkit.WebView
3232
import android.webkit.WebViewClient
3333
import androidx.annotation.VisibleForTesting
34-
import com.salesforce.androidsdk.R
34+
import com.salesforce.androidsdk.R.string.oauth_display_type
3535
import com.salesforce.androidsdk.accounts.UserAccount
3636
import com.salesforce.androidsdk.app.SalesforceSDKManager
3737
import com.salesforce.androidsdk.auth.AppAttestationClient
38+
import com.salesforce.androidsdk.auth.OAuth2
3839
import com.salesforce.androidsdk.auth.OAuth2.ATTESTATION
3940
import com.salesforce.androidsdk.auth.OAuth2.FRONTDOOR_URL_KEY
4041
import com.salesforce.androidsdk.auth.OAuth2.getAuthorizationUrl
@@ -105,51 +106,48 @@ internal class IDPAuthCodeHelper @VisibleForTesting internal constructor(
105106

106107
/**
107108
* Compute relative path of authorization url for SP
108-
* @param salesforceSDKManager The Salesforce SDK manager instance. This
109+
* @param getAuthorizationUrl Gets the Salesforce OAuth2 authorization URL.
110+
* This parameter is intended for testing purposes only. Defaults to
111+
* OAuth2.getAuthorizationUrl for production use
112+
* @param salesforceSdkManager The Salesforce SDK manager instance. This
109113
* parameter is intended for testing purposes only. Defaults to the
110114
* singleton instance for production use
111-
* @return authorization relative path
115+
* @return The authorization relative path
112116
*/
113117
@VisibleForTesting
114118
internal suspend fun getAuthorizationPathForSP(
115-
salesforceSDKManager: SalesforceSDKManager = SalesforceSDKManager.getInstance(),
116-
limitTest: Boolean = false,
119+
getAuthorizationUrl: GetAuthorizationUrl = { useWebServerAuthentication, useHybridAuthentication, loginServer, clientId, callbackUrl, scopes, loginHint, displayType, codeChallenge, additionalParams, sdkManager ->
120+
OAuth2.getAuthorizationUrl(useWebServerAuthentication, useHybridAuthentication, loginServer, clientId, callbackUrl, scopes, loginHint, displayType, codeChallenge, additionalParams, sdkManager)
121+
},
122+
salesforceSdkManager: SalesforceSDKManager = SalesforceSDKManager.getInstance(),
117123
): String? {
118124
SalesforceSDKLogger.d(TAG, "Getting authorization url")
119-
val context = salesforceSDKManager.appContext
120-
val useHybridAuthentication = salesforceSDKManager.useHybridAuthentication
125+
val context = salesforceSdkManager.appContext
126+
val useHybridAuthentication = salesforceSdkManager.useHybridAuthentication
121127

122-
// Add Salesforce Mobile App Attestation parameter to authorization URL if applicable.
123-
if (limitTest) {
124-
return null // LIMIT TEST 1
125-
}
126128
val additionalParams = appAttestationClient?.run {
127-
// val challenge = fetchMobileAppAttestationChallenge() // LIMIT TEST 1.1
128-
// if (limitTest) {
129-
// return null // LIMIT TEST 2
130-
// }
131-
// val attestation = createAppAttestation(challenge) ?: return@run null
132-
// mapOf(ATTESTATION to attestation)
129+
val challenge = fetchMobileAppAttestationChallenge()
130+
val attestation = createAppAttestation(challenge) ?: return@run null
131+
mapOf(ATTESTATION to attestation)
133132
}
134133

135-
// val authorizationUri = getAuthorizationUrl(
136-
// true, // use web server flow
137-
// useHybridAuthentication,
138-
// URI(userAccount.loginServer),
139-
// spConfig.oauthClientId,
140-
// spConfig.oauthCallbackUrl,
141-
// spConfig.oauthScopes,
142-
// null, // Login Hint
143-
// context.getString(R.string.oauth_display_type),
144-
// codeChallenge,
145-
// additionalParams,
146-
// salesforceSDKManager
147-
// )
148-
149-
// return authorizationUri?.let {
150-
// it.path + (it.query?.let { query -> "?$query" } ?: "")
151-
// } ?: null
152-
return null
134+
val authorizationUri = getAuthorizationUrl(
135+
true, // Use web server flow
136+
useHybridAuthentication,
137+
URI(userAccount.loginServer),
138+
spConfig.oauthClientId,
139+
spConfig.oauthCallbackUrl,
140+
spConfig.oauthScopes,
141+
null, // Login Hint
142+
context.getString(oauth_display_type),
143+
codeChallenge,
144+
additionalParams,
145+
salesforceSdkManager
146+
)
147+
148+
return authorizationUri?.let {
149+
it.path + (it.query?.let { query -> "?$query" } ?: "")
150+
}
153151
}
154152

155153
fun getFrontdoorUrl(restClient:RestClient, redirectUri: String): String? {
@@ -239,4 +237,21 @@ internal class IDPAuthCodeHelper @VisibleForTesting internal constructor(
239237
IDPAuthCodeHelper(webView, userAccount, spConfig, codeChallenge, onResult).generateAuthCode()
240238
}
241239
}
242-
}
240+
}
241+
242+
/**
243+
* Computes a Salesforce OAuth authorization URL.
244+
*/
245+
typealias GetAuthorizationUrl = (
246+
useWebServerAuthentication: Boolean,
247+
useHybridAuthentication: Boolean,
248+
loginServer: URI,
249+
clientId: String,
250+
callbackUrl: String,
251+
scopes: Array<String>,
252+
loginHint: String,
253+
displayType: String,
254+
codeChallenge: String,
255+
additionalParams: Map<String, String>,
256+
salesforceSdkManager: SalesforceSDKManager
257+
) -> URI?

libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/idp/IDPAuthCodeHelperTest.kt

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
3232
import com.salesforce.androidsdk.accounts.UserAccount
3333
import com.salesforce.androidsdk.app.SalesforceSDKManager
3434
import com.salesforce.androidsdk.auth.AppAttestationClient
35-
import com.salesforce.androidsdk.auth.OAuth2
3635
import io.mockk.coEvery
3736
import io.mockk.every
3837
import io.mockk.mockk
39-
import io.mockk.mockkStatic
4038
import io.mockk.unmockkAll
4139
import kotlinx.coroutines.ExperimentalCoroutinesApi
4240
import kotlinx.coroutines.test.advanceUntilIdle
@@ -60,15 +58,19 @@ class IDPAuthCodeHelperTest {
6058
unmockkAll()
6159
}
6260

63-
@Ignore
6461
@OptIn(ExperimentalCoroutinesApi::class)
6562
@Test
6663
fun idpAuthCodeHelper_getAuthorizationPathForSP_whenNoAttestationClient_returnsPathAndQueryWithoutAttestation() = runTest {
6764

6865
val mockSDKManager = createMockSalesforceSDKManager()
6966
val idpAuthCodeHelper = createIdpAuthCodeHelper(appAttestationClient = null)
7067

71-
val result = idpAuthCodeHelper.getAuthorizationPathForSP(mockSDKManager)
68+
val result = idpAuthCodeHelper.getAuthorizationPathForSP(
69+
getAuthorizationUrl = { _, _, _, _, _, _, _, _, _, _, _ ->
70+
URI("http://www.example.com")
71+
},
72+
salesforceSdkManager = mockSDKManager
73+
)
7274

7375
advanceUntilIdle()
7476

@@ -97,39 +99,31 @@ class IDPAuthCodeHelperTest {
9799
)
98100
}
99101

100-
// TODO: Prototype test. ECJ20260424
101-
// @Ignore
102-
// kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 1m, the test body did not run to completion
103-
// at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$2.invokeSuspend$lambda$0(TestBuilders.kt:354)
104-
@OptIn(ExperimentalCoroutinesApi::class)
102+
// TODO: PROTOTYPE TEST FOR GOOGLE FIREBASE TEST LAB. ECJ20260424
105103
@Test
106104
fun idpAuthCodeHelper_getAuthorizationPathForSP_whenAttestationClientReturnsAttestation_includesAttestationInQuery() = runTest {
107105

108-
val mockSDKManager = createMockSalesforceSDKManager()
106+
val sdkManager = createMockSalesforceSDKManager()
109107
val appAttestationClient = createMockAttestationClient(attestation = TEST_APP_ATTESTATION)
110108
val idpAuthCodeHelper = createIdpAuthCodeHelper(appAttestationClient = appAttestationClient)
111109

112110
// Mock OAuth2.getAuthorizationUrl to return a URI with attestation in the query
113-
stubOAuthAuthorizationUrl(
114-
returnValue = URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH?attestation=$TEST_APP_ATTESTATION&other=params"),
115-
)
116-
117111
val result = idpAuthCodeHelper.getAuthorizationPathForSP(
118-
salesforceSDKManager = mockSDKManager,
119-
limitTest = true,
112+
getAuthorizationUrl = { _, _, _, _, _, _, _, _, _, _, _ ->
113+
URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH?attestation=$TEST_APP_ATTESTATION&other=params")
114+
},
115+
salesforceSdkManager = sdkManager
120116
)
121117

122-
// advanceUntilIdle() // Limit Test 1.2
123-
124-
// val nonNullResult = requireNotNull(result) {
125-
// "Result should be non-null for a valid login server."
126-
// }
127-
// assertTrue(
128-
// "Result should contain 'attestation=$TEST_APP_ATTESTATION' but was '$nonNullResult'.",
129-
// nonNullResult.contains("attestation=$TEST_APP_ATTESTATION"),
130-
// )
131-
assertTrue(true)
118+
advanceUntilIdle()
132119

120+
val nonNullResult = requireNotNull(result) {
121+
"Result should be non-null for a valid login server."
122+
}
123+
assertTrue(
124+
"Result should contain 'attestation=$TEST_APP_ATTESTATION' but was '$nonNullResult'.",
125+
nonNullResult.contains("attestation=$TEST_APP_ATTESTATION"),
126+
)
133127
}
134128

135129
// Your app crashed due to an ANR. Take a look at your logs to dig deeper.
@@ -138,17 +132,18 @@ class IDPAuthCodeHelperTest {
138132
@Test
139133
fun idpAuthCodeHelper_getAuthorizationPathForSP_whenCreateAppAttestationReturnsNull_excludesAttestationFromQuery() = runTest {
140134

141-
val mockSDKManager = createMockSalesforceSDKManager()
135+
val sdkManager = createMockSalesforceSDKManager()
142136
val appAttestationClient = createMockAttestationClient(attestation = null)
143137
val idpAuthCodeHelper = createIdpAuthCodeHelper(appAttestationClient = appAttestationClient)
144138

145139
// Mock OAuth2.getAuthorizationUrl to return a URI without attestation in the query
146-
stubOAuthAuthorizationUrl(
147-
returnValue = URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH?other=params")
140+
val result = idpAuthCodeHelper.getAuthorizationPathForSP(
141+
getAuthorizationUrl = { _, _, _, _, _, _, _, _, _, _, _ ->
142+
URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH?other=params")
143+
},
144+
salesforceSdkManager = sdkManager
148145
)
149146

150-
val result = idpAuthCodeHelper.getAuthorizationPathForSP(mockSDKManager)
151-
152147
advanceUntilIdle()
153148

154149
val nonNullResult = requireNotNull(result) {
@@ -166,11 +161,15 @@ class IDPAuthCodeHelperTest {
166161
@Test
167162
fun idpAuthCodeHelper_getAuthorizationPathForSP_whenAuthorizationUrlIsNull_returnsNull() = runTest {
168163

169-
val mockSDKManager = createMockSalesforceSDKManager()
170-
stubOAuthAuthorizationUrl(returnValue = null)
164+
val sdkManager = createMockSalesforceSDKManager()
171165
val idpAuthCodeHelper = createIdpAuthCodeHelper(appAttestationClient = null)
172166

173-
val result = idpAuthCodeHelper.getAuthorizationPathForSP(mockSDKManager)
167+
val result = idpAuthCodeHelper.getAuthorizationPathForSP(
168+
getAuthorizationUrl = { _, _, _, _, _, _, _, _, _, _, _ ->
169+
null
170+
},
171+
salesforceSdkManager = sdkManager
172+
)
174173

175174
advanceUntilIdle()
176175

@@ -183,11 +182,15 @@ class IDPAuthCodeHelperTest {
183182
@Test
184183
fun idpAuthCodeHelper_getAuthorizationPathForSP_whenAuthorizationUrlHasNoQuery_returnsPathOnly() = runTest {
185184

186-
val mockSDKManager = createMockSalesforceSDKManager()
187-
stubOAuthAuthorizationUrl(returnValue = URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH"))
185+
val sdkManager = createMockSalesforceSDKManager()
188186
val idpAuthCodeHelper = createIdpAuthCodeHelper(appAttestationClient = null)
189187

190-
val result = idpAuthCodeHelper.getAuthorizationPathForSP(mockSDKManager)
188+
val result = idpAuthCodeHelper.getAuthorizationPathForSP(
189+
getAuthorizationUrl = { _, _, _, _, _, _, _, _, _, _, _ ->
190+
URI("$TEST_LOGIN_SERVER$OAUTH_AUTHORIZE_PATH")
191+
},
192+
salesforceSdkManager = sdkManager
193+
)
191194

192195
advanceUntilIdle()
193196

@@ -233,17 +236,6 @@ class IDPAuthCodeHelperTest {
233236
appAttestationClient = appAttestationClient,
234237
)
235238

236-
private fun stubOAuthAuthorizationUrl(returnValue: URI?) {
237-
// Force OAuth2 class initialization before mocking to avoid ExceptionInInitializerError
238-
OAuth2.TIMESTAMP_FORMAT
239-
mockkStatic(OAuth2::class)
240-
every {
241-
OAuth2.getAuthorizationUrl(
242-
any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(),
243-
)
244-
} returns returnValue
245-
}
246-
247239
// endregion Helpers
248240

249241
private companion object {

0 commit comments

Comments
 (0)