Skip to content

Commit d526e06

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

3 files changed

Lines changed: 173 additions & 153 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: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ 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
@@ -105,51 +105,48 @@ internal class IDPAuthCodeHelper @VisibleForTesting internal constructor(
105105

106106
/**
107107
* Compute relative path of authorization url for SP
108-
* @param salesforceSDKManager The Salesforce SDK manager instance. This
108+
* @param authorizationUrlProvider Gets the Salesforce OAuth2 authorization
109+
* URL. This parameter is intended for testing purposes only. Defaults to
110+
* OAuth2.getAuthorizationUrl for production use
111+
* @param salesforceSdkManager The Salesforce SDK manager instance. This
109112
* parameter is intended for testing purposes only. Defaults to the
110113
* singleton instance for production use
111-
* @return authorization relative path
114+
* @return The authorization relative path
112115
*/
113116
@VisibleForTesting
114117
internal suspend fun getAuthorizationPathForSP(
115-
salesforceSDKManager: SalesforceSDKManager = SalesforceSDKManager.getInstance(),
116-
limitTest: Boolean = false,
118+
authorizationUrlProvider: AuthorizationUrlProvider = { useWebServerAuthentication, useHybridAuthentication, loginServer, clientId, callbackUrl, scopes, loginHint, displayType, codeChallenge, additionalParams, sdkManager ->
119+
getAuthorizationUrl(useWebServerAuthentication, useHybridAuthentication, loginServer, clientId, callbackUrl, scopes, loginHint, displayType, codeChallenge, additionalParams, sdkManager)
120+
},
121+
salesforceSdkManager: SalesforceSDKManager = SalesforceSDKManager.getInstance(),
117122
): String? {
118123
SalesforceSDKLogger.d(TAG, "Getting authorization url")
119-
val context = salesforceSDKManager.appContext
120-
val useHybridAuthentication = salesforceSDKManager.useHybridAuthentication
124+
val context = salesforceSdkManager.appContext
125+
val useHybridAuthentication = salesforceSdkManager.useHybridAuthentication
121126

122-
// Add Salesforce Mobile App Attestation parameter to authorization URL if applicable.
123-
if (limitTest) {
124-
return null // LIMIT TEST 1
125-
}
126127
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)
128+
val challenge = fetchMobileAppAttestationChallenge()
129+
val attestation = createAppAttestation(challenge) ?: return@run null
130+
mapOf(ATTESTATION to attestation)
133131
}
134132

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
133+
val authorizationUri = authorizationUrlProvider(
134+
true, // Use web server flow
135+
useHybridAuthentication,
136+
URI(userAccount.loginServer),
137+
spConfig.oauthClientId,
138+
spConfig.oauthCallbackUrl,
139+
spConfig.oauthScopes,
140+
null, // Login Hint
141+
context.getString(oauth_display_type),
142+
codeChallenge,
143+
additionalParams,
144+
salesforceSdkManager
145+
)
146+
147+
return authorizationUri?.let {
148+
it.path + (it.query?.let { query -> "?$query" } ?: "")
149+
}
153150
}
154151

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

0 commit comments

Comments
 (0)