Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion kalium
Submodule kalium updated 24 files
+46 −6 core/common/src/appleMain/kotlin/com/wire/kalium/common/error/CoreCryptoExceptionMapper.kt
+60 −0 core/common/src/appleTest/kotlin/com/wire/kalium/common/error/CoreCryptoExceptionMapperAppleTest.kt
+40 −2 data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq
+46 −60 data/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Notification.sq
+18 −0 data/persistence/src/commonMain/db_user/migrations/133.sqm
+60 −0 data/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt
+94 −0 docs/adr/0008-androidx-benchmark-for-on-device-dao-benchmarks.md
+8 −3 gradle/libs.versions.toml
+13 −1 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PendingProposalScheduler.kt
+2 −2 logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt
+71 −44 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PendingProposalSchedulerTest.kt
+57 −0 logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt
+16 −4 ...c/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/provider/CryptoTransactionProviderArrangement.kt
+11 −5 logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ClientRepositoryArrangement.kt
+41 −31 ...rc/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt
+52 −32 logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt
+88 −0 test/android-benchmarks/build.gradle.kts
+28 −0 test/android-benchmarks/src/androidTest/AndroidManifest.xml
+52 −0 test/android-benchmarks/src/androidTest/kotlin/com/wire/kalium/benchmarks/android/BenchmarkDb.kt
+138 −0 test/android-benchmarks/src/androidTest/kotlin/com/wire/kalium/benchmarks/android/BenchmarkEntities.kt
+96 −0 test/android-benchmarks/src/androidTest/kotlin/com/wire/kalium/benchmarks/android/MessageInsertBenchmark.kt
+91 −0 test/android-benchmarks/src/androidTest/kotlin/com/wire/kalium/benchmarks/android/MessageSelectBenchmark.kt
+19 −0 test/android-benchmarks/src/main/AndroidManifest.xml
+2 −0 test/data-mocks/src/commonMain/kotlin/com/wire/kalium/logic/data/MockConversation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.tests.core.criticalFlows

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import backendUtils.BackendClient
import backendUtils.team.TeamHelper
import backendUtils.team.TeamRoles
import com.wire.android.tests.core.BaseUiTest
import com.wire.android.tests.core.pages.AllPages
import com.wire.android.tests.support.UiAutomatorSetup
import com.wire.android.tests.support.tags.Category
import com.wire.android.tests.support.tags.TestCaseId
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.inject
import service.TestServiceHelper
import user.usermanager.ClientUserManager
import user.utils.ClientUser

@RunWith(AndroidJUnit4::class)
class UpgradeVersion : BaseUiTest() {
private val pages: AllPages by inject()
private lateinit var device: UiDevice
private lateinit var context: Context
private lateinit var backendClient: BackendClient
private lateinit var teamHelper: TeamHelper
private lateinit var testServiceHelper: TestServiceHelper
private var member1: ClientUser? = null
private var member2: ClientUser? = null

@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().context
device = UiAutomatorSetup.start(UiAutomatorSetup.APP_INTERNAL)
backendClient = BackendClient.loadBackend("STAGING")
teamHelper = TeamHelper()
testServiceHelper = TestServiceHelper(teamHelper.usersManager)
}

@After
fun tearDown() {
// CI provides the new APK path so the shared device is restored even if this test fails before the upgrade step.
val recentWireApkPath = InstrumentationRegistry.getArguments().getString("newApkPath")
if (recentWireApkPath != null) {
UiAutomatorSetup.upgradeWireToRecentVersion(recentWireApkPath)
}
cleanupCreatedUsers(backendClient, teamHelper.usersManager)
}

