Skip to content

Commit ca7876b

Browse files
committed
Add TokenMigrationActivity tests.
1 parent 33d95e1 commit ca7876b

6 files changed

Lines changed: 813 additions & 166 deletions

File tree

.github/test-shards/SalesforceSDK.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"class com.salesforce.androidsdk.ui.DevInfoActivityTest",
2525
"class com.salesforce.androidsdk.security.ScreenLockManagerTest",
2626
"class com.salesforce.androidsdk.security.BiometricAuthenticationManagerTest",
27-
"class com.salesforce.androidsdk.ui.TokenMigrationActivityTest"
27+
"class com.salesforce.androidsdk.ui.TokenMigrationActivityTest",
28+
"class com.salesforce.androidsdk.ui.TokenMigrationWebViewTest"
2829
]
2930
},
3031
{
@@ -70,7 +71,8 @@
7071
"notClass com.salesforce.androidsdk.rest.files.RenditionTypeTest",
7172
"notClass com.salesforce.androidsdk.rest.files.ConnectUriBuilderTest",
7273
"notClass com.salesforce.androidsdk.rest.files.FileRequestsTest",
73-
"notClass com.salesforce.androidsdk.ui.TokenMigrationActivityTest"
74+
"notClass com.salesforce.androidsdk.ui.TokenMigrationActivityTest",
75+
"nonClass com.salesforce.androidsdk.ui.TokenMigrationWebViewTest"
7476
]
7577
}
7678
]

libs/SalesforceSDK/src/com/salesforce/androidsdk/accounts/UserAccountManagerExtension.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,17 @@ fun UserAccountManager.migrateRefreshToken(
7373
)
7474
)
7575

76-
val context = SalesforceSDKManager.getInstance().appContext
77-
val intent = Intent(context, TokenMigrationActivity::class.java)
78-
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
79-
intent.putExtra(TokenMigrationActivity.EXTRA_ORG_ID, orgId)
80-
intent.putExtra(TokenMigrationActivity.EXTRA_USER_ID, userId)
81-
intent.putExtra(TokenMigrationActivity.EXTRA_OAUTH_CONFIG, appConfig)
82-
intent.putExtra(TokenMigrationActivity.EXTRA_CALLBACK_ID, callbackKey)
83-
context.startActivity(intent)
76+
with(SalesforceSDKManager.getInstance().appContext) {
77+
startActivity(
78+
Intent(/* packageContext = */ this, TokenMigrationActivity::class.java).apply {
79+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
80+
putExtra(TokenMigrationActivity.EXTRA_ORG_ID, orgId)
81+
putExtra(TokenMigrationActivity.EXTRA_USER_ID, userId)
82+
putExtra(TokenMigrationActivity.EXTRA_OAUTH_CONFIG, appConfig)
83+
putExtra(TokenMigrationActivity.EXTRA_CALLBACK_ID, callbackKey)
84+
}
85+
)
86+
}
8487
}
8588

