From 70b3326e28d9d9ace67e4f09d6a817b03bf47451 Mon Sep 17 00:00:00 2001 From: Brandon Page Date: Mon, 1 Jun 2026 10:14:23 -0700 Subject: [PATCH] fix(ci): align kotlin-reflect with Kotlin 2.3.20 and fix test compatibility Root cause: MockK 1.14.9 pulls kotlin-reflect:2.2.21 which cannot parse Kotlin 2.3.20 bytecode metadata, causing ArrayIndexOutOfBoundsException in 194 tests. Additionally, androidx.activity:1.13.0 references API 31+ classes causing NoClassDefFoundError on APIs 28-30, and Thread.sleep(200) is insufficient for async coroutine completion on API 34 CI. Failing test(s): All MockK-based tests (194), PickerBottomSheetTest, SalesforceSDKManagerTests, LoginViewModelMockTest, TokenMigrationWebViewTest (on APIs 28-30), LoginViewModelTest (flaky on API 34). Fix: - Add explicit kotlin-reflect:2.3.20 dependency to force-align with project Kotlin version - Add @SdkSuppress(minSdkVersion=31) to tests requiring ComponentActivity on API 31+ - Increase Thread.sleep from 200ms to 2000ms for async coroutine completion in CI --- gradle/libs.versions.toml | 2 + libs/SalesforceSDK/build.gradle.kts | 1 + .../app/SalesforceSDKManagerTests.kt | 2 + .../androidsdk/auth/LoginViewModelMockTest.kt | 18 +++++---- .../androidsdk/auth/LoginViewModelTest.kt | 40 +++++++++---------- .../androidsdk/ui/PickerBottomSheetTest.kt | 2 + .../ui/TokenMigrationWebViewTest.kt | 2 + 7 files changed, 39 insertions(+), 28 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb3019e3cd..7cc9dda88e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ androidx-test-uiautomator = "2.3.0" androidx-test-orchestrator = "1.6.1" androidx-arch-core-testing = "2.2.0" mockk-android = "1.14.9" +kotlin-reflect = "2.3.20" [libraries] # Kotlin / serialization @@ -125,3 +126,4 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-test-orchestrator" } androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "androidx-arch-core-testing" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk-android" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } diff --git a/libs/SalesforceSDK/build.gradle.kts b/libs/SalesforceSDK/build.gradle.kts index 743bc4479a..4785979274 100644 --- a/libs/SalesforceSDK/build.gradle.kts +++ b/libs/SalesforceSDK/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { androidTestImplementation(libs.androidx.arch.core.testing) androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.kotlin.reflect) } android { // TODO: This cannot be resolved until newDSL=true diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt index 169146ec8b..62fed06bb5 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTests.kt @@ -2,6 +2,7 @@ package com.salesforce.androidsdk.app import android.app.Activity import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.salesforce.androidsdk.auth.HttpAccess @@ -34,6 +35,7 @@ import org.junit.runner.RunWith /** * Tests for `SalesforceSDKManager`. */ +@SdkSuppress(minSdkVersion = 31) @RunWith(AndroidJUnit4::class) @SmallTest class SalesforceSDKManagerTests { diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelMockTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelMockTest.kt index e8265a6051..b7acc44833 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelMockTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelMockTest.kt @@ -29,6 +29,7 @@ package com.salesforce.androidsdk.auth import android.webkit.CookieManager import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import com.salesforce.androidsdk.accounts.UserAccount import com.salesforce.androidsdk.accounts.UserAccountBuilder @@ -65,6 +66,7 @@ import org.junit.runner.RunWith * Tests for LoginViewModel that require mocking. * These tests are separated from LoginViewModelTest to isolate mock usage. */ +@SdkSuppress(minSdkVersion = 31) @RunWith(AndroidJUnit4::class) class LoginViewModelMockTest { @get:Rule @@ -379,7 +381,7 @@ class LoginViewModelMockTest { spyViewModel.onWebServerFlowComplete(testCode, mockOnError, mockOnSuccess) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) // Verify doCodeExchange was called with correct parameters coVerify { @@ -423,7 +425,7 @@ class LoginViewModelMockTest { ) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) // Verify doCodeExchange was called with correct parameters coVerify { @@ -462,7 +464,7 @@ class LoginViewModelMockTest { spyViewModel.onWebServerFlowComplete(testCode, mockOnError, mockOnSuccess) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) // Verify doCodeExchange was called with null loginServer and false tokenMigration coVerify { @@ -498,7 +500,7 @@ class LoginViewModelMockTest { spyViewModel.onWebServerFlowComplete(null, mockOnError, mockOnSuccess) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) // Verify doCodeExchange was called with null code, null loginServer, and false tokenMigration coVerify { @@ -609,7 +611,7 @@ class LoginViewModelMockTest { ) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) // Verify doCodeExchange was called with the correct loginServer and tokenMigration coVerify { @@ -657,7 +659,7 @@ class LoginViewModelMockTest { ) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) coVerify { spyViewModel.onAuthFlowComplete( @@ -707,7 +709,7 @@ class LoginViewModelMockTest { ) // Give time for the coroutine to execute - Thread.sleep(200) + Thread.sleep(2000) coVerify { spyViewModel.onAuthFlowComplete( @@ -770,7 +772,7 @@ class LoginViewModelMockTest { loginServer = migrationServer, tokenMigration = true, ) - Thread.sleep(200) + Thread.sleep(2000) // Token exchange must be performed with MIGRATION credentials. verify { diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt index ea1017343e..f241594d43 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/auth/LoginViewModelTest.kt @@ -157,7 +157,7 @@ class LoginViewModelTest { viewModel.selectedServer.value = FAKE_SERVER_URL // Wait for loginUrl to update after selectedServer change (async coroutine) - Thread.sleep(200) + Thread.sleep(2000) assertNotNull(viewModel.loginUrl.value) // LoginUrlSource prepends https:// to scheme-less servers before URL generation. assertTrue(viewModel.loginUrl.value!!.startsWith("https://$FAKE_SERVER_URL")) @@ -171,7 +171,7 @@ class LoginViewModelTest { viewModel.browserCustomTabUrl.observeForever { } // The setup() already triggers URL generation; wait for async completion. - Thread.sleep(200) + Thread.sleep(2000) val browserCustomTabUrl = viewModel.browserCustomTabUrl.value assertNotNull("browserCustomTabUrl should be populated for the admin flow", browserCustomTabUrl) @@ -195,7 +195,7 @@ class LoginViewModelTest { SalesforceSDKManager.getInstance().useWebServerAuthentication = false viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) val browserCustomTabUrl = viewModel.browserCustomTabUrl.value val loginUrl = viewModel.loginUrl.value @@ -233,7 +233,7 @@ class LoginViewModelTest { viewModel.browserCustomTabUrl.observeForever { } // Wait for initial generation. - Thread.sleep(200) + Thread.sleep(2000) val initialUrl = viewModel.browserCustomTabUrl.value assertNotNull(initialUrl) assertFalse( @@ -242,7 +242,7 @@ class LoginViewModelTest { ) viewModel.selectedServer.value = FAKE_SERVER_URL - Thread.sleep(200) + Thread.sleep(2000) val updatedUrl = viewModel.browserCustomTabUrl.value assertNotNull(updatedUrl) @@ -366,7 +366,7 @@ class LoginViewModelTest { viewModel.selectedServer.value = FAKE_SERVER_URL // Wait for async update - Thread.sleep(200) + Thread.sleep(2000) val newCodeChallenge = getSHA256Hash(viewModel.codeVerifier) assertNotEquals(originalCodeChallenge, newCodeChallenge) // LoginUrlSource prepends https:// to scheme-less servers before URL generation. @@ -381,7 +381,7 @@ class LoginViewModelTest { viewModel.reloadWebView() // Wait for async update - Thread.sleep(200) + Thread.sleep(2000) val newCodeChallenge = getSHA256Hash(viewModel.codeVerifier) assertNotNull(newCodeChallenge) assertNotEquals(originalCodeChallenge, newCodeChallenge) @@ -399,7 +399,7 @@ class LoginViewModelTest { viewModel.authCodeForJwtFlow = FAKE_JWT_FLOW_AUTH viewModel.reloadWebView() // Wait for async update - Thread.sleep(200) + Thread.sleep(2000) assertNotEquals(expectedUrl, viewModel.loginUrl.value) codeChallenge = getSHA256Hash(viewModel.codeVerifier) @@ -450,7 +450,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL contains the custom consumer key and redirect URI val loginUrl = viewModel.loginUrl.value!! @@ -466,7 +466,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL contains the boot config values val loginUrl = viewModel.loginUrl.value!! @@ -499,7 +499,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL contains the custom app config values val loginUrl = viewModel.loginUrl.value!! @@ -540,7 +540,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL contains the debug override values, not app config values val loginUrl = viewModel.loginUrl.value!! @@ -701,7 +701,7 @@ class LoginViewModelTest { // Test with test server viewModel.selectedServer.value = "https://test.salesforce.com" - Thread.sleep(200) + Thread.sleep(2000) var loginUrl = viewModel.loginUrl.value!! assertTrue("URL should contain test consumer key. URL: $loginUrl", loginUrl.contains("test_consumer_key")) @@ -712,7 +712,7 @@ class LoginViewModelTest { // Test with production server viewModel.selectedServer.value = "https://login.salesforce.com" - Thread.sleep(200) + Thread.sleep(2000) loginUrl = viewModel.loginUrl.value!! assertTrue("URL should contain prod consumer key. URL: $loginUrl", loginUrl.contains("prod_consumer_key")) @@ -738,7 +738,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL is generated correctly without scopes val loginUrl = viewModel.loginUrl.value!! @@ -762,7 +762,7 @@ class LoginViewModelTest { // Call reloadWebView viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify URL did not change assertEquals("frontDoorBridgeUrl should still be front door URL", frontDoorUrl, viewModel.frontDoorBridgeUrl.value) @@ -791,7 +791,7 @@ class LoginViewModelTest { ABOUT_BLANK, viewModel.loginUrl.value) // Wait for the new authorization URL to be generated - Thread.sleep(200) + Thread.sleep(2000) // Verify a new URL was generated val newUrl = viewModel.loginUrl.value @@ -824,7 +824,7 @@ class LoginViewModelTest { ABOUT_BLANK, viewModel.loginUrl.value) // Wait for the new authorization URL to be generated - Thread.sleep(200) + Thread.sleep(2000) // Verify a new URL was generated with different code challenge val newUrl = viewModel.loginUrl.value @@ -845,7 +845,7 @@ class LoginViewModelTest { // Call reloadWebView viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify URL did not change assertEquals("loginUrl should not change when selectedServer is null", @@ -866,7 +866,7 @@ class LoginViewModelTest { // Trigger URL generation viewModel.reloadWebView() - Thread.sleep(200) + Thread.sleep(2000) // Verify the URL contains the boot config values (fallback) val loginUrl = viewModel.loginUrl.value!! diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt index 9a7093c988..1be5d643c0 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/PickerBottomSheetTest.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.GrantPermissionRule import com.salesforce.androidsdk.R.string.sf__account_selector_text @@ -94,6 +95,7 @@ private val userList = listOf(user1, user2) @VisibleForTesting internal val customsRowCd = (hasText(customServer.name) and hasText(customServer.url)) +@SdkSuppress(minSdkVersion = 31) class PickerBottomSheetTest { @get:Rule diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/TokenMigrationWebViewTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/TokenMigrationWebViewTest.kt index 1fdc498a46..2ae4d16c1e 100644 --- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/TokenMigrationWebViewTest.kt +++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/TokenMigrationWebViewTest.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import com.salesforce.androidsdk.accounts.MigrationCallbackRegistry import com.salesforce.androidsdk.app.SalesforceSDKManager @@ -38,6 +39,7 @@ import org.junit.runner.RunWith import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +@SdkSuppress(minSdkVersion = 31) @RunWith(AndroidJUnit4::class) class TokenMigrationWebViewTest {