/**
* Local runs should preinstall the old Wire APK, then push the new APK to /data/local/tmp/Wire.new.apk.
* Push: adb push /path/to/new.apk /data/local/tmp/Wire.new.apk
* Run: ./gradlew :tests:testsCore:connectedDebugAndroidTest \
* -Pandroid.testInstrumentationRunnerArguments.testCaseId=TC-8607 \
* -Pandroid.testInstrumentationRunnerArguments.newApkPath=/data/local/tmp/Wire.new.apk
* CI also passes oldApkPath so the test can install old Wire itself.
*/
@Suppress("CyclomaticComplexMethod", "LongMethod")
@TestCaseId("TC-8607")
@Category("criticalFlow", "upgrade")
@Test
fun givenTeamUserWithConversationHistory_whenUpdatingFromPreviousWireVersion_thenHistoryIsPreserved() {
step("Given I reinstall the old Wire Version") {
// CI passes oldApkPath when this test runs with other critical flows; local runs should preinstall the old APK.
val oldWireApkPath = InstrumentationRegistry.getArguments().getString("oldApkPath")
if (oldWireApkPath != null) {
UiAutomatorSetup.installOldWireVersion(oldWireApkPath)
}
}

step("There is a team owner with a team named UpgradeTeam") {
teamHelper.usersManager.createTeamOwnerByAlias(
"user1Name",
"UpgradeTeam",
"en_US",
true,
backendClient,
context
)
}

step("Team owner adds members to the team with role Member") {
teamHelper.userXAddsUsersToTeam(
"user1Name",
"user2Name,user3Name",
"UpgradeTeam",
TeamRoles.Member,
backendClient,
context,
true
)
}

step("Team owner has a group conversation with members in the team") {
testServiceHelper.userHasGroupConversationInTeam(
"user1Name",
"UpgradeVersion",
"user2Name,user3Name",
"UpgradeTeam"
)
}

step("Member 1 has a 1:1 conversation with Member 2 in the team") {
testServiceHelper.userHas1on1ConversationInTeam(
"user2Name",
"user3Name",
"UpgradeTeam"
)
}

step("Member 1 is me") {
member1 = teamHelper.usersManager.findUserBy("user2Name", ClientUserManager.FindBy.NAME_ALIAS)
member2 = teamHelper.usersManager.findUserBy("user3Name", ClientUserManager.FindBy.NAME_ALIAS)
}

step("And I see welcome screen before login") {
pages.registrationPage.apply {
assertEmailWelcomePage()
}
}

step("And I open staging deep link login flow") {
pages.loginPage.apply {
clickStagingDeepLink()
clickProceedButtonOnDeeplinkOverlay()
}
}

step("And I login as Member 1") {
pages.loginPage.apply {
enterTeamMemberLoggingEmail(member1?.email ?: "")
clickLoginButton()
enterTeamMemberLoggingPassword(member1?.password ?: "")
clickLoginButton()
}
}

step("And I complete post-login permission and privacy prompts") {
pages.registrationPage.apply {
waitUntilLoginFlowIsCompleted()
clickAllowNotificationButton()
clickDeclineShareDataAlert()
}
}

step("Then I see conversation list") {
pages.conversationListPage.apply {
assertConversationListVisible()
}
}

step("And I see conversation UpgradeVersion in conversation list") {
pages.conversationListPage.apply {
assertGroupConversationVisible("UpgradeVersion")
}
}

step("And I see conversation with Member 2 in conversation list") {
pages.conversationListPage.apply {
assertConversationVisible(member2?.name ?: "")
}
}

step("When I tap on conversation name UpgradeVersion in conversation list") {
pages.conversationListPage.apply {
clickGroupConversation("UpgradeVersion")
}
}

step("And Member 2 sends message to group conversation UpgradeVersion") {
testServiceHelper.apply {
addDevice("user3Name", null, "Device1")
userSendMessageToConversation(
"user3Name",
"Hello!",
"Device1",
"UpgradeVersion",
false
)
}
}

step("Then I see the message from Member 2 in current conversation") {
pages.conversationViewPage.apply {
assertReceivedMessageIsVisibleInCurrentConversation("Hello!")
}
}

step("When I type a reply into the text input field and send it") {
pages.conversationViewPage.apply {
typeMessageInInputField("Hello as well!")
clickSendButton()
}
}

step("Then I see my reply in current conversation") {
pages.conversationViewPage.apply {
assertSentMessageIsVisibleInCurrentConversation("Hello as well!")
}
}

step("When I tap the back arrow to go back to conversation list") {
pages.conversationViewPage.apply {
tapBackButtonToCloseConversationViewPage()
}
}

step("And Member 2 sends message to Member 1") {
testServiceHelper.userSendMessageToPersonalMlsConversation(
"user3Name",
"Hello friend",
"Device1",
"user2Name",
false
)
}

step("And I wait until the notification popup disappears") {
pages.notificationsPage.apply {
waitUntilNotificationPopUpGone()
}
}

step("Then I see conversation with Member 2 is having 1 unread messages in conversation list") {
pages.conversationListPage.apply {
assertConversationHasUnreadMessagesCount(member2?.name ?: "", "1")
}
}

step("When I minimise Wire") {
device.pressHome()
}

step("And I upgrade Wire to the recent version") {
val recentWireApkPath = InstrumentationRegistry.getArguments()
.getString("newApkPath") ?: "/data/local/tmp/Wire.new.apk"
UiAutomatorSetup.upgradeWireToRecentVersion(recentWireApkPath)
}

step("Then I see conversation with Member 2 is having 1 unread messages in conversation list") {
pages.conversationListPage.apply {
assertConversationHasUnreadMessagesCount(member2?.name ?: "", "1")
}
}

step("And I see conversation UpgradeVersion in conversation list") {
pages.conversationListPage.apply {
assertGroupConversationVisible("UpgradeVersion")
}
}

step("When I tap on conversation name UpgradeVersion in conversation list") {
pages.conversationListPage.apply {
clickGroupConversation("UpgradeVersion")
}
}

step("Then I see the reply message in current conversation") {
pages.conversationViewPage.apply {
assertSentMessageIsVisibleInCurrentConversation("Hello as well!")
}
}

step("And I see the message from Member 2 in current conversation") {
pages.conversationViewPage.apply {
assertReceivedMessageIsVisibleInCurrentConversation("Hello!")
}
}

step("When I type the final migration message into the text input field and send it") {
pages.conversationViewPage.apply {
typeMessageInInputField("Upgrade was a success!")
clickSendButton()
}
}

step("Then I see the final migration message in current conversation") {
pages.conversationViewPage.apply {
assertSentMessageIsVisibleInCurrentConversation("Upgrade was a success!")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,16 @@ data class ConversationListPage(private val device: UiDevice) {
return this
}

fun assertGroupConversationVisible(conversationName: String): ConversationListPage {
val conversation = UiWaitUtils.waitElement(UiSelectorParams(text = conversationName))
fun assertConversationVisible(conversationName: String): ConversationListPage {
val conversation = UiWaitUtils.waitElement(conversationNameSelector(conversationName))
assertTrue("Conversation '$conversationName' is not visible", !conversation.visibleBounds.isEmpty)
return this
}

fun assertGroupConversationVisible(conversationName: String): ConversationListPage {
return assertConversationVisible(conversationName)
}

fun clickConnectionRequestOfUser(userName: String): ConversationListPage {
val teamMemberName = UiWaitUtils.waitElement(displayedUserName(userName))
teamMemberName.click()
Expand Down Expand Up @@ -193,6 +197,15 @@ data class ConversationListPage(private val device: UiDevice) {
return this
}

fun assertConversationHasUnreadMessagesCount(
conversationName: String,
expectedCount: String
): ConversationListPage {
assertConversationVisible(conversationName)
assertUnreadMessagesCount(expectedCount)
return this
}

fun assertConversationNotVisible(conversationName: String): ConversationListPage {
val conversation = findElementOrNull(conversationNameSelector(conversationName))
Assert.assertTrue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,37 @@ object UiAutomatorSetup {
device.executeShellCommand("am force-stop $appPackage")
}

// Uses -d because shared CI runs may already have the newer APK installed before this test switches back to old Wire.
fun installOldWireVersion(apkPath: String) {
val device = getDevice()
val output = device.executeShellCommand("pm install -r -d -g $apkPath").trim()

if (!output.contains("Success")) {
val installOutput = output.ifBlank { "<empty>" }
throw IllegalStateException(
"Failed to install old Wire APK from '$apkPath'. Output: $installOutput"
)
}

startApp()
waitAppStart(device)
}

fun upgradeWireToRecentVersion(apkPath: String) {
val device = getDevice()
val output = device.executeShellCommand("pm install -r -d -g $apkPath").trim()

if (!output.contains("Success")) {
val installOutput = output.ifBlank { "<empty>" }
throw IllegalStateException(
"Failed to upgrade Wire using APK from '$apkPath'. Output: $installOutput"
)
}

startApp()
waitAppStart(device)
}

// Setup-level wrapper that pre-grants notifications on Android 13+ via PermissionUtils.
private fun grantNotificationPermissionIfSupported(appPackage: String) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Expand Down
Loading
Loading