8689
/*

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/TokenMigrationActivity.kt

Lines changed: 112 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2026-pffsent, salesforce.com, inc.
2+
* Copyright (c) 2026-present, salesforce.com, inc.
33
* All rights reserved.
44
* Redistribution and use of this software in source and binary forms, with or
55
* without modification, are permitted provided that the following conditions
@@ -27,6 +27,7 @@
2727
package com.salesforce.androidsdk.ui
2828

2929
import android.annotation.SuppressLint
30+
import android.content.Context
3031
import android.graphics.Bitmap
3132
import android.os.Bundle
3233
import android.webkit.WebResourceRequest
@@ -37,7 +38,6 @@ import androidx.activity.compose.setContent
3738
import androidx.activity.enableEdgeToEdge
3839
import androidx.activity.viewModels
3940
import androidx.annotation.VisibleForTesting
40-
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
4141
import androidx.compose.animation.core.animateFloatAsState
4242
import androidx.compose.animation.core.tween
4343
import androidx.compose.foundation.background
@@ -82,10 +82,26 @@ import kotlinx.serialization.json.Json
8282
import kotlinx.serialization.json.jsonObject
8383
import kotlinx.serialization.json.jsonPrimitive
8484

85-
const val HALF_ALPHA = 0.5f
85+
private const val HALF_ALPHA = 0.5f
86+
87+
// Error messages - internal for testing
88+
@VisibleForTesting
89+
internal const val ERROR_PARSE_CALLBACK_ID = "Unable to parse MigrationResult callback id."
90+
@VisibleForTesting
91+
internal const val ERROR_RETRIEVE_CALLBACK = "Unable to retrieve MigrationResult callback."
92+
@VisibleForTesting
93+
internal const val ERROR_PARSE_OAUTH_CONFIG = "Unable to parse OAuthConfig."
94+
@VisibleForTesting
95+
internal const val ERROR_BUILD_USER_ACCOUNT = "Unable to build user account."
96+
@VisibleForTesting
97+
internal const val ERROR_BUILD_REST_CLIENT = "Unable to build RestClient."
98+
@VisibleForTesting
99+
internal const val ERROR_SINGLE_ACCESS_FAILED = "Request for single access bridge url failed"
100+
@VisibleForTesting
101+
internal const val ERROR_TOKEN_INVALID_DESC = "User's existing token may be invalid."
102+
86103
internal class TokenMigrationActivity : ComponentActivity() {
87104

88-
@VisibleForTesting(otherwise = PROTECTED)
89105
private val viewModel: LoginViewModel
90106
by viewModels { SalesforceSDKManager.getInstance().loginViewModelFactory }
91107

@@ -94,36 +110,36 @@ internal class TokenMigrationActivity : ComponentActivity() {
94110
enableEdgeToEdge()
95111

96112
val callbackKey = intent.getStringExtra(EXTRA_CALLBACK_ID) ?: run {
97-
SalesforceSDKLogger.e(TAG, "Unable to parse MigrationResult callback id.")
113+
SalesforceSDKLogger.e(TAG, ERROR_PARSE_CALLBACK_ID)
98114
finish()
99115
return
100116
}
101117
val resultCallback = MigrationCallbackRegistry.consume(callbackKey) ?: run {
102-
SalesforceSDKLogger.e(TAG, "Unable to retrieve MigrationResult callback.")
118+
SalesforceSDKLogger.e(TAG, ERROR_RETRIEVE_CALLBACK)
103119
finish()
104120
return
105121
}
106122

107123
// TODO: Move to non-deprecated getParcelableExtra when min API >= 33
108124
val oAuthConfig = intent.getParcelableExtra<OAuthConfig>(EXTRA_OAUTH_CONFIG) ?: run {
109-
logMigrationError(resultCallback, "Unable to parse OAuthConfig.", null, null)
125+
logMigrationError(resultCallback, ERROR_PARSE_OAUTH_CONFIG, null, null)
110126
return
111127
}
112128

113129
val orgId = intent.getStringExtra(EXTRA_ORG_ID)
114130
val userId = intent.getStringExtra(EXTRA_USER_ID)
115131
if ( orgId == null || userId == null) {
116-
logMigrationError(resultCallback, "Unable to parse OAuthConfig.", null, null)
132+
logMigrationError(resultCallback, ERROR_PARSE_OAUTH_CONFIG, null, null)
117133
return
118134
}
119135
val user = UserAccountManager.getInstance().getUserFromOrgAndUserId(orgId, userId) ?: run {
120-
logMigrationError(resultCallback, "Unable to build user account.", null, null)
136+
logMigrationError(resultCallback, ERROR_BUILD_USER_ACCOUNT, null, null)
121137
return
122138
}
123139
val client = runCatching {
124140
SalesforceSDKManager.getInstance().clientManager.peekRestClient(user)
125141
}.getOrElse { e ->
126-
logMigrationError(resultCallback, "Unable to build RestClient.", null, e as? Exception)
142+
logMigrationError(resultCallback, ERROR_BUILD_REST_CLIENT, null, e as? Exception)
127143
return
128144
}
129145

@@ -149,8 +165,8 @@ internal class TokenMigrationActivity : ComponentActivity() {
149165
} ?: run {
150166
logMigrationError(
151167
resultCallback = resultCallback,
152-
error = "Request for single access bridge url failed",
153-
errorDesc = "User's existing token may be invalid.",
168+
error = ERROR_SINGLE_ACCESS_FAILED,
169+
errorDesc = ERROR_TOKEN_INVALID_DESC,
154170
e = null,
155171
)
156172
return@launch
@@ -199,100 +215,105 @@ internal class TokenMigrationActivity : ComponentActivity() {
199215
frontDoorUrl: String,
200216
resultCallback: MigrationCallbackRegistry.MigrationCallbacks,
201217
instanceServer: String,
202-
): WebView = WebView(this@TokenMigrationActivity).apply {
218+
): WebView = webViewFactory(this@TokenMigrationActivity).apply {
203219
@SuppressLint("SetJavaScriptEnabled") // Required by Salesforce
204220
settings.javaScriptEnabled = true
205221
settings.userAgentString = SalesforceSDKManager.getInstance().userAgent
206222
setBackgroundColor(android.graphics.Color.TRANSPARENT)
223+
webViewClient = TokenMigrationClientManager(resultCallback, instanceServer)
207224

208-
// This implementation is very similar to [LoginActivity.AuthWebViewClient] but the
209-
// code cannot be shared due to the heavy reliance on the ViewModel.
210-
webViewClient = object : WebViewClient() {
211-
212-
override fun shouldOverrideUrlLoading(
213-
view: WebView,
214-
request: WebResourceRequest,
215-
): Boolean {
216-
val url = request.url.toString().replace("///", "/").lowercase()
217-
val callbackUrl = viewModel.oAuthConfig.redirectUri.replace("///", "/").lowercase()
218-
val migrationFinished = url.startsWith(callbackUrl)
219-
220-
if (migrationFinished) {
221-
viewModel.authFinished.value = true
222-
viewModel.loading.value = true
223-
224-
val params = UriFragmentParser.parse(request.url)
225-
val error = params["error"]
226-
// Did we fail?
227-
when {
228-
error != null -> {
229-
logMigrationError(
230-
resultCallback = resultCallback,
231-
error = error,
232-
errorDesc = params["error_description"],
233-
e = null,
234-
)
235-
}
225+
loadUrl(frontDoorUrl)
226+
}
227+
228+
// This implementation is very similar to [LoginActivity.AuthWebViewClient] but the
229+
// code cannot be shared due to the heavy reliance on the ViewModel.
230+
@VisibleForTesting
231+
internal inner class TokenMigrationClientManager(
232+
val resultCallback: MigrationCallbackRegistry.MigrationCallbacks,
233+
val instanceServer: String,
234+
) : WebViewClient() {
235+
236+
override fun shouldOverrideUrlLoading(
237+
view: WebView,
238+
request: WebResourceRequest,
239+
): Boolean {
240+
val url = request.url.toString().replace("///", "/").lowercase()
241+
val callbackUrl = viewModel.oAuthConfig.redirectUri.replace("///", "/").lowercase()
242+
val migrationFinished = url.startsWith(callbackUrl)
243+
244+
if (migrationFinished) {
245+
viewModel.authFinished.value = true
246+
viewModel.loading.value = true
247+
248+
val params = UriFragmentParser.parse(request.url)
249+
val error = params["error"]
250+
// Did we fail?
251+
when {
252+
error != null -> {
253+
logMigrationError(
254+
resultCallback = resultCallback,
255+
error = error,
256+
errorDesc = params["error_description"],
257+
e = null,
258+
)
259+
}
260+
261+
else -> {
262+
// Show loading while we PKCE and/or create user account.
263+
viewModel.authFinished.value = true
264+
viewModel.loading.value = true
236265

237-
else -> {
238-
// Show loading while we PKCE and/or create user account.
239-
viewModel.authFinished.value = true
240-
viewModel.loading.value = true
241-
242-
CoroutineScope(Default).launch {
243-
when {
244-
viewModel.useWebServerFlow ->
245-
viewModel.onWebServerFlowComplete(
246-
code = params["code"],
247-
onAuthFlowError = resultCallback.onMigrationError,
248-
onAuthFlowSuccess = resultCallback.onMigrationSuccess,
249-
loginServer = instanceServer,
250-
tokenMigration = true,
251-
).join()
252-
253-
else ->
254-
viewModel.onAuthFlowComplete(
255-
tr = TokenEndpointResponse(params),
256-
onAuthFlowError = resultCallback.onMigrationError,
257-
onAuthFlowSuccess = resultCallback.onMigrationSuccess,
258-
tokenMigration = true,
259-
)
260-
}
261-
262-
// Wait until we are completely finished so progress indicator is shown.
263-
finish()
266+
CoroutineScope(Default).launch {
267+
when {
268+
viewModel.useWebServerFlow ->
269+
viewModel.onWebServerFlowComplete(
270+
code = params["code"],
271+
onAuthFlowError = resultCallback.onMigrationError,
272+
onAuthFlowSuccess = resultCallback.onMigrationSuccess,
273+
loginServer = instanceServer,
274+
tokenMigration = true,
275+
).join()
276+
277+
else ->
278+
viewModel.onAuthFlowComplete(
279+
tr = TokenEndpointResponse(params),
280+
onAuthFlowError = resultCallback.onMigrationError,
281+
onAuthFlowSuccess = resultCallback.onMigrationSuccess,
282+
tokenMigration = true,
283+
)
264284
}
285+
286+
// Wait until we are completely finished so progress indicator is shown.
287+
finish()
265288
}
266289
}
267290
}
268-
269-
return migrationFinished
270291
}
271292

272-
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
273-
super.onPageStarted(view, url, favicon)
274-
viewModel.loading.value = true
275-
}
293+
return migrationFinished
294+
}
295+
296+
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
297+
super.onPageStarted(view, url, favicon)
298+
viewModel.loading.value = true
299+
}
276300

277-
override fun onPageFinished(view: WebView?, url: String?) {
278-
view?.evaluateJavascript(BACKGROUND_COLOR_JAVASCRIPT) { result ->
279-
makeStatusBarVisible()
280-
validateAndExtractBackgroundColor(result)?.let { color ->
281-
viewModel.dynamicBackgroundColor.value = color
301+
override fun onPageFinished(view: WebView?, url: String?) {
302+
view?.evaluateJavascript(BACKGROUND_COLOR_JAVASCRIPT) { result ->
303+
makeStatusBarVisible()
304+
validateAndExtractBackgroundColor(result)?.let { color ->
305+
viewModel.dynamicBackgroundColor.value = color
282306

283-
// This check is inside validateAndExtractBackgroundColor because we only
284-
// want to stop showing the spinner if WebView UI is actually displayed.
285-
if (!viewModel.authFinished.value) {
286-
viewModel.loading.value = false
287-
}
307+
// This check is inside validateAndExtractBackgroundColor because we only
308+
// want to stop showing the spinner if WebView UI is actually displayed.
309+
if (!viewModel.authFinished.value) {
310+
viewModel.loading.value = false
288311
}
289312
}
290-
291-
super.onPageFinished(view, url)
292313
}
293-
}
294314

295-
loadUrl(frontDoorUrl)
315+
super.onPageFinished(view, url)
316+
}
296317
}
297318

298319
private fun logMigrationError(
@@ -320,5 +341,8 @@ internal class TokenMigrationActivity : ComponentActivity() {
320341
const val EXTRA_CALLBACK_ID = "MIGRATION_CALLBACK"
321342

322343
const val TAG = "TokenMigrationActivity"
344+
345+
// Used for mocking the webview in tests.
346+
var webViewFactory = { context: Context -> WebView(context) }
323347
}
324348
}

libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ class LoginActivityTest {
165165
val observer = activity.BrowserCustomTabUrlObserver(activity)
166166

167167
observer.onChanged(exampleUrl)
168-
verify(exactly = -1) {
168+
verify {
169169
activity.startBrowserCustomTabAuthorization(
170170
match { it == exampleUrl },
171171
match { it == activityResultLauncher }

0 commit comments

Comments
 (0)