diff --git a/changelog.d/7207.sdk b/changelog.d/7207.sdk new file mode 100644 index 0000000000..0bc221e9f7 --- /dev/null +++ b/changelog.d/7207.sdk @@ -0,0 +1 @@ +Ports SDK instrumentation tests to use suspending functions instead of countdown latches diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5c800f720e..1ed3aff057 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -221,6 +221,8 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.androidx.coreTesting androidTestImplementation libs.jetbrains.coroutinesAndroid + androidTestImplementation libs.jetbrains.coroutinesTest + // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt index 260e8dbe05..403f697778 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt @@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) // Change password - commonTestHelper.runBlockingTest { - session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) - } + session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD) // Try to login with the previous password, it will fail val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 0b21f85742..515b687ee8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -44,22 +44,20 @@ class DeactivateAccountTest : InstrumentedTest { val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Deactivate the account - commonTestHelper.runBlockingTest { - session.accountService().deactivateAccount( - eraseAllData = false, - userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = session.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } + session.accountService().deactivateAccount( + eraseAllData = false, + userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) } - ) - } + } + ) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) @@ -74,23 +72,19 @@ class DeactivateAccountTest : InstrumentedTest { // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) val hs = commonTestHelper.createHomeServerConfig() - commonTestHelper.runBlockingTest { - commonTestHelper.matrix.authenticationService.getLoginFlow(hs) - } + commonTestHelper.matrix.authenticationService.getLoginFlow(hs) var accountCreationError: Throwable? = null - commonTestHelper.runBlockingTest { - try { - commonTestHelper.matrix.authenticationService - .getRegistrationWizard() - .createAccount( - session.myUserId.substringAfter("@").substringBefore(":"), - TestConstants.PASSWORD, - null - ) - } catch (failure: Throwable) { - accountCreationError = failure - } + try { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount( + session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null + ) + } catch (failure: Throwable) { + accountCreationError = failure } // Test the error diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index b179c6027e..84a113e667 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -19,18 +19,16 @@ package org.matrix.android.sdk.common import android.content.Context import android.net.Uri import android.util.Log -import androidx.lifecycle.Observer import androidx.test.internal.runner.junit4.statement.UiThreadStatement -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue @@ -51,12 +49,12 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.session.sync.SyncState import timber.log.Timber import java.util.UUID -import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * This class exposes methods to be used in common cases @@ -65,32 +63,42 @@ import java.util.concurrent.TimeUnit class CommonTestHelper internal constructor(context: Context) { companion object { - internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) { + + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) { val testHelper = CommonTestHelper(context) - return try { - block(testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } - internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) { + @OptIn(ExperimentalCoroutinesApi::class) + internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { val testHelper = CommonTestHelper(context) val cryptoTestHelper = CryptoTestHelper(testHelper) - return try { - block(cryptoTestHelper, testHelper) - } finally { - if (autoSignoutOnClose) { - testHelper.cleanUpOpenedSessions() + return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { + try { + withContext(Dispatchers.Default) { + block(cryptoTestHelper, testHelper) + } + } finally { + if (autoSignoutOnClose) { + testHelper.cleanUpOpenedSessions() + } } } } } internal val matrix: TestMatrix - private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) private var accountNumber = 0 private val trackedSessions = mutableListOf() @@ -112,19 +120,17 @@ class CommonTestHelper internal constructor(context: Context) { matrix = _matrix!! } - fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { + suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) } - fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { + suspend fun logIntoAccount(userId: String, testParams: SessionTestParams): Session { return logIntoAccount(userId, TestConstants.PASSWORD, testParams) } - fun cleanUpOpenedSessions() { + suspend fun cleanUpOpenedSessions() { trackedSessions.forEach { - runBlockingTest { - it.signOutService().signOut(true) - } + it.signOutService().signOut(true) } trackedSessions.clear() } @@ -138,27 +144,10 @@ class CommonTestHelper internal constructor(context: Context) { .build() } - /** - * This methods init the event stream and check for initial sync - * - * @param session the session to sync - */ - fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { - val lock = CountDownLatch(1) - coroutineScope.launch { - session.syncService().startSync(true) - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - lock.countDown() - syncLiveData.removeObserver(this) - } - } - } - syncLiveData.observeForever(syncObserver) - } - await(lock, timeout) + suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) { + session.syncService().startSync(true) + val syncLiveData = session.syncService().getSyncStateLive() + syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() } } /** @@ -166,22 +155,11 @@ class CommonTestHelper internal constructor(context: Context) { * * @param session the session to sync */ - fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { - waitWithLatch(timeout) { latch -> - session.clearCache() - val syncLiveData = session.syncService().getSyncStateLive() - val syncObserver = object : Observer { - override fun onChanged(t: SyncState?) { - if (session.syncService().hasAlreadySynced()) { - Timber.v("Clear cache and synced") - syncLiveData.removeObserver(this) - latch.countDown() - } - } - } - syncLiveData.observeForever(syncObserver) - session.syncService().startSync(true) - } + suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { + session.clearCache() + syncSession(session, timeout) + session.syncService().getSyncStateLive().first(timeout) { session.syncService().hasAlreadySynced() } + Timber.v("Clear cache and synced") } /** @@ -191,7 +169,7 @@ class CommonTestHelper internal constructor(context: Context) { * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ - fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { + suspend fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) timeline.start() val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) @@ -204,66 +182,72 @@ class CommonTestHelper internal constructor(context: Context) { /** * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List { + private suspend fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List { val sentEvents = ArrayList(count) (1 until count + 1) .map { "$message #$it" } .chunked(10) .forEach { batchedMessages -> - batchedMessages.forEach { formattedMessage -> - if (rootThreadEventId != null) { - room.relationService().replyInThread( - rootThreadEventId = rootThreadEventId, - replyInThreadText = formattedMessage - ) - } else { - room.sendService().sendTextMessage(formattedMessage) - } - } - waitWithLatch(timeout) { latch -> - val timelineListener = object : Timeline.Listener { + waitFor( + continueWhen = { + wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val timelineListener = object : Timeline.Listener { - override fun onTimelineUpdated(snapshot: List) { - val allSentMessages = snapshot - .filter { it.root.sendState == SendState.SYNCED } - .filter { it.root.getClearType() == EventType.MESSAGE } - .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + override fun onTimelineUpdated(snapshot: List) { + val allSentMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } - val hasSyncedAllBatchedMessages = allSentMessages - .map { - it.root.getClearContent().toModel()?.body + val hasSyncedAllBatchedMessages = allSentMessages + .map { + it.root.getClearContent().toModel()?.body + } + .containsAll(batchedMessages) + + if (allSentMessages.size == count) { + sentEvents.addAll(allSentMessages) + } + if (hasSyncedAllBatchedMessages) { + timeline.removeListener(this) + continuation.resume(Unit) + } + } } - .containsAll(batchedMessages) - - if (allSentMessages.size == count) { - sentEvents.addAll(allSentMessages) + timeline.addListener(timelineListener) + } } - if (hasSyncedAllBatchedMessages) { - timeline.removeListener(this) - latch.countDown() + }, + action = { + batchedMessages.forEach { formattedMessage -> + if (rootThreadEventId != null) { + room.relationService().replyInThread( + rootThreadEventId = rootThreadEventId, + replyInThreadText = formattedMessage + ) + } else { + room.sendService().sendTextMessage(formattedMessage) + } } } - } - timeline.addListener(timelineListener) - } + ) } return sentEvents } - fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { - waitWithLatch { latch -> - retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.getRoomSummary(roomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("# TEST", "${otherSession.myUserId} can see the invite") - } + suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("# TEST", "${otherSession.myUserId} can see the invite") } } } // not sure why it's taking so long :/ - runBlockingTest(90_000) { + wrapWithTimeout(90_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID") try { otherSession.roomService().joinRoom(roomID) @@ -273,11 +257,9 @@ class CommonTestHelper internal constructor(context: Context) { } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - waitWithLatch { - retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.getRoomSummary(roomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + retryPeriodically { + val roomSummary = otherSession.getRoomSummary(roomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } @@ -287,7 +269,7 @@ class CommonTestHelper internal constructor(context: Context) { * @param message the message to send * @param numberOfMessages the number of time the message will be sent */ - fun replyInThreadMessage( + suspend fun replyInThreadMessage( room: Room, message: String, numberOfMessages: Int, @@ -305,15 +287,7 @@ class CommonTestHelper internal constructor(context: Context) { // PRIVATE METHODS ***************************************************************************** - /** - * Creates a unique account - * - * @param userNamePrefix the user name prefix - * @param password the password - * @param testParams test params about the session - * @return the session associated with the newly created account - */ - private fun createAccount( + private suspend fun createAccount( userNamePrefix: String, password: String, testParams: SessionTestParams @@ -331,15 +305,7 @@ class CommonTestHelper internal constructor(context: Context) { } } - /** - * Logs into an existing account - * - * @param userId the userId to log in - * @param password the password to log in - * @param testParams test params about the session - * @return the session associated with the existing account - */ - fun logIntoAccount( + suspend fun logIntoAccount( userId: String, password: String, testParams: SessionTestParams @@ -351,32 +317,25 @@ class CommonTestHelper internal constructor(context: Context) { } } - /** - * Create an account and a dedicated session - * - * @param userName the account username - * @param password the password - * @param sessionTestParams parameters for the test - */ - private fun createAccountAndSync( + private suspend fun createAccountAndSync( userName: String, password: String, sessionTestParams: SessionTestParams ): Session { val hs = createHomeServerConfig() - runBlockingTest { + wrapWithTimeout(TestConstants.timeOutMillis) { matrix.authenticationService.getLoginFlow(hs) } - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(60_000L) { matrix.authenticationService .getRegistrationWizard() .createAccount(userName, password, null) } // Perform dummy step - val registrationResult = runBlockingTest(timeout = 60_000) { + val registrationResult = wrapWithTimeout(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .dummy() @@ -391,29 +350,14 @@ class CommonTestHelper internal constructor(context: Context) { return session } - /** - * Start an account login - * - * @param userName the account username - * @param password the password - * @param sessionTestParams session test params - */ - private fun logAccountAndSync( - userName: String, - password: String, - sessionTestParams: SessionTestParams - ): Session { + private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): Session { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) - val session = runBlockingTest { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } + val session = matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") session.open() if (sessionTestParams.withInitialSync) { syncSession(session) @@ -428,25 +372,21 @@ class CommonTestHelper internal constructor(context: Context) { * @param userName the account username * @param password the password */ - fun logAccountWithError( + suspend fun logAccountWithError( userName: String, password: String ): Throwable { val hs = createHomeServerConfig() - runBlockingTest { - matrix.authenticationService.getLoginFlow(hs) - } + matrix.authenticationService.getLoginFlow(hs) var requestFailure: Throwable? = null - runBlockingTest { - try { - matrix.authenticationService - .getLoginWizard() - .login(userName, password, "myDevice") - } catch (failure: Throwable) { - requestFailure = failure - } + try { + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice") + } catch (failure: Throwable) { + requestFailure = failure } assertNotNull(requestFailure) @@ -482,65 +422,48 @@ class CommonTestHelper internal constructor(context: Context) { ) } - suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { - while (true) { - try { - delay(1000) - } catch (ex: CancellationException) { - // the job was canceled, just stop - return - } - if (condition()) { - latch.countDown() - return + suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) { + wrapWithTimeout(timeout) { + while (!predicate()) { + runBlocking { delay(500) } } } } - fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { - val latch = CountDownLatch(1) - val job = coroutineScope.launch(dispatcher) { - block(latch) - } - await(latch, timeout, job) - } - - fun runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { - return runBlocking { - withTimeout(timeout) { - block() + suspend fun waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback { + override fun onSuccess(data: T) { + continuation.resume(data) + } + } + block(callback) } } } - // Transform a method with a MatrixCallback to a synchronous method - inline fun doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { - val lock = CountDownLatch(1) - var result: T? = null - - val callback = object : TestMatrixCallback(lock) { - override fun onSuccess(data: T) { - result = data - super.onSuccess(data) + suspend fun waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): Throwable { + return wrapWithTimeout(timeout) { + suspendCoroutine { continuation -> + val callback = object : MatrixCallback { + override fun onFailure(failure: Throwable) { + continuation.resume(failure) + } + } + block(callback) } } - - block.invoke(callback) - - await(lock, timeout) - - assertNotNull(result) - return result!! } /** * Clear all provided sessions */ - fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } + suspend fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } - fun signOutAndClose(session: Session) { + suspend fun signOutAndClose(session: Session) { trackedSessions.remove(session) - runBlockingTest(timeout = 60_000) { + wrapWithTimeout(timeout = 60_000L) { session.signOutService().signOut(true) } // no need signout will close diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 41d0d3a7e8..8cd5bee569 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -32,7 +32,7 @@ data class CryptoTestData( val thirdSession: Session? get() = sessions.getOrNull(2) - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { sessions.forEach { testHelper.signOutAndClose(it) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index f36bfb6210..6c6bc7056c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -16,19 +16,15 @@ package org.matrix.android.sdk.common -import android.os.SystemClock import android.util.Log -import androidx.lifecycle.Observer import org.amshove.kluent.fail import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -46,22 +42,16 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.KeyRef -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.toBase64NoPadding import java.util.UUID import kotlin.coroutines.Continuation @@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice session */ - fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = roomHistoryVisibility - name = "MyRoom" - }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = roomHistoryVisibility + name = "MyRoom" + }) if (encryptedRoom) { - testHelper.waitWithLatch { latch -> - val room = aliceSession.getRoom(roomId)!! - room.roomCryptoService().enableEncryption() - val roomSummaryLive = room.getRoomSummaryLive() - val roomSummaryObserver = object : Observer> { - override fun onChanged(roomSummary: Optional) { - if (roomSummary.getOrNull()?.isEncrypted.orFalse()) { - roomSummaryLive.removeObserver(this) - latch.countDown() - } - } - } - roomSummaryLive.observeForever(roomSummaryObserver) - } + val room = aliceSession.getRoom(roomId)!! + waitFor( + continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } }, + action = { room.roomCryptoService().enableEncryption() } + ) } return CryptoTestData(roomId, listOf(aliceSession)) } @@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * @return alice and bob sessions */ - fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData { val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility) val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (t?.isNotEmpty() == true) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - aliceRoom.membershipService().invite(bobSession.myUserId) - } + waitFor( + continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } }, + action = { aliceRoom.membershipService().invite(bobSession.myUserId) } + ) - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val roomJoinedObserver = object : Observer> { - override fun onChanged(t: List?) { - if (bobSession.getRoom(aliceRoomId) - ?.membershipService() - ?.getRoomMember(bobSession.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() + waitFor( + continueWhen = { + bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { + bobSession.getRoom(aliceRoomId) + ?.membershipService() + ?.getRoomMember(bobSession.myUserId) + ?.membership == Membership.JOIN } - } - } - bobRoomSummariesLive.observeForever(roomJoinedObserver) - bobSession.roomService().joinRoom(aliceRoomId) - } + }, + action = { bobSession.roomService().joinRoom(aliceRoomId) } + ) + // Ensure bob can send messages to the room // val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! // assertNotNull(roomFromBobPOV.powerLevels) @@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } - /** - * @return Alice, Bob and Sam session - */ - fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { - val cryptoTestData = doE2ETestWithAliceAndBobInARoom() - val aliceSession = cryptoTestData.firstSession - val aliceRoomId = cryptoTestData.roomId - - val room = aliceSession.getRoom(aliceRoomId)!! - - val samSession = createSamAccountAndInviteToTheRoom(room) - - // wait the initial sync - SystemClock.sleep(1000) - - return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) - } - - /** - * Create Sam account and invite him in the room. He will accept the invitation - * @Return Sam session - */ - fun createSamAccountAndInviteToTheRoom(room: Room): Session { - val samSession = testHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) - - testHelper.runBlockingTest { - room.membershipService().invite(samSession.myUserId, null) - } - - testHelper.runBlockingTest { - samSession.roomService().joinRoom(room.roomId, null, emptyList()) - } - - return samSession - } - /** * @return Alice and Bob sessions */ - fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { + suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { val cryptoTestData = doE2ETestWithAliceAndBobInARoom() val aliceSession = cryptoTestData.firstSession val aliceRoomId = cryptoTestData.roomId @@ -235,49 +165,20 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return cryptoTestData } - private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) - if (andCanDecrypt) { - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } else { - timeLineEvent != null - } + private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { + testHelper.retryPeriodically { + val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) + if (andCanDecrypt) { + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE + } else { + timeLineEvent != null } } } - fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { - assertEquals(EventType.ENCRYPTED, event.type) - assertNotNull(event.content) - - val eventWireContent = event.content.toContent() - assertNotNull(eventWireContent) - - assertNull(eventWireContent["body"]) - assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"]) - - assertNotNull(eventWireContent["ciphertext"]) - assertNotNull(eventWireContent["session_id"]) - assertNotNull(eventWireContent["sender_key"]) - - assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"]) - - assertNotNull(event.eventId) - assertEquals(roomId, event.roomId) - assertEquals(EventType.MESSAGE, event.getClearType()) - // TODO assertTrue(event.getAge() < 10000) - - val eventContent = event.toContent() - assertNotNull(eventContent) - assertEquals(clearMessage, eventContent["body"]) - assertEquals(senderSession.myUserId, event.senderId) - } - - fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { return MegolmBackupAuthData( publicKey = "abcdefg", signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) @@ -292,44 +193,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { ) } - fun createDM(alice: Session, bob: Session): String { - var roomId: String = "" - testHelper.waitWithLatch { latch -> - roomId = alice.roomService().createDirectRoom(bob.myUserId) - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (t?.any { it.roomId == roomId }.orFalse()) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - } - - testHelper.waitWithLatch { latch -> - val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) - val newRoomObserver = object : Observer> { - override fun onChanged(t: List?) { - if (bob.getRoom(roomId) - ?.membershipService() - ?.getRoomMember(bob.myUserId) - ?.membership == Membership.JOIN) { - bobRoomSummariesLive.removeObserver(this) - latch.countDown() - } - } - } - bobRoomSummariesLive.observeForever(newRoomObserver) - bob.roomService().joinRoom(roomId) - } + suspend fun createDM(alice: Session, bob: Session): String { + var roomId = "" + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { it.any { it.roomId == roomId }.orFalse() } + }, + action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) } + ) + waitFor( + continueWhen = { + bob.roomService() + .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) } + .first { + bob.getRoom(roomId) + ?.membershipService() + ?.getRoomMember(bob.myUserId) + ?.membership == Membership.JOIN + } + }, + action = { bob.roomService().joinRoom(roomId) } + ) return roomId } - fun initializeCrossSigning(session: Session) { - testHelper.doSync { + suspend fun initializeCrossSigning(session: Session) { + testHelper.waitForCallback { session.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -350,57 +242,55 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { /** * Initialize cross-signing, set up megolm backup and save all in 4S */ - fun bootstrapSecurity(session: Session) { + suspend fun bootstrapSecurity(session: Session) { initializeCrossSigning(session) val ssssService = session.sharedSecretStorageService() - testHelper.runBlockingTest { - val keyInfo = ssssService.generateKey( - UUID.randomUUID().toString(), - null, - "ssss_key", - EmptyKeySigner() - ) - ssssService.setDefaultKey(keyInfo.keyId) + val keyInfo = ssssService.generateKey( + UUID.randomUUID().toString(), + null, + "ssss_key", + EmptyKeySigner() + ) + ssssService.setDefaultKey(keyInfo.keyId) + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + // set up megolm backup + val creationInfo = testHelper.waitForCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = testHelper.waitForCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> ssssService.storeSecret( - MASTER_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + KEYBACKUP_SECRET_SSSS_NAME, + secret, listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) ) - - ssssService.storeSecret( - SELF_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) - - ssssService.storeSecret( - USER_SIGNING_KEY_SSSS_NAME, - session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) - - // set up megolm backup - val creationInfo = awaitCallback { - session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) - } - val version = awaitCallback { - session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) - - extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> - ssssService.storeSecret( - KEYBACKUP_SECRET_SSSS_NAME, - secret, - listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) - ) - } } } - fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { + suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) @@ -415,12 +305,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { roomId = roomId ).transactionId - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { - it.requestInfo?.fromDevice == alice.sessionParams.deviceId - } != null - } + testHelper.retryPeriodically { + bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } != null } val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { it.requestInfo?.fromDevice == alice.sessionParams.deviceId @@ -429,16 +317,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { var requestID: String? = null // wait for it to be readied - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) - .firstOrNull { it.localId == localId } - if (outgoingRequest?.isReady == true) { - requestID = outgoingRequest.transactionId!! - true - } else { - false - } + testHelper.retryPeriodically { + val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) + .firstOrNull { it.localId == localId } + if (outgoingRequest?.isReady == true) { + requestID = outgoingRequest.transactionId!! + true + } else { + false } } @@ -454,23 +340,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { var alicePovTx: OutgoingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction - Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction + Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") + alicePovTx?.state == VerificationTxState.ShortCodeReady } // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") - if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() - } - bobPovTx?.state == VerificationTxState.ShortCodeReady + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction + Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") + if (bobPovTx?.state == VerificationTxState.OnStarted) { + bobPovTx?.performAccept() } + bobPovTx?.state == VerificationTxState.ShortCodeReady } assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) @@ -478,38 +360,30 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { bobPovTx!!.userHasVerifiedShortCode() alicePovTx!!.userHasVerifiedShortCode() - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) } - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) - } + testHelper.retryPeriodically { + alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId) } } - fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { + suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) aliceSession.cryptoService().setWarnOnUnknownDevices(false) - val roomId = testHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" }) val room = aliceSession.getRoom(roomId)!! - testHelper.runBlockingTest { - room.roomCryptoService().enableEncryption() - } + room.roomCryptoService().enableEncryption() val sessions = mutableListOf(aliceSession) for (index in 1 until numberOfMembers) { val session = testHelper.createAccount("User_$index", defaultSessionParams) - testHelper.runBlockingTest(timeout = 600_000) { room.membershipService().invite(session.myUserId, null) } + room.membershipService().invite(session.myUserId, null) println("TEST -> " + session.myUserId + " invited") - testHelper.runBlockingTest { session.roomService().joinRoom(room.roomId, null, emptyList()) } + session.roomService().joinRoom(room.roomId, null, emptyList()) println("TEST -> " + session.myUserId + " joined") sessions.add(session) } @@ -517,47 +391,41 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) { return CryptoTestData(roomId, sessions) } - fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { + suspend fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "").let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } - } catch (error: MXCryptoError) { - // nop - } + testHelper.retryPeriodically { + val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) } - Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel()?.body + } catch (error: MXCryptoError) { + // nop } + Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel()?.body } } } - fun ensureCannotDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { + suspend fun ensureCannotDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { sentEventIds.forEach { sentEventId -> val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "") - fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - if (expectedError == null) { - assertNotNull(errorType) - } else { - assertEquals("Unexpected reason", expectedError, errorType) - } + try { + session.cryptoService().decryptEvent(event, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + if (expectedError == null) { + assertNotNull(errorType) + } else { + assertEquals("Unexpected reason", expectedError, errorType) } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt new file mode 100644 index 0000000000..8f89d42ac0 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.common + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.resume + +suspend fun T.onMain(block: T.() -> R): R { + return withContext(Dispatchers.Main) { + block(this@onMain) + } +} + +suspend fun LiveData.first(timeout: Long = TestConstants.timeOutMillis, predicate: (T) -> Boolean): T { + return wrapWithTimeout(timeout) { + withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val observer = object : Observer { + override fun onChanged(data: T) { + if (predicate(data)) { + removeObserver(this) + continuation.resume(data) + } + } + } + observeForever(observer) + continuation.invokeOnCancellation { removeObserver(observer) } + } + } + } +} + +suspend fun waitFor(continueWhen: suspend () -> T, action: suspend () -> Unit) { + coroutineScope { + val deferred = async { continueWhen() } + action() + deferred.await() + } +} + +suspend fun wrapWithTimeout(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { + val deferred = coroutineScope { + async { block() } + } + return withTimeout(timeout) { deferred.await() } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt index a48b45a1f5..4e1efbb700 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt @@ -46,30 +46,26 @@ class DecryptRedactedEventTest : InstrumentedTest { roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) // get the event from bob - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true } val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! - testHelper.runBlockingTest { - try { - val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") - Assert.assertEquals( - "Unexpected redacted reason", - redactionReason, - result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason") - ) - Assert.assertEquals( - "Unexpected Redacted event id", - timelineEvent.eventId, - result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts - ) - } catch (failure: Throwable) { - Assert.fail("Should not throw when decrypting a redacted event") - } + try { + val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") + Assert.assertEquals( + "Unexpected redacted reason", + redactionReason, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.content?.get("reason") + ) + Assert.assertEquals( + "Unexpected Redacted event id", + timelineEvent.eventId, + result.clearEvent.toModel()?.unsignedData?.redactedEvent?.redacts + ) + } catch (failure: Throwable) { + Assert.fail("Should not throw when decrypting a redacted event") } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt index 32d63a1934..cbbc4dc74e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt @@ -40,7 +40,6 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestData import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -57,18 +56,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -81,9 +76,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) // Let alice invite bob - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(bobSession.myUserId) - } + roomAlice.membershipService().invite(bobSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) @@ -114,9 +107,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - roomAlice.membershipService().invite(samSession.myUserId) - } + roomAlice.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) @@ -135,7 +126,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(false) @@ -162,7 +153,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { } @Test - fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> + fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val aliceSession = testData.firstSession.also { it.cryptoService().enableShareKeyOnInvite(true) @@ -186,7 +177,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) } - private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple, List, Session> { + private suspend fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple, List, Session> { val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1) val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1) @@ -195,9 +186,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let bob invite sam - commonTestHelper.runBlockingTest { - bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) - } + bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId) return Triple(fromAliceNotSharable, fromBobSharable, samSession) @@ -209,18 +198,14 @@ class E2EShareKeysConfigTest : InstrumentedTest { fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) aliceSession.cryptoService().enableShareKeyOnInvite(false) - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom(CreateRoomParams().apply { - historyVisibility = RoomHistoryVisibility.SHARED - name = "MyRoom" - enableEncryption() - }) - } + val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { + historyVisibility = RoomHistoryVisibility.SHARED + name = "MyRoom" + enableEncryption() + }) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true - } + commonTestHelper.retryPeriodically { + aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true } val roomAlice = aliceSession.roomService().getRoom(roomId)!! @@ -232,18 +217,15 @@ class E2EShareKeysConfigTest : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val keysBackupService = aliceSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = commonTestHelper.doSync { + val megolmBackupCreationInfo = commonTestHelper.waitForCallback { keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = commonTestHelper.doSync { + val version = commonTestHelper.waitForCallback { keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } - commonTestHelper.waitWithLatch { latch -> - keysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) + commonTestHelper.waitForCallback { + keysBackupService.backupAllGroupSessions(null, it) } // signout @@ -253,11 +235,11 @@ class E2EShareKeysConfigTest : InstrumentedTest { newAliceSession.cryptoService().enableShareKeyOnInvite(true) newAliceSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = commonTestHelper.doSync { + val keyVersionResult = commonTestHelper.waitForCallback { kbs.getVersion(version.version, it) } - val importedResult = commonTestHelper.doSync { + val importedResult = commonTestHelper.waitForCallback { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -276,9 +258,7 @@ class E2EShareKeysConfigTest : InstrumentedTest { val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) // Let alice invite sam - commonTestHelper.runBlockingTest { - newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) - } + newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index f883295495..6ba565777f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -18,12 +18,16 @@ package org.matrix.android.sdk.internal.crypto import android.util.Log import androidx.test.filters.LargeTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.fail import org.amshove.kluent.internal.assertEquals import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -57,12 +61,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest -import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.SessionTestParams -import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.mustFail -import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.") @RunWith(JUnit4::class) @@ -70,8 +71,6 @@ import java.util.concurrent.CountDownLatch @LargeTest class E2eeSanityTests : InstrumentedTest { - @get:Rule val rule = RetryTestRule(3) - /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -104,10 +103,8 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "All accounts created") // we want to invite them in the room otherAccounts.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } // All user should accept invite @@ -129,13 +126,11 @@ class E2eeSanityTests : InstrumentedTest { // All should be able to decrypt otherAccounts.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -146,10 +141,8 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${it.myUserId}") - aliceRoomPOV.membershipService().invite(it.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${it.myUserId}") + aliceRoomPOV.membershipService().invite(it.myUserId) } newAccount.forEach { @@ -159,21 +152,17 @@ class E2eeSanityTests : InstrumentedTest { ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) // wait a bit - testHelper.runBlockingTest { - delay(3_000) - } + delay(3_000) // check that messages are encrypted (uisi) newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { - Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED && - timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { + Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.ENCRYPTED && + timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID } } @@ -185,15 +174,13 @@ class E2eeSanityTests : InstrumentedTest { // new members should be able to decrypt it newAccount.forEach { otherSession -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { - Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.MESSAGE && - secondMessage == timelineEvent.root.getClearContent().toModel()?.body + testHelper.retryPeriodically { + val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { + Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}") } + timelineEvent != null && + timelineEvent.root.getClearType() == EventType.MESSAGE && + secondMessage == timelineEvent.root.getClearContent().toModel()?.body } } } @@ -229,10 +216,10 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Create and start key backup for bob ...") val bobKeysBackupService = bobSession.cryptoService().keysBackupService() val keyBackupPassword = "FooBarBaz" - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) } - val version = testHelper.doSync { + val version = testHelper.waitForCallback { bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) } Log.v("#E2E TEST", "... Key backup started and enabled for bob") @@ -248,32 +235,21 @@ class E2eeSanityTests : InstrumentedTest { sentEventIds.add(it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } // we want more so let's discard the session aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - - testHelper.runBlockingTest { - delay(1_000) - } } Log.v("#E2E TEST", "Bob received all and can decrypt") // Let's wait a bit to be sure that bob has backed up the session Log.v("#E2E TEST", "Force key backup for Bob...") - testHelper.waitWithLatch { latch -> - bobKeysBackupService.backupAllGroupSessions( - null, - TestMatrixCallback(latch, true) - ) - } + testHelper.waitForCallback { bobKeysBackupService.backupAllGroupSessions(null, it) } Log.v("#E2E TEST", "... Key backup done for Bob") // Now lets logout both alice and bob to ensure that we won't have any gossiping @@ -284,9 +260,7 @@ class E2eeSanityTests : InstrumentedTest { testHelper.signOutAndClose(bobSession) Log.v("#E2E TEST", "..Logout alice and bob...") - testHelper.runBlockingTest { - delay(1_000) - } + delay(1_000) // Create a new session for bob Log.v("#E2E TEST", "Create a new session for Bob") @@ -295,14 +269,11 @@ class E2eeSanityTests : InstrumentedTest { // check that bob can't currently decrypt Log.v("#E2E TEST", "check that bob can't currently decrypt") sentEventIds.forEach { sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { - Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") - } - timelineEvent != null && - timelineEvent.root.getClearType() == EventType.ENCRYPTED + testHelper.retryPeriodically { + val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { + Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}") } + timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED } } // after initial sync events are not decrypted, so we have to try manually @@ -311,11 +282,11 @@ class E2eeSanityTests : InstrumentedTest { // Let's now import keys from backup newBobSession.cryptoService().keysBackupService().let { kbs -> - val keyVersionResult = testHelper.doSync { + val keyVersionResult = testHelper.waitForCallback { kbs.getVersion(version.version, it) } - val importedResult = testHelper.doSync { + val importedResult = testHelper.waitForCallback { kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, @@ -357,13 +328,11 @@ class E2eeSanityTests : InstrumentedTest { sentEventIds.add(it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -399,22 +368,20 @@ class E2eeSanityTests : InstrumentedTest { val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! .getTimelineEvent(sentEventId)!! .root.content.toModel()!!.sessionId - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() - .first { - it.sessionId == megolmSessionId && - it.roomId == e2eRoomID - } - .results.also { - Log.w("##TEST", "result list is $it") - } - .firstOrNull { it.userId == aliceSession.myUserId } - ?.result - aliceReply != null && - aliceReply is RequestResult.Failure && - WithHeldCode.UNAUTHORISED == aliceReply.code - } + testHelper.retryPeriodically { + val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() + .first { + it.sessionId == megolmSessionId && + it.roomId == e2eRoomID + } + .results.also { + Log.w("##TEST", "result list is $it") + } + .firstOrNull { it.userId == aliceSession.myUserId } + ?.result + aliceReply != null && + aliceReply is RequestResult.Failure && + WithHeldCode.UNAUTHORISED == aliceReply.code } } @@ -455,13 +422,11 @@ class E2eeSanityTests : InstrumentedTest { firstMessage.let { text -> firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -483,13 +448,11 @@ class E2eeSanityTests : InstrumentedTest { secondMessage.let { text -> secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } @@ -503,18 +466,14 @@ class E2eeSanityTests : InstrumentedTest { Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) // Confirm we can decrypt one but not the other - testHelper.runBlockingTest { - mustFail(message = "Should not be able to decrypt event") { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } + mustFail(message = "Should not be able to decrypt event") { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") } - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt event") - } + try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } catch (error: MXCryptoError) { + fail("Should be able to decrypt event") } // Now let's verify bobs session, and re-request keys @@ -533,50 +492,42 @@ class E2eeSanityTests : InstrumentedTest { // old session should have shared the key at earliest known index now // we should be able to decrypt both - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val canDecryptFirst = try { - testHelper.runBlockingTest { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } - true - } catch (error: MXCryptoError) { - false - } - val canDecryptSecond = try { - testHelper.runBlockingTest { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } - true - } catch (error: MXCryptoError) { - false - } - canDecryptFirst && canDecryptSecond + testHelper.retryPeriodically { + val canDecryptFirst = try { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false } + val canDecryptSecond = try { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + true + } catch (error: MXCryptoError) { + false + } + canDecryptFirst && canDecryptSecond } } - private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { - aliceRoomPOV.sendService().sendTextMessage(text) + private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { var sentEventId: String? = null - testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> - val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) - timeline.start() - testHelper.retryPeriodicallyWithLatch(latch) { - val decryptedMsg = timeline.getSnapshot() - .filter { it.root.getClearType() == EventType.MESSAGE } - .also { list -> - val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } - Log.v("#E2E TEST", "Timeline snapshot is $message") - } - .filter { it.root.sendState == SendState.SYNCED } - .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } - sentEventId = decryptedMsg?.eventId - decryptedMsg != null - } + aliceRoomPOV.sendService().sendTextMessage(text) - timeline.dispose() + val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) + timeline.start() + testHelper.retryPeriodically { + val decryptedMsg = timeline.getSnapshot() + .filter { it.root.getClearType() == EventType.MESSAGE } + .also { list -> + val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" } + Log.v("#E2E TEST", "Timeline snapshot is $message") + } + .filter { it.root.sendState == SendState.SYNCED } + .firstOrNull { it.root.getClearContent().toModel()?.body?.startsWith(text) == true } + sentEventId = decryptedMsg?.eventId + decryptedMsg != null } + timeline.dispose() return sentEventId } @@ -593,106 +544,35 @@ class E2eeSanityTests : InstrumentedTest { val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) - val oldCompleteLatch = CountDownLatch(1) - lateinit var oldCode: String - aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { - - override fun verificationRequestUpdated(pr: PendingVerificationRequest) { - val readyInfo = pr.readyInfo - if (readyInfo != null) { - aliceSession.cryptoService().verificationService().beginKeyVerification( - VerificationMethod.SAS, - aliceSession.myUserId, - readyInfo.fromDevice, - readyInfo.transactionId - - ) - } - } - - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "exitsingPov: $tx") - val sasTx = tx as OutgoingSasVerificationTransaction - when (sasTx.uxState) { - OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { - // for the test we just accept? - oldCode = sasTx.getDecimalCodeRepresentation() - sasTx.userHasVerifiedShortCode() - } - OutgoingSasVerificationTransaction.UxState.VERIFIED -> { - // we can release this latch? - oldCompleteLatch.countDown() - } - else -> Unit - } - } - }) - - val newCompleteLatch = CountDownLatch(1) - lateinit var newCode: String - aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { - - override fun verificationRequestCreated(pr: PendingVerificationRequest) { - // let's ready - aliceNewSession.cryptoService().verificationService().readyPendingVerification( - listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), - aliceSession.myUserId, - pr.transactionId!! - ) - } - - var matchOnce = true - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("##TEST", "newPov: $tx") - - val sasTx = tx as IncomingSasVerificationTransaction - when (sasTx.uxState) { - IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { - // no need to accept as there was a request first it will auto accept - } - IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { - if (matchOnce) { - sasTx.userHasVerifiedShortCode() - newCode = sasTx.getDecimalCodeRepresentation() - matchOnce = false - } - } - IncomingSasVerificationTransaction.UxState.VERIFIED -> { - newCompleteLatch.countDown() - } - else -> Unit - } - } - }) - + val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId) + val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId) // initiate self verification aliceSession.cryptoService().verificationService().requestKeyVerification( listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), aliceNewSession.myUserId, listOf(aliceNewSession.sessionParams.deviceId!!) ) - testHelper.await(oldCompleteLatch) - testHelper.await(newCompleteLatch) + + val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode) + assertEquals("Decimal code should have matched", oldCode, newCode) // Assert that devices are verified - val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) - val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) + val newDeviceFromOldPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = + aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) // wait for secret gossiping to happen - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() - } + testHelper.retryPeriodically { + aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null - } + testHelper.retryPeriodically { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null } assertEquals( @@ -725,27 +605,112 @@ class E2eeSanityTests : InstrumentedTest { ) } - private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN + private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { + return scope.async { + suspendCancellableCoroutine { continuation -> + var oldCode: String? = null + val listener = object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + val readyInfo = pr.readyInfo + if (readyInfo != null) { + beginKeyVerification( + VerificationMethod.SAS, + userId, + readyInfo.fromDevice, + readyInfo.transactionId + + ) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "exitsingPov: $tx") + val sasTx = tx as OutgoingSasVerificationTransaction + when (sasTx.uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + // for the test we just accept? + oldCode = sasTx.getDecimalCodeRepresentation() + sasTx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + removeListener(this) + // we can release this latch? + continuation.resume(oldCode!!) + } + else -> Unit + } + } } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } } - private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { - testHelper.waitWithLatch { latch -> - sentEventIds.forEach { sentEventId -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timeLineEvent != null && - timeLineEvent.isEncrypted() && - timeLineEvent.root.getClearType() == EventType.MESSAGE + private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred { + return scope.async { + suspendCancellableCoroutine { continuation -> + var newCode: String? = null + + val listener = object : VerificationService.Listener { + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // let's ready + readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + userId, + pr.transactionId!! + ) + } + + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "newPov: $tx") + + val sasTx = tx as IncomingSasVerificationTransaction + when (sasTx.uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + // no need to accept as there was a request first it will auto accept + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + sasTx.userHasVerifiedShortCode() + newCode = sasTx.getDecimalCodeRepresentation() + matchOnce = false + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + removeListener(this) + continuation.resume(newCode!!) + } + else -> Unit + } + } } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } + } + } + } + + private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN + } + } + } + + private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { + sentEventIds.forEach { sentEventId -> + testHelper.retryPeriodically { + val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt index 32a95008b1..c078bcf03b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt @@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten import org.matrix.android.sdk.api.session.room.model.shouldShareHistory import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest -import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.wrapWithTimeout @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -101,15 +101,13 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") // Bob should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") - } + testHelper.retryPeriodically { + val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } } } @@ -121,10 +119,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST", "Aris user created") // Alice invites new user to the room - testHelper.runBlockingTest { - Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") - aliceRoomPOV.membershipService().invite(arisSession.myUserId) - } + Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") + aliceRoomPOV.membershipService().invite(arisSession.myUserId) waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) @@ -137,30 +133,26 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { null -> { // Aris should be able to decrypt the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE - ).also { - if (it) { - Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") - } + testHelper.retryPeriodically { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE + ).also { + if (it) { + Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") } - } + } } } RoomHistoryVisibility.INVITED, RoomHistoryVisibility.JOINED -> { // Aris should not even be able to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) - ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) - timelineEvent == null - } + testHelper.retryPeriodically { + val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + timelineEvent == null } } } @@ -238,10 +230,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { private fun testRotationDueToVisibilityChange( initRoomHistoryVisibility: RoomHistoryVisibility, nextRoomHistoryVisibility: RoomHistoryVisibilityContent - ) { - val testHelper = CommonTestHelper(context()) - val cryptoTestHelper = CryptoTestHelper(testHelper) - + ) = runCryptoTest(context()) { cryptoTestHelper, testHelper -> val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) val e2eRoomID = cryptoTestData.roomId @@ -267,21 +256,19 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { // Bob should be able to decrypt the message var firstAliceMessageMegolmSessionId: String? = null val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(aliceMessageId!!) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String - Log.v( - "#E2E TEST", - "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" - ) - } + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(aliceMessageId!!) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) } } } @@ -290,21 +277,19 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { var secondAliceMessageSessionId: String? = null sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(secondMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - Log.v( - "#E2E TEST", - "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" - ) - } + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(secondMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String + Log.v( + "#E2E TEST", + "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" + ) } } } @@ -313,50 +298,42 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { Log.v("#E2E TEST ROTATION", "No rotation needed yet") // Let's change the room history visibility - testHelper.runBlockingTest { - aliceRoomPOV.stateService() - .sendStateEvent( - eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, - stateKey = "", - body = RoomHistoryVisibilityContent( - historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr - ).toContent() - ) - } + aliceRoomPOV.stateService() + .sendStateEvent( + eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, + stateKey = "", + body = RoomHistoryVisibilityContent( + historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr + ).toContent() + ) // ensure that the state did synced down - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content - ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content + ?.toModel()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomVisibility = aliceSession.getRoom(e2eRoomID)!! - .stateService() - .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") - roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility - } + testHelper.retryPeriodically { + val roomVisibility = aliceSession.getRoom(e2eRoomID)!! + .stateService() + .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") + roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility } var aliceThirdMessageSessionId: String? = null sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobRoomPov - ?.timelineService() - ?.getTimelineEvent(thirdMessage) - (timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE).also { - if (it) { - aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String - } + testHelper.retryPeriodically { + val timelineEvent = bobRoomPov + ?.timelineService() + ?.getTimelineEvent(thirdMessage) + (timelineEvent != null && + timelineEvent.isEncrypted() && + timelineEvent.root.getClearType() == EventType.MESSAGE).also { + if (it) { + aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String } } } @@ -376,35 +353,31 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { + private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId } - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - otherAccounts.map { - aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership - }.all { - it == Membership.JOIN - } + private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + otherAccounts.map { + aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership + }.all { + it == Membership.JOIN } } } - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - (roomSummary != null && roomSummary.membership == Membership.INVITE).also { - if (it) { - Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") - } + private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + (roomSummary != null && roomSummary.membership == Membership.INVITE).also { + if (it) { + Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice") } } } - testHelper.runBlockingTest(60_000) { + wrapWithTimeout(60_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") try { otherSession.roomService().joinRoom(e2eRoomID) @@ -414,11 +387,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest { } Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) - roomSummary != null && roomSummary.membership == Membership.JOIN - } + testHelper.retryPeriodically { + val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) + roomSummary != null && roomSummary.membership == Membership.JOIN } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index e8e7b1d708..5c817443ce 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -52,15 +52,13 @@ class PreShareKeysTest : InstrumentedTest { Log.d("#Test", "Room Key Received from alice $preShareCount") // Force presharing of new outbound key - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) } - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() - newKeysCount > preShareCount - } + testHelper.retryPeriodically { + val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() + newKeysCount > preShareCount } val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting @@ -85,10 +83,8 @@ class PreShareKeysTest : InstrumentedTest { val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel()?.sessionId) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 5fe7376184..f3b3ccdd23 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.amshove.kluent.shouldBe +import kotlinx.coroutines.suspendCancellableCoroutine +import org.amshove.kluent.shouldBeEqualTo import org.junit.Assert import org.junit.Before import org.junit.FixMethodOrder @@ -45,7 +46,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.olm.OlmSession import timber.log.Timber -import java.util.concurrent.CountDownLatch import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -98,69 +98,37 @@ class UnwedgingTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) bobTimeline.start() - val bobFinalLatch = CountDownLatch(1) - val bobHasThreeDecryptedEventsListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } - Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages") - if (decryptedEventReceivedByBob.size == 3) { - if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { - bobFinalLatch.countDown() - } - } - } - } - bobTimeline.addListener(bobHasThreeDecryptedEventsListener) - - var latch = CountDownLatch(1) - var bobEventsListener = createEventListener(latch, 1) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() // - Alice sends a 1st message with a 1st megolm session roomFromAlicePOV.sendService().sendTextMessage("First message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1) - messagesReceivedByBob.size shouldBe 1 + messagesReceivedByBob.size shouldBeEqualTo 1 val firstMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! // - Store the olm session between A&B devices // Let us pickle our session with bob here so we can later unpickle it // and wedge our session. val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) - sessionIdsForBob!!.size shouldBe 1 + sessionIdsForBob!!.size shouldBeEqualTo 1 val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!! val oldSession = serializeForRealm(olmSession.olmSession) aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) - Thread.sleep(6_000) - latch = CountDownLatch(1) - bobEventsListener = createEventListener(latch, 2) - bobTimeline.addListener(bobEventsListener) messagesReceivedByBob = emptyList() - Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") // - Alice sends a 2nd message with a 2nd megolm session roomFromAlicePOV.sendService().sendTextMessage("Second message") // Wait for the message to be received by Bob - testHelper.await(latch) - bobTimeline.removeListener(bobEventsListener) + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2) - messagesReceivedByBob.size shouldBe 2 + messagesReceivedByBob.size shouldBeEqualTo 2 // Session should have changed val secondMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! Assert.assertNotEquals(firstMessageSession, secondMessageSession) @@ -173,25 +141,18 @@ class UnwedgingTest : InstrumentedTest { bobSession.cryptoService().getMyDevice().identityKey()!! ) olmDevice.clearOlmSessionCache() - Thread.sleep(6_000) // Force new session, and key share aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) + Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") + // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session + roomFromAlicePOV.sendService().sendTextMessage("Third message") + // Bob should not be able to decrypt, because the session key could not be sent // Wait for the message to be received by Bob - testHelper.waitWithLatch { - bobEventsListener = createEventListener(it, 3) - bobTimeline.addListener(bobEventsListener) - messagesReceivedByBob = emptyList() + messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3) - Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") - // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session - roomFromAlicePOV.sendService().sendTextMessage("Third message") - // Bob should not be able to decrypt, because the session key could not be sent - } - bobTimeline.removeListener(bobEventsListener) - - messagesReceivedByBob.size shouldBe 3 + messagesReceivedByBob.size shouldBeEqualTo 3 val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel()!!.sessionId!! Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") @@ -201,11 +162,11 @@ class UnwedgingTest : InstrumentedTest { Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType()) Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType()) // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged - testHelper.await(bobFinalLatch) - bobTimeline.removeListener(bobHasThreeDecryptedEventsListener) + + Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // It's a trick to force key request on fail to decrypt - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -223,24 +184,22 @@ class UnwedgingTest : InstrumentedTest { } // Wait until we received back the key - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - // we should get back the key and be able to decrypt - val result = testHelper.runBlockingTest { - tryOrNull { - bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") - } - } - Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") - result != null + testHelper.retryPeriodically { + // we should get back the key and be able to decrypt + val result = tryOrNull { + bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") } + Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") + result != null } bobTimeline.dispose() } +} - private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { - return object : Timeline.Listener { +private suspend fun Timeline.waitForMessages(expectedCount: Int): List { + return suspendCancellableCoroutine { continuation -> + val listener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { // noop } @@ -250,12 +209,16 @@ class UnwedgingTest : InstrumentedTest { } override fun onTimelineUpdated(snapshot: List) { - messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } + val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED } - if (messagesReceivedByBob.size == expectedNumberOfMessages) { - latch.countDown() + if (messagesReceived.size == expectedCount) { + removeListener(this) + continuation.resume(messagesReceived) } } } + + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index ef3fdfeeda..2bb04a1faa 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -55,7 +55,7 @@ class XSigningTest : InstrumentedTest { fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService() .initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { @@ -101,14 +101,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(bobAuthParams) @@ -117,7 +117,7 @@ class XSigningTest : InstrumentedTest { } // Check that alice can see bob keys - testHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } + testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) @@ -154,14 +154,14 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - testHelper.doSync { + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(aliceAuthParams) } }, it) } - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume(bobAuthParams) @@ -171,12 +171,12 @@ class XSigningTest : InstrumentedTest { // Check that alice can see bob keys val bobUserId = bobSession.myUserId - testHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } + testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) - testHelper.doSync { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } + testHelper.waitForCallback { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } // Now bobs logs in on a new device and verifies it // We will want to test that in alice POV, this new device would be trusted by cross signing @@ -185,7 +185,7 @@ class XSigningTest : InstrumentedTest { val bobSecondDeviceId = bobSession2.sessionParams.deviceId!! // Check that bob first session sees the new login - val data = testHelper.doSync> { + val data = testHelper.waitForCallback> { bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } @@ -197,12 +197,12 @@ class XSigningTest : InstrumentedTest { assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice) // Manually mark it as trusted from first session - testHelper.doSync { + testHelper.waitForCallback { bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) } // Now alice should cross trust bob's second device - val data2 = testHelper.doSync> { + val data2 = testHelper.waitForCallback> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt index 5f26fda946..42a04dbe3f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto.encryption import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder import org.junit.Test @@ -34,54 +34,59 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CryptoTestHelper -import java.util.concurrent.CountDownLatch +import org.matrix.android.sdk.common.waitFor +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class EncryptionTest : InstrumentedTest { @Test - fun test_EncryptionEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room -> - // Send an encryption Event as an Event (and not as a state event) - room.sendService().sendEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } + fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room -> + // Send an encryption Event as an Event (and not as a state event) + room.sendService().sendEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } @Test - fun test_EncryptionStateEvent() { - runCryptoTest(context()) { cryptoTestHelper, testHelper -> - performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room -> - runBlocking { - // Send an encryption Event as a State Event - room.stateService().sendStateEvent( - eventType = EventType.STATE_ROOM_ENCRYPTION, - stateKey = "", - body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() - ) - } - } + fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ -> + performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room -> + // Send an encryption Event as a State Event + room.stateService().sendStateEvent( + eventType = EventType.STATE_ROOM_ENCRYPTION, + stateKey = "", + body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() + ) } } - private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) { + private suspend fun performTest(cryptoTestHelper: CryptoTestHelper, roomShouldBeEncrypted: Boolean, action: suspend (Room) -> Unit) { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false) - val aliceSession = cryptoTestData.firstSession val room = aliceSession.getRoom(cryptoTestData.roomId)!! room.roomCryptoService().isEncrypted() shouldBe false val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) - val latch = CountDownLatch(1) + timeline.start() + waitFor( + continueWhen = { timeline.waitForEncryptedMessages() }, + action = { action.invoke(room) } + ) + timeline.dispose() + + room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted + } +} + +private suspend fun Timeline.waitForEncryptedMessages() { + suspendCancellableCoroutine { continuation -> val timelineListener = object : Timeline.Listener { override fun onTimelineFailure(throwable: Throwable) { } @@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest { .filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION } if (newMessages.isNotEmpty()) { - timeline.removeListener(this) - latch.countDown() + removeListener(this) + continuation.resume(Unit) } } } - timeline.start() - timeline.addListener(timelineListener) - - action.invoke(room) - testHelper.await(latch) - timeline.dispose() - testHelper.waitWithLatch { - room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted - it.countDown() - } + addListener(timelineListener) + continuation.invokeOnCancellation { removeListener(timelineListener) } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 7bb53e139c..60a89b4370 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -63,14 +63,12 @@ class KeyShareTests : InstrumentedTest { Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom( - CreateRoomParams().apply { - visibility = RoomDirectoryVisibility.PRIVATE - enableEncryption() - } - ) - } + val roomId = aliceSession.roomService().createRoom( + CreateRoomParams().apply { + visibility = RoomDirectoryVisibility.PRIVATE + enableEncryption() + } + ) val room = aliceSession.getRoom(roomId) assertNotNull(room) Thread.sleep(4_000) @@ -94,10 +92,8 @@ class KeyShareTests : InstrumentedTest { assertNotNull(receivedEvent) assert(receivedEvent!!.isEncrypted()) - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() @@ -111,15 +107,13 @@ class KeyShareTests : InstrumentedTest { var outGoingRequestId: String? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession2.cryptoService().getOutgoingRoomKeyRequests() - .let { - val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } - outGoingRequestId = outgoing?.requestId - outgoing != null - } - } + commonTestHelper.retryPeriodically { + aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + .let { + val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } + outGoingRequestId = outgoing?.requestId + outgoing != null + } } Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") @@ -131,9 +125,8 @@ class KeyShareTests : InstrumentedTest { // The first session should see an incoming request // the request should be refused, because the device is not trusted - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS + commonTestHelper.retryPeriodically { + // DEBUG LOGS // aliceSession.cryptoService().getIncomingRoomKeyRequests().let { // Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") // Log.v("TEST", "=========================") @@ -143,32 +136,27 @@ class KeyShareTests : InstrumentedTest { // Log.v("TEST", "=========================") // } - val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - incoming != null - } + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> - Log.v("TEST", "=========================") - Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") - Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") - Log.v("TEST", "=========================") - } - - val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val resultCode = (reply?.result as? RequestResult.Failure)?.code - resultCode == WithHeldCode.UNVERIFIED + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("TEST", "=========================") + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("TEST", "=========================") } + + val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val resultCode = (reply?.result as? RequestResult.Failure)?.code + resultCode == WithHeldCode.UNVERIFIED } - commonTestHelper.runBlockingTest { - mustFail { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } + mustFail { + aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") } // Mark the device as trusted @@ -210,12 +198,10 @@ class KeyShareTests : InstrumentedTest { // As it was share previously alice should accept to reshare bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - aliceReply != null && aliceReply.result is RequestResult.Success - } + commonTestHelper.retryPeriodically { + val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + aliceReply != null && aliceReply.result is RequestResult.Success } } @@ -233,12 +219,10 @@ class KeyShareTests : InstrumentedTest { val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) // we wait for alice first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! @@ -247,13 +231,11 @@ class KeyShareTests : InstrumentedTest { // As it was share previously alice should accept to reshare aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success } } @@ -277,12 +259,10 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.syncSession(aliceNewSession) // we wait bob first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -304,26 +284,22 @@ class KeyShareTests : InstrumentedTest { aliceNewSession.cryptoService().enableKeyGossiping(true) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = outgoing?.results - ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - Log.v("TEST", "own device result is $result") - result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + Log.v("TEST", "own device result is $result") + result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobDeviceReply = outgoing?.results - ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } - val result = bobDeviceReply?.result - Log.v("TEST", "bob device result is $result") - result != null && result is RequestResult.Success && result.chainIndex > 0 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } + val result = bobDeviceReply?.result + Log.v("TEST", "bob device result is $result") + result != null && result is RequestResult.Success && result.chainIndex > 0 } // it's a success but still can't decrypt first message @@ -337,21 +313,19 @@ class KeyShareTests : InstrumentedTest { // Let's now try to request aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // DEBUG LOGS - aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> - Log.v("TEST", "=========================") - Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") - Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") - Log.v("TEST", "=========================") - } - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 0 + commonTestHelper.retryPeriodically { + // DEBUG LOGS + aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("TEST", "=========================") + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("TEST", "=========================") } + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 } // now the new session should be able to decrypt all! @@ -363,13 +337,11 @@ class KeyShareTests : InstrumentedTest { ) // Additional test, can we check that bob replied successfully but with a ratcheted key - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } - val result = bobReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 3 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 } commonTestHelper.signOutAndClose(aliceNewSession) @@ -394,12 +366,10 @@ class KeyShareTests : InstrumentedTest { val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) // we wait bob first session to be aware of that session? - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) - .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } - newSession != null - } + commonTestHelper.retryPeriodically { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() @@ -430,14 +400,12 @@ class KeyShareTests : InstrumentedTest { aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) // Should get a reply from bob and not from alice - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } - val result = bobReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 3 - } + commonTestHelper.retryPeriodically { + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 } val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } @@ -450,14 +418,12 @@ class KeyShareTests : InstrumentedTest { aliceSession.syncService().startSync(true) // We should now get a reply from first session - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } - val ownDeviceReply = - outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } - val result = ownDeviceReply?.result - result != null && result is RequestResult.Success && result.chainIndex == 0 - } + commonTestHelper.retryPeriodically { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 } // It should be in sent then cancel diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 0aac4297e4..b4b430484e 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -80,10 +80,8 @@ class WithHeldTests : InstrumentedTest { val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() // await for bob unverified session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null - } + testHelper.retryPeriodically { + bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null } val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! @@ -95,60 +93,52 @@ class WithHeldTests : InstrumentedTest { // Bob should not be able to decrypt because the keys is withheld // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) - } - ) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + } + ) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } // Let's see if the reply we got from bob first session is unverified - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() - .firstOrNull { it.sessionId == megolmSessionId } - ?.results - ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } - ?.result - ?.let { - it as? RequestResult.Failure - } - ?.code == WithHeldCode.UNVERIFIED - } + testHelper.retryPeriodically { + bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() + .firstOrNull { it.sessionId == megolmSessionId } + ?.results + ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } + ?.result + ?.let { + it as? RequestResult.Failure + } + ?.code == WithHeldCode.UNVERIFIED } // enable back sending to unverified aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) - // wait until it's decrypted - ev?.root?.getClearType() == EventType.MESSAGE - } + testHelper.retryPeriodically { + val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) + // wait until it's decrypted + ev?.root?.getClearType() == EventType.MESSAGE } // Previous message should still be undecryptable (partially withheld session) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) - }) { - bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + }) { + bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "") } } @@ -177,26 +167,22 @@ class WithHeldTests : InstrumentedTest { val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId // await for bob session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null - } + testHelper.retryPeriodically { + bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null } // Previous message should still be undecryptable (partially withheld session) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) // .. might need to wait a bit for stability? - testHelper.runBlockingTest { - mustFail( - message = "This session should not be able to decrypt", - failureBlock = { failure -> - val type = (failure as MXCryptoError.Base).errorType - val technicalMessage = failure.technicalMessage - Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) - }) { - bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") - } + mustFail( + message = "This session should not be able to decrypt", + failureBlock = { failure -> + val type = (failure as MXCryptoError.Base).errorType + val technicalMessage = failure.technicalMessage + Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) + Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) + }) { + bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "") } // Ensure that alice has marked the session to be shared with bob @@ -216,10 +202,8 @@ class WithHeldTests : InstrumentedTest { // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null - } + testHelper.retryPeriodically { + bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null } val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( @@ -258,27 +242,21 @@ class WithHeldTests : InstrumentedTest { var sessionId: String? = null // Check that the // await for bob SecondSession session to get the message - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { - // try to decrypt and force key request - tryOrNull { - testHelper.runBlockingTest { - bobSecondSession.cryptoService().decryptEvent(it.root, "") - } - } + testHelper.retryPeriodically { + val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { + // try to decrypt and force key request + tryOrNull { + bobSecondSession.cryptoService().decryptEvent(it.root, "") } - sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId - timeLineEvent != null } + sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId + timeLineEvent != null } // Check that bob second session requested the key - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) - wc?.code == WithHeldCode.UNAUTHORISED - } + testHelper.retryPeriodically { + val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) + wc?.code == WithHeldCode.UNAUTHORISED } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt index cf201611a0..8679cf3c99 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt @@ -30,7 +30,7 @@ internal data class KeysBackupScenarioData( val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val aliceSession2: Session ) { - fun cleanUp(testHelper: CommonTestHelper) { + suspend fun cleanUp(testHelper: CommonTestHelper) { cryptoTestData.cleanUp(testHelper) testHelper.signOutAndClose(aliceSession2) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt index 2439119f01..01c03b8001 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt @@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.FixMethodOrder import org.junit.Rule @@ -48,9 +48,11 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.TestConstants -import org.matrix.android.sdk.common.TestMatrixCallback +import org.matrix.android.sdk.common.waitFor +import java.security.InvalidParameterException import java.util.Collections import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -116,7 +118,7 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(null, null, it) } @@ -133,7 +135,6 @@ class KeysBackupTest : InstrumentedTest { */ @Test fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> - val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) cryptoTestHelper.initializeCrossSigning(bobSession) @@ -143,14 +144,14 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(null, null, it) } assertFalse(keysBackup.isEnabled()) // Create the version - val version = testHelper.doSync { + val version = testHelper.waitForCallback { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -158,10 +159,10 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Check that it's signed with MSK - val versionResult = testHelper.doSync { + val versionResult = testHelper.waitForCallback { keysBackup.getVersion(version.version, it) } - val trust = testHelper.doSync { + val trust = testHelper.waitForCallback { keysBackup.getKeysBackupTrust(versionResult!!, it) } @@ -257,7 +258,7 @@ class KeysBackupTest : InstrumentedTest { var lastBackedUpKeysProgress = 0 - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { assertEquals(nbOfKeys, total) @@ -299,7 +300,7 @@ class KeysBackupTest : InstrumentedTest { val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo // - Check encryptGroupSession() returns stg - val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } + val keyBackupData = keysBackup.encryptGroupSession(session) assertNotNull(keyBackupData) assertNotNull(keyBackupData!!.sessionData) @@ -334,7 +335,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) // - Restore the e2e backup from the homeserver - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -379,7 +380,7 @@ class KeysBackupTest : InstrumentedTest { // assertTrue(unsentRequest != null || sentRequest != null) // // // - Restore the e2e backup from the homeserver -// val importRoomKeysResult = mTestHelper.doSync { +// val importRoomKeysResult = mTestHelper.doSyncSuspending<> { } { // testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, // testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, // null, @@ -429,7 +430,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, true, @@ -445,14 +446,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -489,7 +490,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the recovery key - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -505,14 +506,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -547,13 +548,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong recovery key - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "Bad recovery key", - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + "Bad recovery key", + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -591,7 +592,7 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Trust the backup from the new device with the password - testHelper.doSync { + testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -607,14 +608,14 @@ class KeysBackupTest : InstrumentedTest { assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) // - Retrieve the last version from the server - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() // - It must be the same assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) } @@ -652,13 +653,13 @@ class KeysBackupTest : InstrumentedTest { assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) // - Try to trust the backup from the new device with a wrong password - val latch = CountDownLatch(1) - testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( - testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - badPassword, - TestMatrixCallback(latch, false) - ) - testHelper.await(latch) + testHelper.waitForCallbackError { + testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( + testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, + badPassword, + it + ) + } // - The new device must still see the previous backup as not trusted assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) @@ -679,26 +680,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong recovery key - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeysWithRecoveryKey( + keysBackupService.keysBackupVersion!!, + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -718,7 +714,7 @@ class KeysBackupTest : InstrumentedTest { // - Restore the e2e backup with the password val steps = ArrayList() - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, password, @@ -771,26 +767,21 @@ class KeysBackupTest : InstrumentedTest { val wrongPassword = "passw0rd" val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a wrong password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - wrongPassword, - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + wrongPassword, + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is InvalidParameterException) } /** @@ -808,7 +799,7 @@ class KeysBackupTest : InstrumentedTest { val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) // - Restore the e2e backup with the recovery key. - val importRoomKeysResult = testHelper.doSync { + val importRoomKeysResult = testHelper.waitForCallback { testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, @@ -833,26 +824,21 @@ class KeysBackupTest : InstrumentedTest { val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) + val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService() // - Try to restore the e2e backup with a password - val latch2 = CountDownLatch(1) - var importRoomKeysResult: ImportRoomKeysResult? = null - testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - "password", - null, - null, - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: ImportRoomKeysResult) { - importRoomKeysResult = data - super.onSuccess(data) - } - } - ) - testHelper.await(latch2) + val importRoomKeysResult = testHelper.waitForCallbackError { + keysBackupService.restoreKeyBackupWithPassword( + keysBackupService.keysBackupVersion!!, + "password", + null, + null, + null, + it + ) + } - // onSuccess may not have been called - assertNull(importRoomKeysResult) + assertTrue(importRoomKeysResult is IllegalStateException) } /** @@ -874,12 +860,12 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Get key backup version from the homeserver - val keysVersionResult = testHelper.doSync { + val keysVersionResult = testHelper.waitForCallback { keysBackup.getCurrentVersion(it) }.toKeysVersionResult() // - Check the returned KeyBackupVersion is trusted - val keysBackupVersionTrust = testHelper.doSync { + val keysBackupVersionTrust = testHelper.waitForCallback { keysBackup.getKeysBackupTrust(keysVersionResult!!, it) } @@ -918,34 +904,39 @@ class KeysBackupTest : InstrumentedTest { assertFalse(keysBackup.isEnabled()) // Wait for keys backup to be finished - val latch0 = CountDownLatch(1) var count = 0 - keysBackup.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (newState == KeysBackupState.ReadyToBackUp) { - count++ + waitFor( + continueWhen = { + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (newState == KeysBackupState.ReadyToBackUp) { + count++ - if (count == 2) { - // Remove itself from the list of listeners - keysBackup.removeListener(this) - - latch0.countDown() + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + continuation.resume(Unit) + } + } + } + } + keysBackup.addListener(listener) + continuation.invokeOnCancellation { keysBackup.removeListener(listener) } } - } - } - }) - - // - Make alice back up her keys to her homeserver - keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + action = { + // - Make alice back up her keys to her homeserver + keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) + }, + ) assertTrue(keysBackup.isEnabled()) - testHelper.await(latch0) - // - Create a new backup with fake data on the homeserver, directly using the rest client val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo() - testHelper.doSync { + testHelper.waitForCallback { (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -953,9 +944,7 @@ class KeysBackupTest : InstrumentedTest { (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() // - Make alice back up all her keys again - val latch2 = CountDownLatch(1) - keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false)) - testHelper.await(latch2) + testHelper.waitForCallbackError { keysBackup.backupAllGroupSessions(null, it) } // -> That must fail and her backup state must be WrongBackUpVersion assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) @@ -991,7 +980,7 @@ class KeysBackupTest : InstrumentedTest { keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) // Wait for keys backup to finish by asking again to backup keys. - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(null, it) } @@ -1016,19 +1005,7 @@ class KeysBackupTest : InstrumentedTest { val stateObserver2 = StateObserver(keysBackup2) - var isSuccessful = false - val latch2 = CountDownLatch(1) - keysBackup2.backupAllGroupSessions( - null, - object : TestMatrixCallback(latch2, false) { - override fun onSuccess(data: Unit) { - isSuccessful = true - super.onSuccess(data) - } - }) - testHelper.await(latch2) - - assertFalse(isSuccessful) + testHelper.waitForCallbackError { keysBackup2.backupAllGroupSessions(null, it) } // Backup state must be NotTrusted assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) @@ -1042,24 +1019,25 @@ class KeysBackupTest : InstrumentedTest { ) // -> Backup should automatically enable on the new device - val latch4 = CountDownLatch(1) - keysBackup2.addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - // Check the backup completes - if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { - // Remove itself from the list of listeners - keysBackup2.removeListener(this) - - latch4.countDown() + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + // Check the backup completes + if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + continuation.resume(Unit) + } } } - }) - testHelper.await(latch4) + keysBackup2.addListener(listener) + continuation.invokeOnCancellation { keysBackup2.removeListener(listener) } + } // -> It must use the same backup version assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) - testHelper.doSync { + testHelper.waitForCallback { aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) } @@ -1092,7 +1070,7 @@ class KeysBackupTest : InstrumentedTest { assertTrue(keysBackup.isEnabled()) // Delete the backup - testHelper.doSync { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } + testHelper.waitForCallback { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } // Backup is now disabled assertFalse(keysBackup.isEnabled()) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt index 2cc2b506b9..10abf93bcb 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Assert import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session @@ -29,7 +30,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.assertDictEquals import org.matrix.android.sdk.common.assertListEquals import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import java.util.concurrent.CountDownLatch +import kotlin.coroutines.resume internal class KeysBackupTestHelper( private val testHelper: CommonTestHelper, @@ -47,7 +48,7 @@ internal class KeysBackupTestHelper( * * @param password optional password */ - fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { + suspend fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() waitForKeybackUpBatching() @@ -64,7 +65,7 @@ internal class KeysBackupTestHelper( var lastProgress = 0 var lastTotal = 0 - testHelper.doSync { + testHelper.waitForCallback { keysBackup.backupAllGroupSessions(object : ProgressListener { override fun onProgress(progress: Int, total: Int) { lastProgress = progress @@ -97,13 +98,13 @@ internal class KeysBackupTestHelper( ) } - fun prepareAndCreateKeysBackupData( + suspend fun prepareAndCreateKeysBackupData( keysBackup: KeysBackupService, password: String? = null ): PrepareKeysBackupDataResult { val stateObserver = StateObserver(keysBackup) - val megolmBackupCreationInfo = testHelper.doSync { + val megolmBackupCreationInfo = testHelper.waitForCallback { keysBackup.prepareKeysBackupVersion(password, null, it) } @@ -112,7 +113,7 @@ internal class KeysBackupTestHelper( Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) // Create the version - val keysVersion = testHelper.doSync { + val keysVersion = testHelper.waitForCallback { keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) } @@ -129,25 +130,26 @@ internal class KeysBackupTestHelper( * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the * KeysBackup object to be in the specified state */ - fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { + suspend fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) { // If already in the wanted state, return - if (session.cryptoService().keysBackupService().getState() == state) { + val keysBackupService = session.cryptoService().keysBackupService() + if (keysBackupService.getState() == state) { return } // Else observe state changes - val latch = CountDownLatch(1) - - session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { - override fun onStateChange(newState: KeysBackupState) { - if (newState == state) { - session.cryptoService().keysBackupService().removeListener(this) - latch.countDown() + suspendCancellableCoroutine { continuation -> + val listener = object : KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + if (newState == state) { + keysBackupService.removeListener(this) + continuation.resume(Unit) + } } } - }) - - testHelper.await(latch) + keysBackupService.addListener(listener) + continuation.invokeOnCancellation { keysBackupService.removeListener(listener) } + } } fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt index 53cf802b91..0dfecffbde 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt @@ -58,18 +58,16 @@ class ReplayAttackTest : InstrumentedTest { val fakeEventWithTheSameIndex = sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId)) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - // Lets decrypt the fake event that will have the same message index - val exception = assertFailsWith { - // An exception should be thrown while the same index would have been used for the previous decryption - aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) - } - assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + // Lets decrypt the fake event that will have the same message index + val exception = assertFailsWith { + // An exception should be thrown while the same index would have been used for the previous decryption + aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId) } + assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType) cryptoTestData.cleanUp(testHelper) } @@ -93,17 +91,15 @@ class ReplayAttackTest : InstrumentedTest { Assert.assertTrue("Message should be sent", sentEvents.size == 1) assertEquals(sentEvents.size, 1) - testHelper.runBlockingTest { - // Lets assume we are from the main timelineId - val timelineId = "timelineId" - // Lets decrypt the original event + // Lets assume we are from the main timelineId + val timelineId = "timelineId" + // Lets decrypt the original event + aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) + try { + // Lets try to decrypt the same event aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - try { - // Lets try to decrypt the same event - aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) - } catch (ex: Throwable) { - fail("Shouldn't throw a decryption error for same event") - } + } catch (ex: Throwable) { + fail("Shouldn't throw a decryption error for same event") } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt index c8be6aae74..0467d082a3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.ssss -import androidx.lifecycle.Observer import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -37,12 +36,12 @@ import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo -import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService @RunWith(AndroidJUnit4::class) @@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - var accountData: UserAccountDataEvent? = null // Assert Account data is updated - testHelper.waitWithLatch { - val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") - val accountDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { - accountData = t.getOrNull() - } - it.countDown() - } - liveAccountData.observeForever(accountDataObserver) - } + val accountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") } + .first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" } + .getOrNull() + assertNotNull("Key should be stored in account data", accountData) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) assertNotNull("Key Content cannot be parsed", parsed) @@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest { assertEquals("Unexpected key name", "Test Key", parsed.name) assertNull("Key was not generated from passphrase", parsed.passphrase) - var defaultKeyAccountData: UserAccountDataEvent? = null + quadS.setDefaultKey(TEST_KEY_ID) + val defaultKeyAccountData = aliceSession.accountDataService() + .onMain { getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } + .first { it.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID } + .getOrNull() + // Set as default key - testHelper.waitWithLatch { latch -> - quadS.setDefaultKey(TEST_KEY_ID) - val liveDefAccountData = - aliceSession.accountDataService().getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) - val accountDefDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) { - defaultKeyAccountData = t.getOrNull()!! - latch.countDown() - } - } - liveDefAccountData.observeForever(accountDefDataObserver) - } assertNotNull(defaultKeyAccountData?.content) assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) @@ -112,21 +96,19 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId = "My.Key" - val info = generatedSecret(testHelper, aliceSession, keyId, true) + val info = generatedSecret(aliceSession, keyId, true) val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) // Store a secret val clearSecret = "42".toByteArray().toBase64NoPadding() - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "secret.of.life", - clearSecret, - listOf(KeyRef(null, keySpec)) // default key - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "secret.of.life", + clearSecret, + listOf(KeyRef(null, keySpec)) // default key + ) - val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life") + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *> assertNotNull("Element should be encrypted", encryptedContent) @@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest { // Try to decrypt?? - val decryptedSecret = testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "secret.of.life", - null, // default key - keySpec!! - ) - } + val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret( + "secret.of.life", + null, // default key + keySpec!! + ) assertEquals("Secret mismatch", clearSecret, decryptedSecret) } @@ -159,14 +139,10 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" - testHelper.runBlockingTest { - quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) - } + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner) // Test that we don't need to wait for an account data sync to access directly the keyid from DB - testHelper.runBlockingTest { - quadS.setDefaultKey(TEST_KEY_ID) - } + quadS.setDefaultKey(TEST_KEY_ID) } @Test @@ -174,22 +150,20 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" - val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true) + val key1Info = generatedSecret(aliceSession, keyId1, true) val keyId2 = "Key2" - val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true) + val key2Info = generatedSecret(aliceSession, keyId2, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf( - KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), - KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) - ) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf( + KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), + KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) + ) + ) val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret") val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> @@ -200,21 +174,17 @@ class QuadSTests : InstrumentedTest { assertNotNull(encryptedContent?.get(keyId2)) // Assert that can decrypt with both keys - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! - ) - } + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! + ) - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId2, - RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! - ) - } + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId2, + RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! + ) } @Test @@ -224,104 +194,84 @@ class QuadSTests : InstrumentedTest { val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val keyId1 = "Key.1" val passphrase = "The good pass phrase" - val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true) + val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true) val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" - testHelper.runBlockingTest { - aliceSession.sharedSecretStorageService().storeSecret( - "my.secret", - mySecretText.toByteArray().toBase64NoPadding(), - listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) - ) - } + aliceSession.sharedSecretStorageService().storeSecret( + "my.secret", + mySecretText.toByteArray().toBase64NoPadding(), + listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) + ) - testHelper.runBlockingTest { - try { - aliceSession.sharedSecretStorageService().getSecret( - "my.secret", - keyId1, - RawBytesKeySpec.fromPassphrase( - "A bad passphrase", - key1Info.content?.passphrase?.salt ?: "", - key1Info.content?.passphrase?.iterations ?: 0, - null - ) - ) - } catch (throwable: Throwable) { - assert(throwable is SharedSecretStorageError.BadMac) - } - } - - // Now try with correct key - testHelper.runBlockingTest { + try { aliceSession.sharedSecretStorageService().getSecret( "my.secret", keyId1, RawBytesKeySpec.fromPassphrase( - passphrase, + "A bad passphrase", key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.iterations ?: 0, null ) ) + } catch (throwable: Throwable) { + assert(throwable is SharedSecretStorageError.BadMac) } + + // Now try with correct key + aliceSession.sharedSecretStorageService().getSecret( + "my.secret", + keyId1, + RawBytesKeySpec.fromPassphrase( + passphrase, + key1Info.content?.passphrase?.salt ?: "", + key1Info.content?.passphrase?.iterations ?: 0, + null + ) + ) } - private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent { - var accountData: UserAccountDataEvent? = null - testHelper.waitWithLatch { - val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) - val accountDataObserver = Observer?> { t -> - if (t?.getOrNull()?.type == type) { - accountData = t.getOrNull() - it.countDown() - } - } - liveAccountData.observeForever(accountDataObserver) - } + private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent { + val accountData = session.accountDataService() + .onMain { getLiveUserAccountDataEvent(type) } + .first { it.getOrNull()?.type == type } + .getOrNull() + assertNotNull("Account Data type:$type should be found", accountData) return accountData!! } - private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKey(keyId, null, keyId, emptyKeySigner) - } + val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo } - private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { + private suspend fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { val quadS = session.sharedSecretStorageService() - val creationInfo = testHelper.runBlockingTest { - quadS.generateKeyWithPassphrase( - keyId, - keyId, - passphrase, - emptyKeySigner, - null - ) - } + val creationInfo = quadS.generateKeyWithPassphrase( + keyId, + keyId, + passphrase, + emptyKeySigner, + null + ) - assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") + assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") if (asDefault) { - testHelper.runBlockingTest { - quadS.setDefaultKey(keyId) - } - assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) + quadS.setDefaultKey(keyId) + assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) } return creationInfo diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt index 1bffbeeeaa..fd2136edd5 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt @@ -547,23 +547,19 @@ class SASTest : InstrumentedTest { var requestID: String? = null - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - requestID = prAlicePOV?.transactionId - Log.v("TEST", "== alicePOV is $prAlicePOV") - prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + requestID = prAlicePOV?.transactionId + Log.v("TEST", "== alicePOV is $prAlicePOV") + prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId } Log.v("TEST", "== requestID is $requestID") - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() - Log.v("TEST", "== prBobPOV is $prBobPOV") - prBobPOV?.transactionId == requestID - } + testHelper.retryPeriodically { + val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() + Log.v("TEST", "== prBobPOV is $prBobPOV") + prBobPOV?.transactionId == requestID } bobVerificationService.readyPendingVerification( @@ -573,12 +569,10 @@ class SASTest : InstrumentedTest { ) // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() - Log.v("TEST", "== prAlicePOV is $prAlicePOV") - prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null - } + testHelper.retryPeriodically { + val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() + Log.v("TEST", "== prAlicePOV is $prAlicePOV") + prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null } // Start concurrent! @@ -602,20 +596,16 @@ class SASTest : InstrumentedTest { var alicePovTx: SasVerificationTransaction? var bobPovTx: SasVerificationTransaction? - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== alicePovTx is $alicePovTx") - alicePovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== alicePovTx is $alicePovTx") + alicePovTx?.state == VerificationTxState.ShortCodeReady } // wait for alice to get the ready - testHelper.waitWithLatch { - testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction - Log.v("TEST", "== bobPovTx is $bobPovTx") - bobPovTx?.state == VerificationTxState.ShortCodeReady - } + testHelper.retryPeriodically { + bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction + Log.v("TEST", "== bobPovTx is $bobPovTx") + bobPovTx?.state == VerificationTxState.ShortCodeReady } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 3f22906965..4ecfe5be8f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -164,7 +164,7 @@ class VerificationTest : InstrumentedTest { val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! - testHelper.doSync { callback -> + testHelper.waitForCallback { callback -> aliceSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -181,7 +181,7 @@ class VerificationTest : InstrumentedTest { ) } - testHelper.doSync { callback -> + testHelper.waitForCallback { callback -> bobSession.cryptoService().crossSigningService() .initializeCrossSigning( object : UserInteractiveAuthInterceptor { @@ -261,7 +261,11 @@ class VerificationTest : InstrumentedTest { val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) - val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams) + val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount( + aliceSessionToVerify.myUserId, + TestConstants.PASSWORD, + defaultSessionParams + ) val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW) @@ -286,11 +290,9 @@ class VerificationTest : InstrumentedTest { otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId), ) - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) - requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } - } + testHelper.retryPeriodically { + val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) + requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice } } testHelper.signOutAndClose(aliceSessionToVerify) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index 59b3b14532..656e00bcbd 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.session.room.timeline import androidx.test.filters.LargeTest -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine import org.amshove.kluent.internal.assertEquals import org.junit.FixMethodOrder import org.junit.Ignore @@ -35,6 +35,9 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.TestConstants +import org.matrix.android.sdk.common.waitFor +import org.matrix.android.sdk.common.wrapWithTimeout +import kotlin.coroutines.resume @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -69,30 +72,36 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30)) bobTimeline.start() - commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { - val listener = object : Timeline.Listener { + waitFor( + continueWhen = { + wrapWithTimeout(timeout = TestConstants.timeOutMillis * 10) { + suspendCancellableCoroutine { continuation -> + val listener = object : Timeline.Listener { - override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if (direction == Timeline.Direction.FORWARDS) { - return + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if (direction == Timeline.Direction.FORWARDS) { + return + } + if (state.hasMoreToLoad && !state.loading) { + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } else if (!state.hasMoreToLoad) { + bobTimeline.removeListener(this) + continuation.resume(Unit) + } + } + } + bobTimeline.addListener(listener) + continuation.invokeOnCancellation { bobTimeline.removeListener(listener) } + } } - if (state.hasMoreToLoad && !state.loading) { - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } else if (!state.hasMoreToLoad) { - bobTimeline.removeListener(this) - it.countDown() - } - } - } - bobTimeline.addListener(listener) - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) - } + }, + action = { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) } + ) + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) - val onlySentEvents = runBlocking { - bobTimeline.getSnapshot() - } + val onlySentEvents = bobTimeline.getSnapshot() .filter { it.root.isTextMessage() }.filter { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt index 7c97426c39..6ef90193d8 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt @@ -85,9 +85,7 @@ class SearchMessagesTest : InstrumentedTest { 2 ) - val data = commonTestHelper.runBlockingTest { - block.invoke(cryptoTestData) - } + val data = block.invoke(cryptoTestData) assertTrue(data.results?.size == 2) assertTrue( diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt index 2cd579df24..df131cc19a 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt @@ -55,15 +55,11 @@ class SpaceCreationTest : InstrumentedTest { val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val roomName = "My Space" val topic = "A public space for test" - var spaceId: String = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(roomName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(roomName, topic, null, true) - commonTestHelper.waitWithLatch { - commonTestHelper.retryPeriodicallyWithLatch(it) { - session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null - } + commonTestHelper.retryPeriodically { + val roomSummary = session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary() + roomSummary?.name == roomName && roomSummary.topic == topic } val syncedSpace = session.spaceService().getSpace(spaceId) @@ -79,14 +75,12 @@ class SpaceCreationTest : InstrumentedTest { assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) var powerLevelsContent: PowerLevelsContent? = null - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - powerLevelsContent = syncedSpace.asRoom() - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - powerLevelsContent != null - } + commonTestHelper.retryPeriodically { + powerLevelsContent = syncedSpace.asRoom() + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + ?.toModel() + powerLevelsContent != null } assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) @@ -116,19 +110,13 @@ class SpaceCreationTest : InstrumentedTest { val roomName = "My Space" val topic = "A public space for test" - val spaceId: String - runBlocking { - spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) - // wait a bit to let the summary update it self :/ - delay(400) - } + val spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) + // wait a bit to let the summary update it self :/ + delay(400) // Try to join from bob, it's a public space no need to invite - val joinResult: JoinSpaceResult - runBlocking { - joinResult = bobSession.spaceService().joinSpace(spaceId) - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) @@ -152,43 +140,24 @@ class SpaceCreationTest : InstrumentedTest { val syncedSpace = aliceSession.spaceService().getSpace(spaceId) // create a room - var firstChild: String? = null - commonTestHelper.waitWithLatch { - firstChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "FirstRoom" - this.topic = "Description of first room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val firstChild: String = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "FirstRoom" + this.topic = "Description of first room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true) - var secondChild: String? = null - commonTestHelper.waitWithLatch { - secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { - this.name = "SecondRoom" - this.topic = "Description of second room" - this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT - }) - it.countDown() - } + val secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { + this.name = "SecondRoom" + this.topic = "Description of second room" + this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + }) - commonTestHelper.waitWithLatch { - syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) - it.countDown() - } + syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true) // Try to join from bob, it's a public space no need to invite - var joinResult: JoinSpaceResult? = null - commonTestHelper.waitWithLatch { - joinResult = bobSession.spaceService().joinSpace(spaceId) - // wait a bit to let the summary update it self :/ - it.countDown() - } + val joinResult = bobSession.spaceService().joinSpace(spaceId) assertEquals(JoinSpaceResult.Success, joinResult) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt index 18645fd6d9..abe9af5e38 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.session.space import android.util.Log -import androidx.lifecycle.Observer -import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -39,16 +37,17 @@ import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest import org.matrix.android.sdk.common.SessionTestParams +import org.matrix.android.sdk.common.first +import org.matrix.android.sdk.common.onMain +import org.matrix.android.sdk.common.waitFor @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @@ -60,40 +59,28 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceName = "My Space" val topic = "A public space for test" - var spaceId = "" - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, topic, null, true) - } + val spaceId = session.spaceService().createSpace(spaceName, topic, null, true) val syncedSpace = session.spaceService().getSpace(spaceId) - var roomId = "" - commonTestHelper.runBlockingTest { - roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) - } + val roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" }) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - syncedSpace!!.addChildren(roomId, viaServers, null, true) - } + syncedSpace!!.addChildren(roomId, viaServers, null, true) - commonTestHelper.runBlockingTest { - session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - } + session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents - val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } - parents?.forEach { - Log.d("## TEST", "parent : $it") - } - parents?.size == 1 && - parents.first().roomSummary?.name == spaceName && - canonicalParents?.size == 1 && - canonicalParents.first().roomSummary?.name == spaceName + commonTestHelper.retryPeriodically { + val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents + val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } + parents?.forEach { + Log.d("## TEST", "parent : $it") } + parents?.size == 1 && + parents.first().roomSummary?.name == spaceName && + canonicalParents?.size == 1 && + canonicalParents.first().roomSummary?.name == spaceName } } @@ -169,7 +156,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -178,7 +164,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) /* val spaceBInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -188,7 +173,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -199,22 +183,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // Create orphan rooms - - var orphan1 = "" - commonTestHelper.runBlockingTest { - orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) - } - - var orphan2 = "" - commonTestHelper.runBlockingTest { - orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) - } + val orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" }) + val orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" }) val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) @@ -235,15 +209,15 @@ class SpaceHierarchyTest : InstrumentedTest { assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" }) // Add a non canonical child and check that it does not appear as orphan - commonTestHelper.runBlockingTest { - val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) - spaceA!!.addChildren(a3, viaServers, null, false) - } + val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) + spaceA.addChildren(a3, viaServers, null, false) + + val orphansUpdate = session.roomService().onMain { + getRoomSummariesLive(roomSummaryQueryParams { + spaceFilter = SpaceFilter.OrphanRooms + }) + }.first { it.size == 2 } - Thread.sleep(6_000) - val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams { - spaceFilter = SpaceFilter.OrphanRooms - }) assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size) } @@ -253,7 +227,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -262,7 +235,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -273,16 +245,12 @@ class SpaceHierarchyTest : InstrumentedTest { // add C as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers) // add back A as subspace of C - commonTestHelper.runBlockingTest { - val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) - spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) - } + val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) + spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true) // A -> C -> A @@ -300,7 +268,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( @@ -310,7 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( @@ -323,13 +289,10 @@ class SpaceHierarchyTest : InstrumentedTest { // add B as a subspace of A val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) - session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) - } + spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) + session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( @@ -338,52 +301,39 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.waitWithLatch { latch -> - - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer> { - override fun onChanged(children: List?) { -// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + waitFor( + continueWhen = { + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + children.any { it.name == "C1" } && children.any { it.name == "C2" } } + }, + action = { + // add C as subspace of B + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) } - } - - flatAChildren.observeForever(childObserver) - - // add C as subspace of B - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - - // C1 and C2 should be in flatten child of A now - } + ) + // C1 and C2 should be in flatten child of A now // Test part one of the rooms val bRoomId = spaceBInfo.roomIds.first() - commonTestHelper.waitWithLatch { latch -> - val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) - val childObserver = object : Observer> { - override fun onChanged(children: List?) { - System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") - if (children?.any { it.roomId == bRoomId } == false) { - // B1 has been added live! - latch.countDown() - flatAChildren.removeObserver(this) + waitFor( + continueWhen = { + // The room should have disappear from flat children + session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children -> + println("## TEST | Space A flat children update : ${children.map { it.name }}") + !children.any { it.roomId == bRoomId } } + }, + action = { + // part from b room + session.roomService().leaveRoom(bRoomId) } - } + ) - // The room should have disapear from flat children - flatAChildren.observeForever(childObserver) - // part from b room - session.roomService().leaveRoom(bRoomId) - } commonTestHelper.signOutAndClose(session) } @@ -392,68 +342,57 @@ class SpaceHierarchyTest : InstrumentedTest { val roomIds: List ) - private fun createPublicSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPublicSpace( session: Session, spaceName: String, childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + val spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - roomIds = childInfo.map { entry -> - session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val roomIds = childInfo.map { entry -> + session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) } - private fun createPrivateSpace( - commonTestHelper: CommonTestHelper, + private suspend fun createPrivateSpace( session: Session, spaceName: String, childInfo: List> /** Name, auto-join, canonical*/ ): TestSpaceCreationResult { - var spaceId = "" - var roomIds: List = emptyList() - commonTestHelper.runBlockingTest { - spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) - val syncedSpace = session.spaceService().getSpace(spaceId) - val viaServers = listOf(session.sessionParams.homeServerHost ?: "") - roomIds = - childInfo.map { entry -> - val homeServerCapabilities = session - .homeServerCapabilitiesService() - .getHomeServerCapabilities() - session.roomService().createRoom(CreateRoomParams().apply { - name = entry.first - this.featurePreset = RestrictedRoomPreset( - homeServerCapabilities, - listOf( - RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) - ) - ) - }) - } - roomIds.forEachIndexed { index, roomId -> - syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) - val canonical = childInfo[index].third - if (canonical != null) { - session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) - } + val spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) + val syncedSpace = session.spaceService().getSpace(spaceId) + val viaServers = listOf(session.sessionParams.homeServerHost ?: "") + val roomIds = childInfo.map { entry -> + val homeServerCapabilities = session + .homeServerCapabilitiesService() + .getHomeServerCapabilities() + session.roomService().createRoom(CreateRoomParams().apply { + name = entry.first + this.featurePreset = RestrictedRoomPreset( + homeServerCapabilities, + listOf( + RoomJoinRulesAllowEntry.restrictedToRoom(spaceId) + ) + ) + }) + } + roomIds.forEachIndexed { index, roomId -> + syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) + val canonical = childInfo[index].third + if (canonical != null) { + session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) } } return TestSpaceCreationResult(spaceId, roomIds) @@ -464,7 +403,6 @@ class SpaceHierarchyTest : InstrumentedTest { val session = commonTestHelper.createAccount("John", SessionTestParams(true)) /* val spaceAInfo = */ createPublicSpace( - commonTestHelper, session, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -473,7 +411,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, session, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -483,7 +420,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceCInfo = createPublicSpace( - commonTestHelper, session, "SpaceC", listOf( Triple("C1", true /*auto-join*/, true/*canonical*/), @@ -494,10 +430,8 @@ class SpaceHierarchyTest : InstrumentedTest { val viaServers = listOf(session.sessionParams.homeServerHost ?: "") // add C as subspace of B - runBlocking { - val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) - spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) - } + val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) + spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) // Thread.sleep(4_000) // + A @@ -507,11 +441,9 @@ class SpaceHierarchyTest : InstrumentedTest { // + C // + c1, c2 - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } - rootSpaces.size == 2 - } + commonTestHelper.retryPeriodically { + val rootSpaces = session.spaceService().getRootSpaceSummaries() + rootSpaces.size == 2 } } @@ -521,7 +453,6 @@ class SpaceHierarchyTest : InstrumentedTest { val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val spaceAInfo = createPrivateSpace( - commonTestHelper, aliceSession, "Private Space A", listOf( Triple("General", true /*suggested*/, true/*canonical*/), @@ -529,85 +460,58 @@ class SpaceHierarchyTest : InstrumentedTest { ) ) - commonTestHelper.runBlockingTest { - aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) + aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null) + + bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) + + val bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) + bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) + + aliceSession.roomService().joinRoom(bobRoomId) + + commonTestHelper.retryPeriodically { + aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true } - commonTestHelper.runBlockingTest { - bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList()) - } + bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - var bobRoomId = "" - commonTestHelper.runBlockingTest { - bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) - bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId) - } - - commonTestHelper.runBlockingTest { - aliceSession.roomService().joinRoom(bobRoomId) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true - } - } - - commonTestHelper.runBlockingTest { - bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) - stateEvent != null - } + commonTestHelper.retryPeriodically { + val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId)) + stateEvent != null } // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space - commonTestHelper.runBlockingTest { - // we can see the state event - // but it is not valid and room is not in hierarchy - assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) - } + // we can see the state event + // but it is not valid and room is not in hierarchy + assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true) // Let's now try to make alice admin of the room - commonTestHelper.waitWithLatch { - val room = bobSession.getRoom(bobRoomId)!! - val currentPLContent = room + val room = bobSession.getRoom(bobRoomId)!! + val currentPLContent = room + .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) + ?.content + .toModel() + + val newPowerLevelsContent = currentPLContent + ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) + ?.toContent() + + room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) + + commonTestHelper.retryPeriodically { + val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content - .toModel() - - val newPowerLevelsContent = currentPLContent - ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value) - ?.toContent() - - room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!) - it.countDown() + ?.toModel() + ?.let { PowerLevelsHelper(it) } + powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!! - .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) - ?.content - ?.toModel() - ?.let { PowerLevelsHelper(it) } - powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT) - } - } + aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - commonTestHelper.waitWithLatch { - aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) - it.countDown() - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true - } + commonTestHelper.retryPeriodically { + bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true } } @@ -616,7 +520,6 @@ class SpaceHierarchyTest : InstrumentedTest { val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val spaceAInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceA", listOf( Triple("A1", true /*auto-join*/, true/*canonical*/), @@ -625,7 +528,6 @@ class SpaceHierarchyTest : InstrumentedTest { ) val spaceBInfo = createPublicSpace( - commonTestHelper, aliceSession, "SpaceB", listOf( Triple("B1", true /*auto-join*/, true/*canonical*/), @@ -641,51 +543,39 @@ class SpaceHierarchyTest : InstrumentedTest { val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId) val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId) - commonTestHelper.runBlockingTest { - spaceA!!.addChildren(B1roomId, viaServers, null, true) + spaceA!!.addChildren(B1roomId, viaServers, null, true) + + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) } val newAName = "FooBar" - commonTestHelper.runBlockingTest { - spaceA!!.asRoom().stateService().updateName(newAName) + spaceA.asRoom().stateService().updateName(newAName) + + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(B1roomId) + roomSummary != null && + roomSummary.directParentNames.size == 2 && + roomSummary.directParentNames.contains(newAName) && + roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) } - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(B1roomId) - roomSummary != null && - roomSummary.directParentNames.size == 2 && - roomSummary.directParentNames.contains(newAName) && - roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name) - } - } - - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) - roomSummary != null && - roomSummary.directParentNames.size == 1 && - roomSummary.directParentNames.contains(newAName) - } + commonTestHelper.retryPeriodically { + val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first()) + roomSummary != null && + roomSummary.directParentNames.size == 1 && + roomSummary.directParentNames.contains(newAName) } } }