Instrumentation test coroutines (#7207)

Converting SDK instrumentation tests from CountdownLatch to suspending functions
This commit is contained in:
Adam Brown 2022-09-27 13:37:23 +01:00 committed by GitHub
parent a422361872
commit fad02062d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1488 additions and 2039 deletions

1
changelog.d/7207.sdk Normal file
View file

@ -0,0 +1 @@
Ports SDK instrumentation tests to use suspending functions instead of countdown latches

View file

@ -221,6 +221,8 @@ dependencies {
androidTestImplementation libs.mockk.mockkAndroid androidTestImplementation libs.mockk.mockkAndroid
androidTestImplementation libs.androidx.coreTesting androidTestImplementation libs.androidx.coreTesting
androidTestImplementation libs.jetbrains.coroutinesAndroid androidTestImplementation libs.jetbrains.coroutinesAndroid
androidTestImplementation libs.jetbrains.coroutinesTest
// Plant Timber tree for test // Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule androidTestImplementation libs.tests.timberJunitRule

View file

@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Change password // 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 // Try to login with the previous password, it will fail
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)

View file

@ -44,22 +44,20 @@ class DeactivateAccountTest : InstrumentedTest {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Deactivate the account // Deactivate the account
commonTestHelper.runBlockingTest { session.accountService().deactivateAccount(
session.accountService().deactivateAccount( eraseAllData = false,
eraseAllData = false, userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { promise.resume(
promise.resume( UserPasswordAuth(
UserPasswordAuth( user = session.myUserId,
user = session.myUserId, password = TestConstants.PASSWORD,
password = TestConstants.PASSWORD, session = flowResponse.session
session = flowResponse.session )
) )
)
}
} }
) }
} )
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) 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) // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE)
val hs = commonTestHelper.createHomeServerConfig() val hs = commonTestHelper.createHomeServerConfig()
commonTestHelper.runBlockingTest { commonTestHelper.matrix.authenticationService.getLoginFlow(hs)
commonTestHelper.matrix.authenticationService.getLoginFlow(hs)
}
var accountCreationError: Throwable? = null var accountCreationError: Throwable? = null
commonTestHelper.runBlockingTest { try {
try { commonTestHelper.matrix.authenticationService
commonTestHelper.matrix.authenticationService .getRegistrationWizard()
.getRegistrationWizard() .createAccount(
.createAccount( session.myUserId.substringAfter("@").substringBefore(":"),
session.myUserId.substringAfter("@").substringBefore(":"), TestConstants.PASSWORD,
TestConstants.PASSWORD, null
null )
) } catch (failure: Throwable) {
} catch (failure: Throwable) { accountCreationError = failure
accountCreationError = failure
}
} }
// Test the error // Test the error

View file

@ -19,18 +19,16 @@ package org.matrix.android.sdk.common
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer
import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking 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.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.SyncState
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/** /**
* This class exposes methods to be used in common cases * 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) { class CommonTestHelper internal constructor(context: Context) {
companion object { 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) val testHelper = CommonTestHelper(context)
return try { return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
block(testHelper) try {
} finally { withContext(Dispatchers.Default) {
if (autoSignoutOnClose) { block(testHelper)
testHelper.cleanUpOpenedSessions() }
} 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 testHelper = CommonTestHelper(context)
val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestHelper = CryptoTestHelper(testHelper)
return try { return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
block(cryptoTestHelper, testHelper) try {
} finally { withContext(Dispatchers.Default) {
if (autoSignoutOnClose) { block(cryptoTestHelper, testHelper)
testHelper.cleanUpOpenedSessions() }
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
}
} }
} }
} }
} }
internal val matrix: TestMatrix internal val matrix: TestMatrix
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var accountNumber = 0 private var accountNumber = 0
private val trackedSessions = mutableListOf<Session>() private val trackedSessions = mutableListOf<Session>()
@ -112,19 +120,17 @@ class CommonTestHelper internal constructor(context: Context) {
matrix = _matrix!! matrix = _matrix!!
} }
fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session { suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams) 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) return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
} }
fun cleanUpOpenedSessions() { suspend fun cleanUpOpenedSessions() {
trackedSessions.forEach { trackedSessions.forEach {
runBlockingTest { it.signOutService().signOut(true)
it.signOutService().signOut(true)
}
} }
trackedSessions.clear() trackedSessions.clear()
} }
@ -138,27 +144,10 @@ class CommonTestHelper internal constructor(context: Context) {
.build() .build()
} }
/** suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) {
* This methods init the event stream and check for initial sync session.syncService().startSync(true)
* val syncLiveData = session.syncService().getSyncStateLive()
* @param session the session to sync syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() }
*/
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<SyncState> {
override fun onChanged(t: SyncState?) {
if (session.syncService().hasAlreadySynced()) {
lock.countDown()
syncLiveData.removeObserver(this)
}
}
}
syncLiveData.observeForever(syncObserver)
}
await(lock, timeout)
} }
/** /**
@ -166,22 +155,11 @@ class CommonTestHelper internal constructor(context: Context) {
* *
* @param session the session to sync * @param session the session to sync
*/ */
fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) { suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) {
waitWithLatch(timeout) { latch -> session.clearCache()
session.clearCache() syncSession(session, timeout)
val syncLiveData = session.syncService().getSyncStateLive() session.syncService().getSyncStateLive().first(timeout) { session.syncService().hasAlreadySynced() }
val syncObserver = object : Observer<SyncState> { Timber.v("Clear cache and synced")
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)
}
} }
/** /**
@ -191,7 +169,7 @@ class CommonTestHelper internal constructor(context: Context) {
* @param message the message to send * @param message the message to send
* @param nbOfMessages the number of time the message will be sent * @param nbOfMessages the number of time the message will be sent
*/ */
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> { suspend fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) val timeline = room.timelineService().createTimeline(null, TimelineSettings(10))
timeline.start() timeline.start()
val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) 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 * 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<TimelineEvent> { private suspend fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
val sentEvents = ArrayList<TimelineEvent>(count) val sentEvents = ArrayList<TimelineEvent>(count)
(1 until count + 1) (1 until count + 1)
.map { "$message #$it" } .map { "$message #$it" }
.chunked(10) .chunked(10)
.forEach { batchedMessages -> .forEach { batchedMessages ->
batchedMessages.forEach { formattedMessage -> waitFor(
if (rootThreadEventId != null) { continueWhen = {
room.relationService().replyInThread( wrapWithTimeout(timeout) {
rootThreadEventId = rootThreadEventId, suspendCoroutine<Unit> { continuation ->
replyInThreadText = formattedMessage val timelineListener = object : Timeline.Listener {
)
} else {
room.sendService().sendTextMessage(formattedMessage)
}
}
waitWithLatch(timeout) { latch ->
val timelineListener = object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val allSentMessages = snapshot val allSentMessages = snapshot
.filter { it.root.sendState == SendState.SYNCED } .filter { it.root.sendState == SendState.SYNCED }
.filter { it.root.getClearType() == EventType.MESSAGE } .filter { it.root.getClearType() == EventType.MESSAGE }
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true } .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
val hasSyncedAllBatchedMessages = allSentMessages val hasSyncedAllBatchedMessages = allSentMessages
.map { .map {
it.root.getClearContent().toModel<MessageContent>()?.body it.root.getClearContent().toModel<MessageContent>()?.body
}
.containsAll(batchedMessages)
if (allSentMessages.size == count) {
sentEvents.addAll(allSentMessages)
}
if (hasSyncedAllBatchedMessages) {
timeline.removeListener(this)
continuation.resume(Unit)
}
}
} }
.containsAll(batchedMessages) timeline.addListener(timelineListener)
}
if (allSentMessages.size == count) {
sentEvents.addAll(allSentMessages)
} }
if (hasSyncedAllBatchedMessages) { },
timeline.removeListener(this) action = {
latch.countDown() batchedMessages.forEach { formattedMessage ->
if (rootThreadEventId != null) {
room.relationService().replyInThread(
rootThreadEventId = rootThreadEventId,
replyInThreadText = formattedMessage
)
} else {
room.sendService().sendTextMessage(formattedMessage)
}
} }
} }
} )
timeline.addListener(timelineListener)
}
} }
return sentEvents return sentEvents
} }
fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) { suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
waitWithLatch { latch -> retryPeriodically {
retryPeriodicallyWithLatch(latch) { val roomSummary = otherSession.getRoomSummary(roomID)
val roomSummary = otherSession.getRoomSummary(roomID) (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
(roomSummary != null && roomSummary.membership == Membership.INVITE).also { if (it) {
if (it) { Log.v("# TEST", "${otherSession.myUserId} can see the invite")
Log.v("# TEST", "${otherSession.myUserId} can see the invite")
}
} }
} }
} }
// not sure why it's taking so long :/ // 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") Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
try { try {
otherSession.roomService().joinRoom(roomID) otherSession.roomService().joinRoom(roomID)
@ -273,11 +257,9 @@ class CommonTestHelper internal constructor(context: Context) {
} }
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
waitWithLatch { retryPeriodically {
retryPeriodicallyWithLatch(it) { val roomSummary = otherSession.getRoomSummary(roomID)
val roomSummary = otherSession.getRoomSummary(roomID) roomSummary != null && roomSummary.membership == Membership.JOIN
roomSummary != null && roomSummary.membership == Membership.JOIN
}
} }
} }
@ -287,7 +269,7 @@ class CommonTestHelper internal constructor(context: Context) {
* @param message the message to send * @param message the message to send
* @param numberOfMessages the number of time the message will be sent * @param numberOfMessages the number of time the message will be sent
*/ */
fun replyInThreadMessage( suspend fun replyInThreadMessage(
room: Room, room: Room,
message: String, message: String,
numberOfMessages: Int, numberOfMessages: Int,
@ -305,15 +287,7 @@ class CommonTestHelper internal constructor(context: Context) {
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
/** private suspend fun createAccount(
* 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(
userNamePrefix: String, userNamePrefix: String,
password: String, password: String,
testParams: SessionTestParams testParams: SessionTestParams
@ -331,15 +305,7 @@ class CommonTestHelper internal constructor(context: Context) {
} }
} }
/** suspend fun logIntoAccount(
* 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(
userId: String, userId: String,
password: String, password: String,
testParams: SessionTestParams testParams: SessionTestParams
@ -351,32 +317,25 @@ class CommonTestHelper internal constructor(context: Context) {
} }
} }
/** private suspend fun createAccountAndSync(
* 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(
userName: String, userName: String,
password: String, password: String,
sessionTestParams: SessionTestParams sessionTestParams: SessionTestParams
): Session { ): Session {
val hs = createHomeServerConfig() val hs = createHomeServerConfig()
runBlockingTest { wrapWithTimeout(TestConstants.timeOutMillis) {
matrix.authenticationService.getLoginFlow(hs) matrix.authenticationService.getLoginFlow(hs)
} }
runBlockingTest(timeout = 60_000) { wrapWithTimeout(60_000L) {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()
.createAccount(userName, password, null) .createAccount(userName, password, null)
} }
// Perform dummy step // Perform dummy step
val registrationResult = runBlockingTest(timeout = 60_000) { val registrationResult = wrapWithTimeout(timeout = 60_000) {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()
.dummy() .dummy()
@ -391,29 +350,14 @@ class CommonTestHelper internal constructor(context: Context) {
return session return session
} }
/** private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): 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 {
val hs = createHomeServerConfig() val hs = createHomeServerConfig()
runBlockingTest { matrix.authenticationService.getLoginFlow(hs)
matrix.authenticationService.getLoginFlow(hs)
}
val session = runBlockingTest { val session = matrix.authenticationService
matrix.authenticationService .getLoginWizard()
.getLoginWizard() .login(userName, password, "myDevice")
.login(userName, password, "myDevice")
}
session.open() session.open()
if (sessionTestParams.withInitialSync) { if (sessionTestParams.withInitialSync) {
syncSession(session) syncSession(session)
@ -428,25 +372,21 @@ class CommonTestHelper internal constructor(context: Context) {
* @param userName the account username * @param userName the account username
* @param password the password * @param password the password
*/ */
fun logAccountWithError( suspend fun logAccountWithError(
userName: String, userName: String,
password: String password: String
): Throwable { ): Throwable {
val hs = createHomeServerConfig() val hs = createHomeServerConfig()
runBlockingTest { matrix.authenticationService.getLoginFlow(hs)
matrix.authenticationService.getLoginFlow(hs)
}
var requestFailure: Throwable? = null var requestFailure: Throwable? = null
runBlockingTest { try {
try { matrix.authenticationService
matrix.authenticationService .getLoginWizard()
.getLoginWizard() .login(userName, password, "myDevice")
.login(userName, password, "myDevice") } catch (failure: Throwable) {
} catch (failure: Throwable) { requestFailure = failure
requestFailure = failure
}
} }
assertNotNull(requestFailure) assertNotNull(requestFailure)
@ -482,65 +422,48 @@ class CommonTestHelper internal constructor(context: Context) {
) )
} }
suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) {
while (true) { wrapWithTimeout(timeout) {
try { while (!predicate()) {
delay(1000) runBlocking { delay(500) }
} catch (ex: CancellationException) {
// the job was canceled, just stop
return
}
if (condition()) {
latch.countDown()
return
} }
} }
} }
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) { suspend fun <T> waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
val latch = CountDownLatch(1) return wrapWithTimeout(timeout) {
val job = coroutineScope.launch(dispatcher) { suspendCoroutine { continuation ->
block(latch) val callback = object : MatrixCallback<T> {
} override fun onSuccess(data: T) {
await(latch, timeout, job) continuation.resume(data)
} }
}
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T { block(callback)
return runBlocking {
withTimeout(timeout) {
block()
} }
} }
} }
// Transform a method with a MatrixCallback to a synchronous method suspend fun <T> waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): Throwable {
inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T { return wrapWithTimeout(timeout) {
val lock = CountDownLatch(1) suspendCoroutine { continuation ->
var result: T? = null val callback = object : MatrixCallback<T> {
override fun onFailure(failure: Throwable) {
val callback = object : TestMatrixCallback<T>(lock) { continuation.resume(failure)
override fun onSuccess(data: T) { }
result = data }
super.onSuccess(data) block(callback)
} }
} }
block.invoke(callback)
await(lock, timeout)
assertNotNull(result)
return result!!
} }
/** /**
* Clear all provided sessions * Clear all provided sessions
*/ */
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } suspend fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) { suspend fun signOutAndClose(session: Session) {
trackedSessions.remove(session) trackedSessions.remove(session)
runBlockingTest(timeout = 60_000) { wrapWithTimeout(timeout = 60_000L) {
session.signOutService().signOut(true) session.signOutService().signOut(true)
} }
// no need signout will close // no need signout will close

View file

@ -32,7 +32,7 @@ data class CryptoTestData(
val thirdSession: Session? val thirdSession: Session?
get() = sessions.getOrNull(2) get() = sessions.getOrNull(2)
fun cleanUp(testHelper: CommonTestHelper) { suspend fun cleanUp(testHelper: CommonTestHelper) {
sessions.forEach { sessions.forEach {
testHelper.signOutAndClose(it) testHelper.signOutAndClose(it)
} }

View file

@ -16,19 +16,15 @@
package org.matrix.android.sdk.common package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer
import org.amshove.kluent.fail import org.amshove.kluent.fail
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse 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.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod 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.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.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.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility 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.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent 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.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
import org.matrix.android.sdk.api.session.securestorage.KeyRef 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 org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID import java.util.UUID
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/** /**
* @return alice session * @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 aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = testHelper.runBlockingTest { val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
aliceSession.roomService().createRoom(CreateRoomParams().apply { historyVisibility = roomHistoryVisibility
historyVisibility = roomHistoryVisibility name = "MyRoom"
name = "MyRoom" })
})
}
if (encryptedRoom) { if (encryptedRoom) {
testHelper.waitWithLatch { latch -> val room = aliceSession.getRoom(roomId)!!
val room = aliceSession.getRoom(roomId)!! waitFor(
room.roomCryptoService().enableEncryption() continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } },
val roomSummaryLive = room.getRoomSummaryLive() action = { room.roomCryptoService().enableEncryption() }
val roomSummaryObserver = object : Observer<Optional<RoomSummary>> { )
override fun onChanged(roomSummary: Optional<RoomSummary>) {
if (roomSummary.getOrNull()?.isEncrypted.orFalse()) {
roomSummaryLive.removeObserver(this)
latch.countDown()
}
}
}
roomSummaryLive.observeForever(roomSummaryObserver)
}
} }
return CryptoTestData(roomId, listOf(aliceSession)) return CryptoTestData(roomId, listOf(aliceSession))
} }
@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/** /**
* @return alice and bob sessions * @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 cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId val aliceRoomId = cryptoTestData.roomId
@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
testHelper.waitWithLatch { latch -> waitFor(
val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } },
val newRoomObserver = object : Observer<List<RoomSummary>> { action = { aliceRoom.membershipService().invite(bobSession.myUserId) }
override fun onChanged(t: List<RoomSummary>?) { )
if (t?.isNotEmpty() == true) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
aliceRoom.membershipService().invite(bobSession.myUserId)
}
testHelper.waitWithLatch { latch -> waitFor(
val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) continueWhen = {
val roomJoinedObserver = object : Observer<List<RoomSummary>> { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first {
override fun onChanged(t: List<RoomSummary>?) { bobSession.getRoom(aliceRoomId)
if (bobSession.getRoom(aliceRoomId) ?.membershipService()
?.membershipService() ?.getRoomMember(bobSession.myUserId)
?.getRoomMember(bobSession.myUserId) ?.membership == Membership.JOIN
?.membership == Membership.JOIN) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
} }
} },
} action = { bobSession.roomService().joinRoom(aliceRoomId) }
bobRoomSummariesLive.observeForever(roomJoinedObserver) )
bobSession.roomService().joinRoom(aliceRoomId)
}
// Ensure bob can send messages to the room // Ensure bob can send messages to the room
// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! // val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
// assertNotNull(roomFromBobPOV.powerLevels) // assertNotNull(roomFromBobPOV.powerLevels)
@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) 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 * @return Alice and Bob sessions
*/ */
fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData { suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceAndBobInARoom() val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId val aliceRoomId = cryptoTestData.roomId
@ -235,49 +165,20 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return cryptoTestData return cryptoTestData
} }
private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) { private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId) if (andCanDecrypt) {
if (andCanDecrypt) { timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE } else {
} else { timeLineEvent != null
timeLineEvent != null
}
} }
} }
} }
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) { private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
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 {
return MegolmBackupAuthData( return MegolmBackupAuthData(
publicKey = "abcdefg", publicKey = "abcdefg",
signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop")) 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 { suspend fun createDM(alice: Session, bob: Session): String {
var roomId: String = "" var roomId = ""
testHelper.waitWithLatch { latch -> waitFor(
roomId = alice.roomService().createDirectRoom(bob.myUserId) continueWhen = {
val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { }) bob.roomService()
val newRoomObserver = object : Observer<List<RoomSummary>> { .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }
override fun onChanged(t: List<RoomSummary>?) { .first { it.any { it.roomId == roomId }.orFalse() }
if (t?.any { it.roomId == roomId }.orFalse()) { },
bobRoomSummariesLive.removeObserver(this) action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) }
latch.countDown() )
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
}
testHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (bob.getRoom(roomId)
?.membershipService()
?.getRoomMember(bob.myUserId)
?.membership == Membership.JOIN) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
bob.roomService().joinRoom(roomId)
}
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 return roomId
} }
fun initializeCrossSigning(session: Session) { suspend fun initializeCrossSigning(session: Session) {
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
session.cryptoService().crossSigningService() session.cryptoService().crossSigningService()
.initializeCrossSigning( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor { object : UserInteractiveAuthInterceptor {
@ -350,57 +242,55 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/** /**
* Initialize cross-signing, set up megolm backup and save all in 4S * Initialize cross-signing, set up megolm backup and save all in 4S
*/ */
fun bootstrapSecurity(session: Session) { suspend fun bootstrapSecurity(session: Session) {
initializeCrossSigning(session) initializeCrossSigning(session)
val ssssService = session.sharedSecretStorageService() val ssssService = session.sharedSecretStorageService()
testHelper.runBlockingTest { val keyInfo = ssssService.generateKey(
val keyInfo = ssssService.generateKey( UUID.randomUUID().toString(),
UUID.randomUUID().toString(), null,
null, "ssss_key",
"ssss_key", EmptyKeySigner()
EmptyKeySigner() )
) ssssService.setDefaultKey(keyInfo.keyId)
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<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = testHelper.waitForCallback<KeysVersion> {
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( ssssService.storeSecret(
MASTER_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, secret,
listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec)) 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<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
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(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
@ -415,12 +305,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
roomId = roomId roomId = roomId
).transactionId ).transactionId
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { it.requestInfo?.fromDevice == alice.sessionParams.deviceId
it.requestInfo?.fromDevice == alice.sessionParams.deviceId } != null
} != null
}
} }
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId it.requestInfo?.fromDevice == alice.sessionParams.deviceId
@ -429,16 +317,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
var requestID: String? = null var requestID: String? = null
// wait for it to be readied // wait for it to be readied
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) .firstOrNull { it.localId == localId }
.firstOrNull { it.localId == localId } if (outgoingRequest?.isReady == true) {
if (outgoingRequest?.isReady == true) { requestID = outgoingRequest.transactionId!!
requestID = outgoingRequest.transactionId!! true
true } else {
} else { false
false
}
} }
} }
@ -454,23 +340,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
var alicePovTx: OutgoingSasVerificationTransaction? = null var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null var bobPovTx: IncomingSasVerificationTransaction? = null
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") alicePovTx?.state == VerificationTxState.ShortCodeReady
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
} }
// wait for alice to get the ready // wait for alice to get the ready
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") if (bobPovTx?.state == VerificationTxState.OnStarted) {
if (bobPovTx?.state == VerificationTxState.OnStarted) { bobPovTx?.performAccept()
bobPovTx?.performAccept()
}
bobPovTx?.state == VerificationTxState.ShortCodeReady
} }
bobPovTx?.state == VerificationTxState.ShortCodeReady
} }
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation()) assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
@ -478,38 +360,30 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
bobPovTx!!.userHasVerifiedShortCode() bobPovTx!!.userHasVerifiedShortCode()
alicePovTx!!.userHasVerifiedShortCode() alicePovTx!!.userHasVerifiedShortCode()
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
} }
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
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) val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
aliceSession.cryptoService().setWarnOnUnknownDevices(false) aliceSession.cryptoService().setWarnOnUnknownDevices(false)
val roomId = testHelper.runBlockingTest { val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
}
val room = aliceSession.getRoom(roomId)!! val room = aliceSession.getRoom(roomId)!!
testHelper.runBlockingTest { room.roomCryptoService().enableEncryption()
room.roomCryptoService().enableEncryption()
}
val sessions = mutableListOf(aliceSession) val sessions = mutableListOf(aliceSession)
for (index in 1 until numberOfMembers) { for (index in 1 until numberOfMembers) {
val session = testHelper.createAccount("User_$index", defaultSessionParams) 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") 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") println("TEST -> " + session.myUserId + " joined")
sessions.add(session) sessions.add(session)
} }
@ -517,47 +391,41 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return CryptoTestData(roomId, sessions) return CryptoTestData(roomId, sessions)
} }
fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) { suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId -> sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root try {
testHelper.runBlockingTest { session.cryptoService().decryptEvent(event, "").let { result ->
try { event.mxDecryptionResult = OlmDecryptionResult(
session.cryptoService().decryptEvent(event, "").let { result -> payload = result.clearEvent,
event.mxDecryptionResult = OlmDecryptionResult( senderKey = result.senderCurve25519Key,
payload = result.clearEvent, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
senderKey = result.senderCurve25519Key, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, )
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} catch (error: MXCryptoError) {
// nop
}
} }
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") } catch (error: MXCryptoError) {
event.getClearType() == EventType.MESSAGE && // nop
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
} }
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
event.getClearType() == EventType.MESSAGE &&
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
} }
} }
} }
fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { suspend fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest { try {
try { session.cryptoService().decryptEvent(event, "")
session.cryptoService().decryptEvent(event, "") fail("Should not be able to decrypt event")
fail("Should not be able to decrypt event") } catch (error: MXCryptoError) {
} catch (error: MXCryptoError) { val errorType = (error as? MXCryptoError.Base)?.errorType
val errorType = (error as? MXCryptoError.Base)?.errorType if (expectedError == null) {
if (expectedError == null) { assertNotNull(errorType)
assertNotNull(errorType) } else {
} else { assertEquals("Unexpected reason", expectedError, errorType)
assertEquals("Unexpected reason", expectedError, errorType)
}
} }
} }
} }

View file

@ -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, R> T.onMain(block: T.() -> R): R {
return withContext(Dispatchers.Main) {
block(this@onMain)
}
}
suspend fun <T> LiveData<T>.first(timeout: Long = TestConstants.timeOutMillis, predicate: (T) -> Boolean): T {
return wrapWithTimeout(timeout) {
withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(data: T) {
if (predicate(data)) {
removeObserver(this)
continuation.resume(data)
}
}
}
observeForever(observer)
continuation.invokeOnCancellation { removeObserver(observer) }
}
}
}
}
suspend fun <T> waitFor(continueWhen: suspend () -> T, action: suspend () -> Unit) {
coroutineScope {
val deferred = async { continueWhen() }
action()
deferred.await()
}
}
suspend fun <T> wrapWithTimeout(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
val deferred = coroutineScope {
async { block() }
}
return withTimeout(timeout) { deferred.await() }
}

View file

@ -46,30 +46,26 @@ class DecryptRedactedEventTest : InstrumentedTest {
roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason) roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
// get the event from bob // get the event from bob
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
}
} }
val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!! val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
testHelper.runBlockingTest { try {
try { val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "") Assert.assertEquals(
Assert.assertEquals( "Unexpected redacted reason",
"Unexpected redacted reason", redactionReason,
redactionReason, result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason") )
) Assert.assertEquals(
Assert.assertEquals( "Unexpected Redacted event id",
"Unexpected Redacted event id", timelineEvent.eventId,
timelineEvent.eventId, result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts )
) } catch (failure: Throwable) {
} catch (failure: Throwable) { Assert.fail("Should not throw when decrypting a redacted event")
Assert.fail("Should not throw when decrypting a redacted event")
}
} }
} }
} }

View file

@ -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.CryptoTestData
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -57,18 +56,14 @@ class E2EShareKeysConfigTest : InstrumentedTest {
fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
aliceSession.cryptoService().enableShareKeyOnInvite(false) aliceSession.cryptoService().enableShareKeyOnInvite(false)
val roomId = commonTestHelper.runBlockingTest { val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
aliceSession.roomService().createRoom(CreateRoomParams().apply { historyVisibility = RoomHistoryVisibility.SHARED
historyVisibility = RoomHistoryVisibility.SHARED name = "MyRoom"
name = "MyRoom" enableEncryption()
enableEncryption() })
})
}
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
} }
val roomAlice = aliceSession.roomService().getRoom(roomId)!! val roomAlice = aliceSession.roomService().getRoom(roomId)!!
@ -81,9 +76,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true)) val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
// Let alice invite bob // Let alice invite bob
commonTestHelper.runBlockingTest { roomAlice.membershipService().invite(bobSession.myUserId)
roomAlice.membershipService().invite(bobSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId) commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId)
@ -114,9 +107,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam // Let alice invite sam
commonTestHelper.runBlockingTest { roomAlice.membershipService().invite(samSession.myUserId)
roomAlice.membershipService().invite(samSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
@ -135,7 +126,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
} }
@Test @Test
fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
val aliceSession = testData.firstSession.also { val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(false) it.cryptoService().enableShareKeyOnInvite(false)
@ -162,7 +153,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
} }
@Test @Test
fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
val aliceSession = testData.firstSession.also { val aliceSession = testData.firstSession.also {
it.cryptoService().enableShareKeyOnInvite(true) it.cryptoService().enableShareKeyOnInvite(true)
@ -186,7 +177,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
fromBobSharable.map { it.root.getClearContent()?.get("body") as String }) fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
} }
private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> { private suspend fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1) val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1)
val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 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)) val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let bob invite sam // 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) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId)
return Triple(fromAliceNotSharable, fromBobSharable, samSession) return Triple(fromAliceNotSharable, fromBobSharable, samSession)
@ -209,18 +198,14 @@ class E2EShareKeysConfigTest : InstrumentedTest {
fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
aliceSession.cryptoService().enableShareKeyOnInvite(false) aliceSession.cryptoService().enableShareKeyOnInvite(false)
val roomId = commonTestHelper.runBlockingTest { val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
aliceSession.roomService().createRoom(CreateRoomParams().apply { historyVisibility = RoomHistoryVisibility.SHARED
historyVisibility = RoomHistoryVisibility.SHARED name = "MyRoom"
name = "MyRoom" enableEncryption()
enableEncryption() })
})
}
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
}
} }
val roomAlice = aliceSession.roomService().getRoom(roomId)!! val roomAlice = aliceSession.roomService().getRoom(roomId)!!
@ -232,18 +217,15 @@ class E2EShareKeysConfigTest : InstrumentedTest {
Log.v("#E2E TEST", "Create and start key backup for bob ...") Log.v("#E2E TEST", "Create and start key backup for bob ...")
val keysBackupService = aliceSession.cryptoService().keysBackupService() val keysBackupService = aliceSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz" val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> { val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
} }
val version = commonTestHelper.doSync<KeysVersion> { val version = commonTestHelper.waitForCallback<KeysVersion> {
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
} }
commonTestHelper.waitWithLatch { latch -> commonTestHelper.waitForCallback<Unit> {
keysBackupService.backupAllGroupSessions( keysBackupService.backupAllGroupSessions(null, it)
null,
TestMatrixCallback(latch, true)
)
} }
// signout // signout
@ -253,11 +235,11 @@ class E2EShareKeysConfigTest : InstrumentedTest {
newAliceSession.cryptoService().enableShareKeyOnInvite(true) newAliceSession.cryptoService().enableShareKeyOnInvite(true)
newAliceSession.cryptoService().keysBackupService().let { kbs -> newAliceSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> { val keyVersionResult = commonTestHelper.waitForCallback<KeysVersionResult?> {
kbs.getVersion(version.version, it) kbs.getVersion(version.version, it)
} }
val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> { val importedResult = commonTestHelper.waitForCallback<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword( kbs.restoreKeyBackupWithPassword(
keyVersionResult!!, keyVersionResult!!,
keyBackupPassword, keyBackupPassword,
@ -276,9 +258,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true)) val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
// Let alice invite sam // Let alice invite sam
commonTestHelper.runBlockingTest { newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
}
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId) commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)

View file

@ -18,12 +18,16 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log import android.util.Log
import androidx.test.filters.LargeTest 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.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.fail import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 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
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest 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.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail 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.") // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@ -70,8 +71,6 @@ import java.util.concurrent.CountDownLatch
@LargeTest @LargeTest
class E2eeSanityTests : InstrumentedTest { class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
/** /**
* Simple test that create an e2ee room. * Simple test that create an e2ee room.
* Some new members are added, and a message is sent. * Some new members are added, and a message is sent.
@ -104,10 +103,8 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "All accounts created") Log.v("#E2E TEST", "All accounts created")
// we want to invite them in the room // we want to invite them in the room
otherAccounts.forEach { otherAccounts.forEach {
testHelper.runBlockingTest { Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
Log.v("#E2E TEST", "Alice invites ${it.myUserId}") aliceRoomPOV.membershipService().invite(it.myUserId)
aliceRoomPOV.membershipService().invite(it.myUserId)
}
} }
// All user should accept invite // All user should accept invite
@ -129,13 +126,11 @@ class E2eeSanityTests : InstrumentedTest {
// All should be able to decrypt // All should be able to decrypt
otherAccounts.forEach { otherSession -> otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
} }
} }
@ -146,10 +141,8 @@ class E2eeSanityTests : InstrumentedTest {
} }
newAccount.forEach { newAccount.forEach {
testHelper.runBlockingTest { Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
Log.v("#E2E TEST", "Alice invites ${it.myUserId}") aliceRoomPOV.membershipService().invite(it.myUserId)
aliceRoomPOV.membershipService().invite(it.myUserId)
}
} }
newAccount.forEach { newAccount.forEach {
@ -159,21 +152,17 @@ class E2eeSanityTests : InstrumentedTest {
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit // wait a bit
testHelper.runBlockingTest { delay(3_000)
delay(3_000)
}
// check that messages are encrypted (uisi) // check that messages are encrypted (uisi)
newAccount.forEach { otherSession -> newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also { Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
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
} }
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 // new members should be able to decrypt it
newAccount.forEach { otherSession -> newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also { Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
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<MessageContent>()?.body
} }
timelineEvent != null &&
timelineEvent.root.getClearType() == EventType.MESSAGE &&
secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
} }
} }
} }
@ -229,10 +216,10 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Create and start key backup for bob ...") Log.v("#E2E TEST", "Create and start key backup for bob ...")
val bobKeysBackupService = bobSession.cryptoService().keysBackupService() val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz" val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it) bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
} }
val version = testHelper.doSync<KeysVersion> { val version = testHelper.waitForCallback<KeysVersion> {
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it) bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
} }
Log.v("#E2E TEST", "... Key backup started and enabled for bob") Log.v("#E2E TEST", "... Key backup started and enabled for bob")
@ -248,32 +235,21 @@ class E2eeSanityTests : InstrumentedTest {
sentEventIds.add(it) sentEventIds.add(it)
} }
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
} }
// we want more so let's discard the session // we want more so let's discard the session
aliceSession.cryptoService().discardOutboundSession(e2eRoomID) aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
testHelper.runBlockingTest {
delay(1_000)
}
} }
Log.v("#E2E TEST", "Bob received all and can decrypt") 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 // Let's wait a bit to be sure that bob has backed up the session
Log.v("#E2E TEST", "Force key backup for Bob...") Log.v("#E2E TEST", "Force key backup for Bob...")
testHelper.waitWithLatch { latch -> testHelper.waitForCallback<Unit> { bobKeysBackupService.backupAllGroupSessions(null, it) }
bobKeysBackupService.backupAllGroupSessions(
null,
TestMatrixCallback(latch, true)
)
}
Log.v("#E2E TEST", "... Key backup done for Bob") 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 // 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) testHelper.signOutAndClose(bobSession)
Log.v("#E2E TEST", "..Logout alice and bob...") Log.v("#E2E TEST", "..Logout alice and bob...")
testHelper.runBlockingTest { delay(1_000)
delay(1_000)
}
// Create a new session for bob // Create a new session for bob
Log.v("#E2E TEST", "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 // check that bob can't currently decrypt
Log.v("#E2E TEST", "check that bob can't currently decrypt") Log.v("#E2E TEST", "check that bob can't currently decrypt")
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also { Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
}
timelineEvent != null &&
timelineEvent.root.getClearType() == EventType.ENCRYPTED
} }
timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED
} }
} }
// after initial sync events are not decrypted, so we have to try manually // 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 // Let's now import keys from backup
newBobSession.cryptoService().keysBackupService().let { kbs -> newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.doSync<KeysVersionResult?> { val keyVersionResult = testHelper.waitForCallback<KeysVersionResult?> {
kbs.getVersion(version.version, it) kbs.getVersion(version.version, it)
} }
val importedResult = testHelper.doSync<ImportRoomKeysResult> { val importedResult = testHelper.waitForCallback<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword( kbs.restoreKeyBackupWithPassword(
keyVersionResult!!, keyVersionResult!!,
keyBackupPassword, keyBackupPassword,
@ -357,13 +328,11 @@ class E2eeSanityTests : InstrumentedTest {
sentEventIds.add(it) sentEventIds.add(it)
} }
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
} }
} }
@ -399,22 +368,20 @@ class E2eeSanityTests : InstrumentedTest {
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
.getTimelineEvent(sentEventId)!! .getTimelineEvent(sentEventId)!!
.root.content.toModel<EncryptedEventContent>()!!.sessionId .root.content.toModel<EncryptedEventContent>()!!.sessionId
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() .first {
.first { it.sessionId == megolmSessionId &&
it.sessionId == megolmSessionId && it.roomId == e2eRoomID
it.roomId == e2eRoomID }
} .results.also {
.results.also { Log.w("##TEST", "result list is $it")
Log.w("##TEST", "result list is $it") }
} .firstOrNull { it.userId == aliceSession.myUserId }
.firstOrNull { it.userId == aliceSession.myUserId } ?.result
?.result aliceReply != null &&
aliceReply != null && aliceReply is RequestResult.Failure &&
aliceReply is RequestResult.Failure && WithHeldCode.UNAUTHORISED == aliceReply.code
WithHeldCode.UNAUTHORISED == aliceReply.code
}
} }
} }
@ -455,13 +422,11 @@ class E2eeSanityTests : InstrumentedTest {
firstMessage.let { text -> firstMessage.let { text ->
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
} }
} }
@ -483,13 +448,11 @@ class E2eeSanityTests : InstrumentedTest {
secondMessage.let { text -> secondMessage.let { text ->
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) timeLineEvent != null &&
timeLineEvent != null && timeLineEvent.isEncrypted() &&
timeLineEvent.isEncrypted() && timeLineEvent.root.getClearType() == EventType.MESSAGE
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
} }
} }
@ -503,18 +466,14 @@ class E2eeSanityTests : InstrumentedTest {
Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId) Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
// Confirm we can decrypt one but not the other // Confirm we can decrypt one but not the other
testHelper.runBlockingTest { mustFail(message = "Should not be able to decrypt event") {
mustFail(message = "Should not be able to decrypt event") { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
}
} }
testHelper.runBlockingTest { try {
try { newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") } catch (error: MXCryptoError) {
} catch (error: MXCryptoError) { fail("Should be able to decrypt event")
fail("Should be able to decrypt event")
}
} }
// Now let's verify bobs session, and re-request keys // 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 // old session should have shared the key at earliest known index now
// we should be able to decrypt both // we should be able to decrypt both
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val canDecryptFirst = try {
val canDecryptFirst = try { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
testHelper.runBlockingTest { true
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") } catch (error: MXCryptoError) {
} false
true
} catch (error: MXCryptoError) {
false
}
val canDecryptSecond = try {
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
}
canDecryptFirst && canDecryptSecond
} }
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? { private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendService().sendTextMessage(text)
var sentEventId: String? = null var sentEventId: String? = null
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> aliceRoomPOV.sendService().sendTextMessage(text)
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<MessageContent>()?.body?.startsWith(text) == true }
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
}
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<MessageContent>()?.body?.startsWith(text) == true }
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
} }
timeline.dispose()
return sentEventId return sentEventId
} }
@ -593,106 +544,35 @@ class E2eeSanityTests : InstrumentedTest {
val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val oldCompleteLatch = CountDownLatch(1) val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId)
lateinit var oldCode: String val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId)
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
}
}
})
// initiate self verification // initiate self verification
aliceSession.cryptoService().verificationService().requestKeyVerification( aliceSession.cryptoService().verificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceNewSession.myUserId, aliceNewSession.myUserId,
listOf(aliceNewSession.sessionParams.deviceId!!) listOf(aliceNewSession.sessionParams.deviceId!!)
) )
testHelper.await(oldCompleteLatch)
testHelper.await(newCompleteLatch) val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode)
assertEquals("Decimal code should have matched", oldCode, newCode) assertEquals("Decimal code should have matched", oldCode, newCode)
// Assert that devices are verified // Assert that devices are verified
val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) val newDeviceFromOldPov: CryptoDeviceInfo? =
val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) 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("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) Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
// wait for secret gossiping to happen // wait for secret gossiping to happen
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
}
} }
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
}
} }
assertEquals( assertEquals(
@ -725,27 +605,112 @@ class E2eeSanityTests : InstrumentedTest {
) )
} }
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) { private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
testHelper.waitWithLatch { latch -> return scope.async {
testHelper.retryPeriodicallyWithLatch(latch) { suspendCancellableCoroutine { continuation ->
otherAccounts.map { var oldCode: String? = null
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership val listener = object : VerificationService.Listener {
}.all {
it == Membership.JOIN 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<String>, session: Session, e2eRoomID: String) { private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
testHelper.waitWithLatch { latch -> return scope.async {
sentEventIds.forEach { sentEventId -> suspendCancellableCoroutine { continuation ->
testHelper.retryPeriodicallyWithLatch(latch) { var newCode: String? = null
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null && val listener = object : VerificationService.Listener {
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE 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<Session>, 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<String>, 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
} }
} }
} }

View file

@ -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.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest 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.SessionTestParams
import org.matrix.android.sdk.common.wrapWithTimeout
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -101,15 +101,13 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
// Bob should be able to decrypt the message // Bob should be able to decrypt the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null &&
(timelineEvent != null && timelineEvent.isEncrypted() &&
timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also {
timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) {
if (it) { Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
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") Log.v("#E2E TEST", "Aris user created")
// Alice invites new user to the room // Alice invites new user to the room
testHelper.runBlockingTest { Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}") aliceRoomPOV.membershipService().invite(arisSession.myUserId)
aliceRoomPOV.membershipService().invite(arisSession.myUserId)
}
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper) waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
@ -137,30 +133,26 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
null null
-> { -> {
// Aris should be able to decrypt the message // Aris should be able to decrypt the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null &&
(timelineEvent != null && timelineEvent.isEncrypted() &&
timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE
timelineEvent.root.getClearType() == EventType.MESSAGE ).also {
).also { if (it) {
if (it) { Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
}
} }
} }
} }
} }
RoomHistoryVisibility.INVITED, RoomHistoryVisibility.INVITED,
RoomHistoryVisibility.JOINED -> { RoomHistoryVisibility.JOINED -> {
// Aris should not even be able to get the message // Aris should not even be able to get the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID) ?.timelineService()
?.timelineService() ?.getTimelineEvent(aliceMessageId!!)
?.getTimelineEvent(aliceMessageId!!) timelineEvent == null
timelineEvent == null
}
} }
} }
} }
@ -238,10 +230,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
private fun testRotationDueToVisibilityChange( private fun testRotationDueToVisibilityChange(
initRoomHistoryVisibility: RoomHistoryVisibility, initRoomHistoryVisibility: RoomHistoryVisibility,
nextRoomHistoryVisibility: RoomHistoryVisibilityContent nextRoomHistoryVisibility: RoomHistoryVisibilityContent
) { ) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId val e2eRoomID = cryptoTestData.roomId
@ -267,21 +256,19 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
// Bob should be able to decrypt the message // Bob should be able to decrypt the message
var firstAliceMessageMegolmSessionId: String? = null var firstAliceMessageMegolmSessionId: String? = null
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID) val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = bobRoomPov
val timelineEvent = bobRoomPov ?.timelineService()
?.timelineService() ?.getTimelineEvent(aliceMessageId!!)
?.getTimelineEvent(aliceMessageId!!) (timelineEvent != null &&
(timelineEvent != null && timelineEvent.isEncrypted() &&
timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also {
timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) {
if (it) { firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String Log.v(
Log.v( "#E2E TEST",
"#E2E TEST", "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
"Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}" )
)
}
} }
} }
} }
@ -290,21 +277,19 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
var secondAliceMessageSessionId: String? = null var secondAliceMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage -> sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = bobRoomPov
val timelineEvent = bobRoomPov ?.timelineService()
?.timelineService() ?.getTimelineEvent(secondMessage)
?.getTimelineEvent(secondMessage) (timelineEvent != null &&
(timelineEvent != null && timelineEvent.isEncrypted() &&
timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also {
timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) {
if (it) { secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String Log.v(
Log.v( "#E2E TEST",
"#E2E TEST", "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
"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") Log.v("#E2E TEST ROTATION", "No rotation needed yet")
// Let's change the room history visibility // Let's change the room history visibility
testHelper.runBlockingTest { aliceRoomPOV.stateService()
aliceRoomPOV.stateService() .sendStateEvent(
.sendStateEvent( eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "",
stateKey = "", body = RoomHistoryVisibilityContent(
body = RoomHistoryVisibilityContent( historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr ).toContent()
).toContent() )
)
}
// ensure that the state did synced down // ensure that the state did synced down
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
} }
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
val roomVisibility = aliceSession.getRoom(e2eRoomID)!! .stateService()
.stateService() .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty) ?.content
?.content ?.toModel<RoomHistoryVisibilityContent>()
?.toModel<RoomHistoryVisibilityContent>() Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}") roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
} }
var aliceThirdMessageSessionId: String? = null var aliceThirdMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage -> sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timelineEvent = bobRoomPov
val timelineEvent = bobRoomPov ?.timelineService()
?.timelineService() ?.getTimelineEvent(thirdMessage)
?.getTimelineEvent(thirdMessage) (timelineEvent != null &&
(timelineEvent != null && timelineEvent.isEncrypted() &&
timelineEvent.isEncrypted() && timelineEvent.root.getClearType() == EventType.MESSAGE).also {
timelineEvent.root.getClearType() == EventType.MESSAGE).also { if (it) {
if (it) { aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
}
} }
} }
} }
@ -376,35 +353,31 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper) 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 return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
} }
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) { private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { otherAccounts.map {
otherAccounts.map { aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership }.all {
}.all { it == Membership.JOIN
it == Membership.JOIN
}
} }
} }
} }
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) { private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
(roomSummary != null && roomSummary.membership == Membership.INVITE).also { if (it) {
if (it) { Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
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") Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try { try {
otherSession.roomService().joinRoom(e2eRoomID) otherSession.roomService().joinRoom(e2eRoomID)
@ -414,11 +387,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
} }
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...") Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID) roomSummary != null && roomSummary.membership == Membership.JOIN
roomSummary != null && roomSummary.membership == Membership.JOIN
}
} }
} }
} }

View file

@ -52,15 +52,13 @@ class PreShareKeysTest : InstrumentedTest {
Log.d("#Test", "Room Key Received from alice $preShareCount") Log.d("#Test", "Room Key Received from alice $preShareCount")
// Force presharing of new outbound key // Force presharing of new outbound key
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it) aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it)
} }
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() newKeysCount > preShareCount
newKeysCount > preShareCount
}
} }
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
@ -85,10 +83,8 @@ class PreShareKeysTest : InstrumentedTest {
val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first() val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId) assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
}
} }
} }
} }

View file

@ -17,7 +17,8 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4 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.Assert
import org.junit.Before import org.junit.Before
import org.junit.FixMethodOrder 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.android.sdk.internal.crypto.store.db.serializeForRealm
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -98,69 +98,37 @@ class UnwedgingTest : InstrumentedTest {
val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20)) val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20))
bobTimeline.start() bobTimeline.start()
val bobFinalLatch = CountDownLatch(1)
val bobHasThreeDecryptedEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
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() messagesReceivedByBob = emptyList()
// - Alice sends a 1st message with a 1st megolm session // - Alice sends a 1st message with a 1st megolm session
roomFromAlicePOV.sendService().sendTextMessage("First message") roomFromAlicePOV.sendService().sendTextMessage("First message")
// Wait for the message to be received by Bob // Wait for the message to be received by Bob
testHelper.await(latch) messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1)
bobTimeline.removeListener(bobEventsListener)
messagesReceivedByBob.size shouldBe 1 messagesReceivedByBob.size shouldBeEqualTo 1
val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// - Store the olm session between A&B devices // - Store the olm session between A&B devices
// Let us pickle our session with bob here so we can later unpickle it // Let us pickle our session with bob here so we can later unpickle it
// and wedge our session. // and wedge our session.
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!) 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 olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
val oldSession = serializeForRealm(olmSession.olmSession) val oldSession = serializeForRealm(olmSession.olmSession)
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
Thread.sleep(6_000)
latch = CountDownLatch(1)
bobEventsListener = createEventListener(latch, 2)
bobTimeline.addListener(bobEventsListener)
messagesReceivedByBob = emptyList() messagesReceivedByBob = emptyList()
Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session") Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
// - 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") roomFromAlicePOV.sendService().sendTextMessage("Second message")
// Wait for the message to be received by Bob // Wait for the message to be received by Bob
testHelper.await(latch) messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2)
bobTimeline.removeListener(bobEventsListener)
messagesReceivedByBob.size shouldBe 2 messagesReceivedByBob.size shouldBeEqualTo 2
// Session should have changed // Session should have changed
val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Assert.assertNotEquals(firstMessageSession, secondMessageSession) Assert.assertNotEquals(firstMessageSession, secondMessageSession)
@ -173,25 +141,18 @@ class UnwedgingTest : InstrumentedTest {
bobSession.cryptoService().getMyDevice().identityKey()!! bobSession.cryptoService().getMyDevice().identityKey()!!
) )
olmDevice.clearOlmSessionCache() olmDevice.clearOlmSessionCache()
Thread.sleep(6_000)
// Force new session, and key share // Force new session, and key share
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId) 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 // Wait for the message to be received by Bob
testHelper.waitWithLatch { messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3)
bobEventsListener = createEventListener(it, 3)
bobTimeline.addListener(bobEventsListener)
messagesReceivedByBob = emptyList()
Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session") messagesReceivedByBob.size shouldBeEqualTo 3
// - 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
val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!! val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession") 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[1].root.getClearType())
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].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 // 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 // It's a trick to force key request on fail to decrypt
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor { object : UserInteractiveAuthInterceptor {
@ -223,24 +184,22 @@ class UnwedgingTest : InstrumentedTest {
} }
// Wait until we received back the key // Wait until we received back the key
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { // we should get back the key and be able to decrypt
// we should get back the key and be able to decrypt val result = tryOrNull {
val result = testHelper.runBlockingTest { bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
tryOrNull {
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
}
}
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
} }
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
} }
bobTimeline.dispose() bobTimeline.dispose()
} }
}
private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener { private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> {
return object : Timeline.Listener { return suspendCancellableCoroutine { continuation ->
val listener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
// noop // noop
} }
@ -250,12 +209,16 @@ class UnwedgingTest : InstrumentedTest {
} }
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED } val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED }
if (messagesReceivedByBob.size == expectedNumberOfMessages) { if (messagesReceived.size == expectedCount) {
latch.countDown() removeListener(this)
continuation.resume(messagesReceived)
} }
} }
} }
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
} }
} }

View file

@ -55,7 +55,7 @@ class XSigningTest : InstrumentedTest {
fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper -> fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(object : UserInteractiveAuthInterceptor { .initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
@ -101,14 +101,14 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams) promise.resume(aliceAuthParams)
} }
}, it) }, it)
} }
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams) promise.resume(bobAuthParams)
@ -117,7 +117,7 @@ class XSigningTest : InstrumentedTest {
} }
// Check that alice can see bob keys // Check that alice can see bob keys
testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId) val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId)
assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey()) assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey())
@ -154,14 +154,14 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams) promise.resume(aliceAuthParams)
} }
}, it) }, it)
} }
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams) promise.resume(bobAuthParams)
@ -171,12 +171,12 @@ class XSigningTest : InstrumentedTest {
// Check that alice can see bob keys // Check that alice can see bob keys
val bobUserId = bobSession.myUserId val bobUserId = bobSession.myUserId
testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) } testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) }
val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId) val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId)
assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false) assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false)
testHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) } testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) }
// Now bobs logs in on a new device and verifies 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 // 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!! val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
// Check that bob first session sees the new login // Check that bob first session sees the new login
val data = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { val data = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) 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) assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
// Manually mark it as trusted from first session // Manually mark it as trusted from first session
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it) bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it)
} }
// Now alice should cross trust bob's second device // Now alice should cross trust bob's second device
val data2 = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { val data2 = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
} }

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.crypto.encryption package org.matrix.android.sdk.internal.crypto.encryption
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.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.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestHelper 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) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
class EncryptionTest : InstrumentedTest { class EncryptionTest : InstrumentedTest {
@Test @Test
fun test_EncryptionEvent() { fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
runCryptoTest(context()) { cryptoTestHelper, testHelper -> performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room -> // Send an encryption Event as an Event (and not as a state event)
// Send an encryption Event as an Event (and not as a state event) room.sendService().sendEvent(
room.sendService().sendEvent( eventType = EventType.STATE_ROOM_ENCRYPTION,
eventType = EventType.STATE_ROOM_ENCRYPTION, content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent() )
)
}
} }
} }
@Test @Test
fun test_EncryptionStateEvent() { fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
runCryptoTest(context()) { cryptoTestHelper, testHelper -> performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room -> // Send an encryption Event as a State Event
runBlocking { room.stateService().sendStateEvent(
// Send an encryption Event as a State Event eventType = EventType.STATE_ROOM_ENCRYPTION,
room.stateService().sendStateEvent( stateKey = "",
eventType = EventType.STATE_ROOM_ENCRYPTION, body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
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 cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val room = aliceSession.getRoom(cryptoTestData.roomId)!! val room = aliceSession.getRoom(cryptoTestData.roomId)!!
room.roomCryptoService().isEncrypted() shouldBe false room.roomCryptoService().isEncrypted() shouldBe false
val timeline = room.timelineService().createTimeline(null, TimelineSettings(10)) 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<Unit> { continuation ->
val timelineListener = object : Timeline.Listener { val timelineListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) { override fun onTimelineFailure(throwable: Throwable) {
} }
@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest {
.filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION } .filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION }
if (newMessages.isNotEmpty()) { if (newMessages.isNotEmpty()) {
timeline.removeListener(this) removeListener(this)
latch.countDown() continuation.resume(Unit)
} }
} }
} }
timeline.start() addListener(timelineListener)
timeline.addListener(timelineListener) continuation.invokeOnCancellation { removeListener(timelineListener) }
action.invoke(room)
testHelper.await(latch)
timeline.dispose()
testHelper.waitWithLatch {
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
it.countDown()
}
} }
} }

View file

@ -63,14 +63,12 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message // Create an encrypted room and add a message
val roomId = commonTestHelper.runBlockingTest { val roomId = aliceSession.roomService().createRoom(
aliceSession.roomService().createRoom( CreateRoomParams().apply {
CreateRoomParams().apply { visibility = RoomDirectoryVisibility.PRIVATE
visibility = RoomDirectoryVisibility.PRIVATE enableEncryption()
enableEncryption() }
} )
)
}
val room = aliceSession.getRoom(roomId) val room = aliceSession.getRoom(roomId)
assertNotNull(room) assertNotNull(room)
Thread.sleep(4_000) Thread.sleep(4_000)
@ -94,10 +92,8 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(receivedEvent) assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted()) assert(receivedEvent!!.isEncrypted())
commonTestHelper.runBlockingTest { mustFail {
mustFail { aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
}
} }
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@ -111,15 +107,13 @@ class KeyShareTests : InstrumentedTest {
var outGoingRequestId: String? = null var outGoingRequestId: String? = null
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
aliceSession2.cryptoService().getOutgoingRoomKeyRequests() .let {
.let { val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } outGoingRequestId = outgoing?.requestId
outGoingRequestId = outgoing?.requestId outgoing != null
outgoing != null }
}
}
} }
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
@ -131,9 +125,8 @@ class KeyShareTests : InstrumentedTest {
// The first session should see an incoming request // The first session should see an incoming request
// the request should be refused, because the device is not trusted // the request should be refused, because the device is not trusted
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS
// DEBUG LOGS
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let { // aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") // Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
// Log.v("TEST", "=========================") // Log.v("TEST", "=========================")
@ -143,32 +136,27 @@ class KeyShareTests : InstrumentedTest {
// Log.v("TEST", "=========================") // Log.v("TEST", "=========================")
// } // }
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming != null incoming != null
}
} }
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS
// DEBUG LOGS aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> Log.v("TEST", "=========================")
Log.v("TEST", "=========================") Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") Log.v("TEST", "=========================")
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
} }
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 {
mustFail { aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
}
} }
// Mark the device as trusted // Mark the device as trusted
@ -210,12 +198,10 @@ class KeyShareTests : InstrumentedTest {
// As it was share previously alice should accept to reshare // As it was share previously alice should accept to reshare
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } aliceReply != null && aliceReply.result is RequestResult.Success
aliceReply != null && aliceReply.result is RequestResult.Success
}
} }
} }
@ -233,12 +219,10 @@ class KeyShareTests : InstrumentedTest {
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait for alice first session to be aware of that session? // we wait for alice first session to be aware of that session?
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } newSession != null
newSession != null
}
} }
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!! val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
@ -247,13 +231,11 @@ class KeyShareTests : InstrumentedTest {
// As it was share previously alice should accept to reshare // As it was share previously alice should accept to reshare
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val ownDeviceReply =
val ownDeviceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
}
} }
} }
@ -277,12 +259,10 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.syncSession(aliceNewSession) commonTestHelper.syncSession(aliceNewSession)
// we wait bob first session to be aware of that session? // we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } newSession != null
newSession != null
}
} }
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
@ -304,26 +284,22 @@ class KeyShareTests : InstrumentedTest {
aliceNewSession.cryptoService().enableKeyGossiping(true) aliceNewSession.cryptoService().enableKeyGossiping(true)
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val ownDeviceReply = outgoing?.results
val ownDeviceReply = outgoing?.results ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } val result = ownDeviceReply?.result
val result = ownDeviceReply?.result Log.v("TEST", "own device result is $result")
Log.v("TEST", "own device result is $result") result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
}
} }
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val bobDeviceReply = outgoing?.results
val bobDeviceReply = outgoing?.results ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } val result = bobDeviceReply?.result
val result = bobDeviceReply?.result Log.v("TEST", "bob device result is $result")
Log.v("TEST", "bob device result is $result") result != null && result is RequestResult.Success && result.chainIndex > 0
result != null && result is RequestResult.Success && result.chainIndex > 0
}
} }
// it's a success but still can't decrypt first message // it's a success but still can't decrypt first message
@ -337,21 +313,19 @@ class KeyShareTests : InstrumentedTest {
// Let's now try to request // Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS
// DEBUG LOGS aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> Log.v("TEST", "=========================")
Log.v("TEST", "=========================") Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") Log.v("TEST", "=========================")
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
} }
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! // 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 // Additional test, can we check that bob replied successfully but with a ratcheted key
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } val result = bobReply?.result
val result = bobReply?.result result != null && result is RequestResult.Success && result.chainIndex == 3
result != null && result is RequestResult.Success && result.chainIndex == 3
}
} }
commonTestHelper.signOutAndClose(aliceNewSession) commonTestHelper.signOutAndClose(aliceNewSession)
@ -394,12 +366,10 @@ class KeyShareTests : InstrumentedTest {
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait bob first session to be aware of that session? // we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } newSession != null
newSession != null
}
} }
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
@ -430,14 +400,12 @@ class KeyShareTests : InstrumentedTest {
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
// Should get a reply from bob and not from alice // Should get a reply from bob and not from alice
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } val result = bobReply?.result
val result = bobReply?.result result != null && result is RequestResult.Success && result.chainIndex == 3
result != null && result is RequestResult.Success && result.chainIndex == 3
}
} }
val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
@ -450,14 +418,12 @@ class KeyShareTests : InstrumentedTest {
aliceSession.syncService().startSync(true) aliceSession.syncService().startSync(true)
// We should now get a reply from first session // We should now get a reply from first session
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } val ownDeviceReply =
val ownDeviceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } val result = ownDeviceReply?.result
val result = ownDeviceReply?.result result != null && result is RequestResult.Success && result.chainIndex == 0
result != null && result is RequestResult.Success && result.chainIndex == 0
}
} }
// It should be in sent then cancel // It should be in sent then cancel

View file

@ -80,10 +80,8 @@ class WithHeldTests : InstrumentedTest {
val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first() val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
// await for bob unverified session to get the message // await for bob unverified session to get the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
}
} }
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! 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 // Bob should not be able to decrypt because the keys is withheld
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { mustFail(
mustFail( message = "This session should not be able to decrypt",
message = "This session should not be able to decrypt", failureBlock = { failure ->
failureBlock = { failure -> val type = (failure as MXCryptoError.Base).errorType
val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) }
} ) {
) { bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
} }
// Let's see if the reply we got from bob first session is unverified // Let's see if the reply we got from bob first session is unverified
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() .firstOrNull { it.sessionId == megolmSessionId }
.firstOrNull { it.sessionId == megolmSessionId } ?.results
?.results ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } ?.result
?.result ?.let {
?.let { it as? RequestResult.Failure
it as? RequestResult.Failure }
} ?.code == WithHeldCode.UNVERIFIED
?.code == WithHeldCode.UNVERIFIED
}
} }
// enable back sending to unverified // enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first() val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId) // wait until it's decrypted
// wait until it's decrypted ev?.root?.getClearType() == EventType.MESSAGE
ev?.root?.getClearType() == EventType.MESSAGE
}
} }
// Previous message should still be undecryptable (partially withheld session) // Previous message should still be undecryptable (partially withheld session)
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { mustFail(
mustFail( message = "This session should not be able to decrypt",
message = "This session should not be able to decrypt", failureBlock = { failure ->
failureBlock = { failure -> val type = (failure as MXCryptoError.Base).errorType
val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) }) {
}) { bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
} }
} }
@ -177,26 +167,22 @@ class WithHeldTests : InstrumentedTest {
val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
// await for bob session to get the message // await for bob session to get the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
}
} }
// Previous message should still be undecryptable (partially withheld session) // Previous message should still be undecryptable (partially withheld session)
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
// .. might need to wait a bit for stability? // .. might need to wait a bit for stability?
testHelper.runBlockingTest { mustFail(
mustFail( message = "This session should not be able to decrypt",
message = "This session should not be able to decrypt", failureBlock = { failure ->
failureBlock = { failure -> val type = (failure as MXCryptoError.Base).errorType
val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage
val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage) }) {
}) { bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
}
} }
// Ensure that alice has marked the session to be shared with bob // Ensure that alice has marked the session to be shared with bob
@ -216,10 +202,8 @@ class WithHeldTests : InstrumentedTest {
// Check that the // Check that the
// await for bob SecondSession session to get the message // await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
}
} }
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject( val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(
@ -258,27 +242,21 @@ class WithHeldTests : InstrumentedTest {
var sessionId: String? = null var sessionId: String? = null
// Check that the // Check that the
// await for bob SecondSession session to get the message // await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also { // try to decrypt and force key request
// try to decrypt and force key request tryOrNull {
tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "")
testHelper.runBlockingTest {
bobSecondSession.cryptoService().decryptEvent(it.root, "")
}
}
} }
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null
} }
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null
} }
// Check that bob second session requested the key // Check that bob second session requested the key
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!) wc?.code == WithHeldCode.UNAUTHORISED
wc?.code == WithHeldCode.UNAUTHORISED
}
} }
} }
} }

View file

@ -30,7 +30,7 @@ internal data class KeysBackupScenarioData(
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult, val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session val aliceSession2: Session
) { ) {
fun cleanUp(testHelper: CommonTestHelper) { suspend fun cleanUp(testHelper: CommonTestHelper) {
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(aliceSession2) testHelper.signOutAndClose(aliceSession2)
} }

View file

@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Rule 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.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.RetryTestRule import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.TestConstants 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.Collections
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -116,7 +118,7 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled()) assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(null, null, it) keysBackup.prepareKeysBackupVersion(null, null, it)
} }
@ -133,7 +135,6 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams) val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
cryptoTestHelper.initializeCrossSigning(bobSession) cryptoTestHelper.initializeCrossSigning(bobSession)
@ -143,14 +144,14 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled()) assertFalse(keysBackup.isEnabled())
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(null, null, it) keysBackup.prepareKeysBackupVersion(null, null, it)
} }
assertFalse(keysBackup.isEnabled()) assertFalse(keysBackup.isEnabled())
// Create the version // Create the version
val version = testHelper.doSync<KeysVersion> { val version = testHelper.waitForCallback<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
} }
@ -158,10 +159,10 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(keysBackup.isEnabled()) assertTrue(keysBackup.isEnabled())
// Check that it's signed with MSK // Check that it's signed with MSK
val versionResult = testHelper.doSync<KeysVersionResult?> { val versionResult = testHelper.waitForCallback<KeysVersionResult?> {
keysBackup.getVersion(version.version, it) keysBackup.getVersion(version.version, it)
} }
val trust = testHelper.doSync<KeysBackupVersionTrust> { val trust = testHelper.waitForCallback<KeysBackupVersionTrust> {
keysBackup.getKeysBackupTrust(versionResult!!, it) keysBackup.getKeysBackupTrust(versionResult!!, it)
} }
@ -257,7 +258,7 @@ class KeysBackupTest : InstrumentedTest {
var lastBackedUpKeysProgress = 0 var lastBackedUpKeysProgress = 0
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener { keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) { override fun onProgress(progress: Int, total: Int) {
assertEquals(nbOfKeys, total) assertEquals(nbOfKeys, total)
@ -299,7 +300,7 @@ class KeysBackupTest : InstrumentedTest {
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg // - Check encryptGroupSession() returns stg
val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) } val keyBackupData = keysBackup.encryptGroupSession(session)
assertNotNull(keyBackupData) assertNotNull(keyBackupData)
assertNotNull(keyBackupData!!.sessionData) assertNotNull(keyBackupData!!.sessionData)
@ -334,7 +335,7 @@ class KeysBackupTest : InstrumentedTest {
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver // - Restore the e2e backup from the homeserver
val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@ -379,7 +380,7 @@ class KeysBackupTest : InstrumentedTest {
// assertTrue(unsentRequest != null || sentRequest != null) // assertTrue(unsentRequest != null || sentRequest != null)
// //
// // - Restore the e2e backup from the homeserver // // - Restore the e2e backup from the homeserver
// val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { // val importRoomKeysResult = mTestHelper.doSyncSuspending<> { }<ImportRoomKeysResult> {
// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, // testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, // testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
// null, // null,
@ -429,7 +430,7 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device // - Trust the backup from the new device
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion( testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
true, true,
@ -445,14 +446,14 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server // - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult() }.toKeysVersionResult()
// - It must be the same // - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
} }
@ -489,7 +490,7 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the recovery key // - Trust the backup from the new device with the recovery key
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@ -505,14 +506,14 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server // - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult() }.toKeysVersionResult()
// - It must be the same // - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
} }
@ -547,13 +548,13 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong recovery key // - Try to trust the backup from the new device with a wrong recovery key
val latch = CountDownLatch(1) testHelper.waitForCallbackError<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
"Bad recovery key", "Bad recovery key",
TestMatrixCallback(latch, false) it
) )
testHelper.await(latch) }
// - The new device must still see the previous backup as not trusted // - The new device must still see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
@ -591,7 +592,7 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Trust the backup from the new device with the password // - Trust the backup from the new device with the password
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password, password,
@ -607,14 +608,14 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled()) assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
// - Retrieve the last version from the server // - Retrieve the last version from the server
val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it) testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult() }.toKeysVersionResult()
// - It must be the same // - It must be the same
assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version) assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it) testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
} }
@ -652,13 +653,13 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState()) assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
// - Try to trust the backup from the new device with a wrong password // - Try to trust the backup from the new device with a wrong password
val latch = CountDownLatch(1) testHelper.waitForCallbackError<Unit> {
testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase( testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
badPassword, badPassword,
TestMatrixCallback(latch, false) it
) )
testHelper.await(latch) }
// - The new device must still see the previous backup as not trusted // - The new device must still see the previous backup as not trusted
assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion) assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
@ -679,26 +680,21 @@ class KeysBackupTest : InstrumentedTest {
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a wrong recovery key // - Try to restore the e2e backup with a wrong recovery key
val latch2 = CountDownLatch(1) val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
var importRoomKeysResult: ImportRoomKeysResult? = null keysBackupService.restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, keysBackupService.keysBackupVersion!!,
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null,
null, null,
null, null,
object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { it
override fun onSuccess(data: ImportRoomKeysResult) { )
importRoomKeysResult = data }
super.onSuccess(data)
}
}
)
testHelper.await(latch2)
// onSuccess may not have been called assertTrue(importRoomKeysResult is InvalidParameterException)
assertNull(importRoomKeysResult)
} }
/** /**
@ -718,7 +714,7 @@ class KeysBackupTest : InstrumentedTest {
// - Restore the e2e backup with the password // - Restore the e2e backup with the password
val steps = ArrayList<StepProgressListener.Step>() val steps = ArrayList<StepProgressListener.Step>()
val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword( testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
password, password,
@ -771,26 +767,21 @@ class KeysBackupTest : InstrumentedTest {
val wrongPassword = "passw0rd" val wrongPassword = "passw0rd"
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a wrong password // - Try to restore the e2e backup with a wrong password
val latch2 = CountDownLatch(1) val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
var importRoomKeysResult: ImportRoomKeysResult? = null keysBackupService.restoreKeyBackupWithPassword(
testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, keysBackupService.keysBackupVersion!!,
wrongPassword, wrongPassword,
null, null,
null, null,
null, null,
object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { it
override fun onSuccess(data: ImportRoomKeysResult) { )
importRoomKeysResult = data }
super.onSuccess(data)
}
}
)
testHelper.await(latch2)
// onSuccess may not have been called assertTrue(importRoomKeysResult is InvalidParameterException)
assertNull(importRoomKeysResult)
} }
/** /**
@ -808,7 +799,7 @@ class KeysBackupTest : InstrumentedTest {
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the recovery key. // - Restore the e2e backup with the recovery key.
val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> { val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@ -833,26 +824,21 @@ class KeysBackupTest : InstrumentedTest {
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper) val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null) val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
// - Try to restore the e2e backup with a password // - Try to restore the e2e backup with a password
val latch2 = CountDownLatch(1) val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
var importRoomKeysResult: ImportRoomKeysResult? = null keysBackupService.restoreKeyBackupWithPassword(
testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, keysBackupService.keysBackupVersion!!,
"password", "password",
null, null,
null, null,
null, null,
object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) { it
override fun onSuccess(data: ImportRoomKeysResult) { )
importRoomKeysResult = data }
super.onSuccess(data)
}
}
)
testHelper.await(latch2)
// onSuccess may not have been called assertTrue(importRoomKeysResult is IllegalStateException)
assertNull(importRoomKeysResult)
} }
/** /**
@ -874,12 +860,12 @@ class KeysBackupTest : InstrumentedTest {
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Get key backup version from the homeserver // Get key backup version from the homeserver
val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> { val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
keysBackup.getCurrentVersion(it) keysBackup.getCurrentVersion(it)
}.toKeysVersionResult() }.toKeysVersionResult()
// - Check the returned KeyBackupVersion is trusted // - Check the returned KeyBackupVersion is trusted
val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> { val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
keysBackup.getKeysBackupTrust(keysVersionResult!!, it) keysBackup.getKeysBackupTrust(keysVersionResult!!, it)
} }
@ -918,34 +904,39 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled()) assertFalse(keysBackup.isEnabled())
// Wait for keys backup to be finished // Wait for keys backup to be finished
val latch0 = CountDownLatch(1)
var count = 0 var count = 0
keysBackup.addListener(object : KeysBackupStateListener { waitFor(
override fun onStateChange(newState: KeysBackupState) { continueWhen = {
// Check the backup completes suspendCancellableCoroutine<Unit> { continuation ->
if (newState == KeysBackupState.ReadyToBackUp) { val listener = object : KeysBackupStateListener {
count++ override fun onStateChange(newState: KeysBackupState) {
// Check the backup completes
if (newState == KeysBackupState.ReadyToBackUp) {
count++
if (count == 2) { if (count == 2) {
// Remove itself from the list of listeners // Remove itself from the list of listeners
keysBackup.removeListener(this) keysBackup.removeListener(this)
continuation.resume(Unit)
latch0.countDown() }
}
}
}
keysBackup.addListener(listener)
continuation.invokeOnCancellation { keysBackup.removeListener(listener) }
} }
} },
} action = {
}) // - Make alice back up her keys to her homeserver
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// - Make alice back up her keys to her homeserver },
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) )
assertTrue(keysBackup.isEnabled()) assertTrue(keysBackup.isEnabled())
testHelper.await(latch0)
// - Create a new backup with fake data on the homeserver, directly using the rest client // - Create a new backup with fake data on the homeserver, directly using the rest client
val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo() val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo()
testHelper.doSync<KeysVersion> { testHelper.waitForCallback<KeysVersion> {
(keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it) (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it)
} }
@ -953,9 +944,7 @@ class KeysBackupTest : InstrumentedTest {
(cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers() (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers()
// - Make alice back up all her keys again // - Make alice back up all her keys again
val latch2 = CountDownLatch(1) testHelper.waitForCallbackError<Unit> { keysBackup.backupAllGroupSessions(null, it) }
keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false))
testHelper.await(latch2)
// -> That must fail and her backup state must be WrongBackUpVersion // -> That must fail and her backup state must be WrongBackUpVersion
assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState()) assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState())
@ -991,7 +980,7 @@ class KeysBackupTest : InstrumentedTest {
keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup) keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Wait for keys backup to finish by asking again to backup keys. // Wait for keys backup to finish by asking again to backup keys.
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(null, it) keysBackup.backupAllGroupSessions(null, it)
} }
@ -1016,19 +1005,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver2 = StateObserver(keysBackup2) val stateObserver2 = StateObserver(keysBackup2)
var isSuccessful = false testHelper.waitForCallbackError<Unit> { keysBackup2.backupAllGroupSessions(null, it) }
val latch2 = CountDownLatch(1)
keysBackup2.backupAllGroupSessions(
null,
object : TestMatrixCallback<Unit>(latch2, false) {
override fun onSuccess(data: Unit) {
isSuccessful = true
super.onSuccess(data)
}
})
testHelper.await(latch2)
assertFalse(isSuccessful)
// Backup state must be NotTrusted // Backup state must be NotTrusted
assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState()) 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 // -> Backup should automatically enable on the new device
val latch4 = CountDownLatch(1) suspendCancellableCoroutine<Unit> { continuation ->
keysBackup2.addListener(object : KeysBackupStateListener { val listener = object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) { override fun onStateChange(newState: KeysBackupState) {
// Check the backup completes // Check the backup completes
if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) { if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) {
// Remove itself from the list of listeners // Remove itself from the list of listeners
keysBackup2.removeListener(this) keysBackup2.removeListener(this)
continuation.resume(Unit)
latch4.countDown() }
} }
} }
}) keysBackup2.addListener(listener)
testHelper.await(latch4) continuation.invokeOnCancellation { keysBackup2.removeListener(listener) }
}
// -> It must use the same backup version // -> It must use the same backup version
assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion) assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion)
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it) aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it)
} }
@ -1092,7 +1070,7 @@ class KeysBackupTest : InstrumentedTest {
assertTrue(keysBackup.isEnabled()) assertTrue(keysBackup.isEnabled())
// Delete the backup // Delete the backup
testHelper.doSync<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) } testHelper.waitForCallback<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) }
// Backup is now disabled // Backup is now disabled
assertFalse(keysBackup.isEnabled()) assertFalse(keysBackup.isEnabled())

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.keysbackup package org.matrix.android.sdk.internal.crypto.keysbackup
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.Assert import org.junit.Assert
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session 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.assertDictEquals
import org.matrix.android.sdk.common.assertListEquals import org.matrix.android.sdk.common.assertListEquals
import org.matrix.android.sdk.internal.crypto.MegolmSessionData import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import java.util.concurrent.CountDownLatch import kotlin.coroutines.resume
internal class KeysBackupTestHelper( internal class KeysBackupTestHelper(
private val testHelper: CommonTestHelper, private val testHelper: CommonTestHelper,
@ -47,7 +48,7 @@ internal class KeysBackupTestHelper(
* *
* @param password optional password * @param password optional password
*/ */
fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData { suspend fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages() val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
waitForKeybackUpBatching() waitForKeybackUpBatching()
@ -64,7 +65,7 @@ internal class KeysBackupTestHelper(
var lastProgress = 0 var lastProgress = 0
var lastTotal = 0 var lastTotal = 0
testHelper.doSync<Unit> { testHelper.waitForCallback<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener { keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) { override fun onProgress(progress: Int, total: Int) {
lastProgress = progress lastProgress = progress
@ -97,13 +98,13 @@ internal class KeysBackupTestHelper(
) )
} }
fun prepareAndCreateKeysBackupData( suspend fun prepareAndCreateKeysBackupData(
keysBackup: KeysBackupService, keysBackup: KeysBackupService,
password: String? = null password: String? = null
): PrepareKeysBackupDataResult { ): PrepareKeysBackupDataResult {
val stateObserver = StateObserver(keysBackup) val stateObserver = StateObserver(keysBackup)
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> { val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(password, null, it) keysBackup.prepareKeysBackupVersion(password, null, it)
} }
@ -112,7 +113,7 @@ internal class KeysBackupTestHelper(
Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled()) Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled())
// Create the version // Create the version
val keysVersion = testHelper.doSync<KeysVersion> { val keysVersion = testHelper.waitForCallback<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it) 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 * 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 * 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 already in the wanted state, return
if (session.cryptoService().keysBackupService().getState() == state) { val keysBackupService = session.cryptoService().keysBackupService()
if (keysBackupService.getState() == state) {
return return
} }
// Else observe state changes // Else observe state changes
val latch = CountDownLatch(1) suspendCancellableCoroutine<Unit> { continuation ->
val listener = object : KeysBackupStateListener {
session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener { override fun onStateChange(newState: KeysBackupState) {
override fun onStateChange(newState: KeysBackupState) { if (newState == state) {
if (newState == state) { keysBackupService.removeListener(this)
session.cryptoService().keysBackupService().removeListener(this) continuation.resume(Unit)
latch.countDown() }
} }
} }
}) keysBackupService.addListener(listener)
continuation.invokeOnCancellation { keysBackupService.removeListener(listener) }
testHelper.await(latch) }
} }
fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {

View file

@ -58,18 +58,16 @@ class ReplayAttackTest : InstrumentedTest {
val fakeEventWithTheSameIndex = val fakeEventWithTheSameIndex =
sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId)) sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
testHelper.runBlockingTest { // Lets assume we are from the main timelineId
// Lets assume we are from the main timelineId val timelineId = "timelineId"
val timelineId = "timelineId" // Lets decrypt the original event
// Lets decrypt the original event aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId) // Lets decrypt the fake event that will have the same message index
// Lets decrypt the fake event that will have the same message index val exception = assertFailsWith<MXCryptoError.Base> {
val exception = assertFailsWith<MXCryptoError.Base> { // An exception should be thrown while the same index would have been used for the previous decryption
// An exception should be thrown while the same index would have been used for the previous decryption aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
}
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
} }
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)
} }
@ -93,17 +91,15 @@ class ReplayAttackTest : InstrumentedTest {
Assert.assertTrue("Message should be sent", sentEvents.size == 1) Assert.assertTrue("Message should be sent", sentEvents.size == 1)
assertEquals(sentEvents.size, 1) assertEquals(sentEvents.size, 1)
testHelper.runBlockingTest { // Lets assume we are from the main timelineId
// Lets assume we are from the main timelineId val timelineId = "timelineId"
val timelineId = "timelineId" // Lets decrypt the original event
// 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) aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
try { } catch (ex: Throwable) {
// Lets try to decrypt the same event fail("Shouldn't throw a decryption error for same event")
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
} catch (ex: Throwable) {
fail("Shouldn't throw a decryption error for same event")
}
} }
} }
} }

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto.ssss package org.matrix.android.sdk.internal.crypto.ssss
import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull 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.SecretStorageKeyContent
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo 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.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.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants 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 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" 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 // Assert Account data is updated
testHelper.waitWithLatch { val accountData = aliceSession.accountDataService()
val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") .onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") }
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> .first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" }
if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") { .getOrNull()
accountData = t.getOrNull()
}
it.countDown()
}
liveAccountData.observeForever(accountDataObserver)
}
assertNotNull("Key should be stored in account data", accountData) assertNotNull("Key should be stored in account data", accountData)
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
assertNotNull("Key Content cannot be parsed", parsed) assertNotNull("Key Content cannot be parsed", parsed)
@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest {
assertEquals("Unexpected key name", "Test Key", parsed.name) assertEquals("Unexpected key name", "Test Key", parsed.name)
assertNull("Key was not generated from passphrase", parsed.passphrase) 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 // Set as default key
testHelper.waitWithLatch { latch ->
quadS.setDefaultKey(TEST_KEY_ID)
val liveDefAccountData =
aliceSession.accountDataService().getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) {
defaultKeyAccountData = t.getOrNull()!!
latch.countDown()
}
}
liveDefAccountData.observeForever(accountDefDataObserver)
}
assertNotNull(defaultKeyAccountData?.content) assertNotNull(defaultKeyAccountData?.content)
assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key")) 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 aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId = "My.Key" val keyId = "My.Key"
val info = generatedSecret(testHelper, aliceSession, keyId, true) val info = generatedSecret(aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey) val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
// Store a secret // Store a secret
val clearSecret = "42".toByteArray().toBase64NoPadding() val clearSecret = "42".toByteArray().toBase64NoPadding()
testHelper.runBlockingTest { aliceSession.sharedSecretStorageService().storeSecret(
aliceSession.sharedSecretStorageService().storeSecret( "secret.of.life",
"secret.of.life", clearSecret,
clearSecret, listOf(KeyRef(null, keySpec)) // default key
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<*, *> val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent) assertNotNull("Element should be encrypted", encryptedContent)
@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest {
// Try to decrypt?? // Try to decrypt??
val decryptedSecret = testHelper.runBlockingTest { val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret(
aliceSession.sharedSecretStorageService().getSecret( "secret.of.life",
"secret.of.life", null, // default key
null, // default key keySpec!!
keySpec!! )
)
}
assertEquals("Secret mismatch", clearSecret, decryptedSecret) assertEquals("Secret mismatch", clearSecret, decryptedSecret)
} }
@ -159,14 +139,10 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" 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 // 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 @Test
@ -174,22 +150,20 @@ class QuadSTests : InstrumentedTest {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1" val keyId1 = "Key.1"
val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true) val key1Info = generatedSecret(aliceSession, keyId1, true)
val keyId2 = "Key2" 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" val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
testHelper.runBlockingTest { aliceSession.sharedSecretStorageService().storeSecret(
aliceSession.sharedSecretStorageService().storeSecret( "my.secret",
"my.secret", mySecretText.toByteArray().toBase64NoPadding(),
mySecretText.toByteArray().toBase64NoPadding(), listOf(
listOf( KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) )
) )
)
}
val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret") val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret")
val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *> val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *>
@ -200,21 +174,17 @@ class QuadSTests : InstrumentedTest {
assertNotNull(encryptedContent?.get(keyId2)) assertNotNull(encryptedContent?.get(keyId2))
// Assert that can decrypt with both keys // Assert that can decrypt with both keys
testHelper.runBlockingTest { aliceSession.sharedSecretStorageService().getSecret(
aliceSession.sharedSecretStorageService().getSecret( "my.secret",
"my.secret", keyId1,
keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!
RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!! )
)
}
testHelper.runBlockingTest { aliceSession.sharedSecretStorageService().getSecret(
aliceSession.sharedSecretStorageService().getSecret( "my.secret",
"my.secret", keyId2,
keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!! )
)
}
} }
@Test @Test
@ -224,104 +194,84 @@ class QuadSTests : InstrumentedTest {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1" val keyId1 = "Key.1"
val passphrase = "The good pass phrase" 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" val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
testHelper.runBlockingTest { aliceSession.sharedSecretStorageService().storeSecret(
aliceSession.sharedSecretStorageService().storeSecret( "my.secret",
"my.secret", mySecretText.toByteArray().toBase64NoPadding(),
mySecretText.toByteArray().toBase64NoPadding(), listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))) )
)
}
testHelper.runBlockingTest { try {
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 {
aliceSession.sharedSecretStorageService().getSecret( aliceSession.sharedSecretStorageService().getSecret(
"my.secret", "my.secret",
keyId1, keyId1,
RawBytesKeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
passphrase, "A bad passphrase",
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,
null 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 { private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
var accountData: UserAccountDataEvent? = null val accountData = session.accountDataService()
testHelper.waitWithLatch { .onMain { getLiveUserAccountDataEvent(type) }
val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type) .first { it.getOrNull()?.type == type }
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t -> .getOrNull()
if (t?.getOrNull()?.type == type) {
accountData = t.getOrNull()
it.countDown()
}
}
liveAccountData.observeForever(accountDataObserver)
}
assertNotNull("Account Data type:$type should be found", accountData) assertNotNull("Account Data type:$type should be found", accountData)
return 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 quadS = session.sharedSecretStorageService()
val creationInfo = testHelper.runBlockingTest { val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner)
quadS.generateKey(keyId, null, keyId, emptyKeySigner)
}
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) { if (asDefault) {
testHelper.runBlockingTest { quadS.setDefaultKey(keyId)
quadS.setDefaultKey(keyId) assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
} }
return creationInfo 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 quadS = session.sharedSecretStorageService()
val creationInfo = testHelper.runBlockingTest { val creationInfo = quadS.generateKeyWithPassphrase(
quadS.generateKeyWithPassphrase( keyId,
keyId, keyId,
keyId, passphrase,
passphrase, emptyKeySigner,
emptyKeySigner, null
null )
)
}
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) { if (asDefault) {
testHelper.runBlockingTest { quadS.setDefaultKey(keyId)
quadS.setDefaultKey(keyId) assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
} }
return creationInfo return creationInfo

View file

@ -547,23 +547,19 @@ class SASTest : InstrumentedTest {
var requestID: String? = null var requestID: String? = null
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() requestID = prAlicePOV?.transactionId
requestID = prAlicePOV?.transactionId Log.v("TEST", "== alicePOV is $prAlicePOV")
Log.v("TEST", "== alicePOV is $prAlicePOV") prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
}
} }
Log.v("TEST", "== requestID is $requestID") Log.v("TEST", "== requestID is $requestID")
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull() Log.v("TEST", "== prBobPOV is $prBobPOV")
Log.v("TEST", "== prBobPOV is $prBobPOV") prBobPOV?.transactionId == requestID
prBobPOV?.transactionId == requestID
}
} }
bobVerificationService.readyPendingVerification( bobVerificationService.readyPendingVerification(
@ -573,12 +569,10 @@ class SASTest : InstrumentedTest {
) )
// wait for alice to get the ready // wait for alice to get the ready
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull() Log.v("TEST", "== prAlicePOV is $prAlicePOV")
Log.v("TEST", "== prAlicePOV is $prAlicePOV") prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
}
} }
// Start concurrent! // Start concurrent!
@ -602,20 +596,16 @@ class SASTest : InstrumentedTest {
var alicePovTx: SasVerificationTransaction? var alicePovTx: SasVerificationTransaction?
var bobPovTx: SasVerificationTransaction? var bobPovTx: SasVerificationTransaction?
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction
alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction Log.v("TEST", "== alicePovTx is $alicePovTx")
Log.v("TEST", "== alicePovTx is $alicePovTx") alicePovTx?.state == VerificationTxState.ShortCodeReady
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
} }
// wait for alice to get the ready // wait for alice to get the ready
testHelper.waitWithLatch { testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(it) { bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction
bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction Log.v("TEST", "== bobPovTx is $bobPovTx")
Log.v("TEST", "== bobPovTx is $bobPovTx") bobPovTx?.state == VerificationTxState.ShortCodeReady
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
} }
} }
} }

View file

@ -164,7 +164,7 @@ class VerificationTest : InstrumentedTest {
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!! val bobSession = cryptoTestData.secondSession!!
testHelper.doSync<Unit> { callback -> testHelper.waitForCallback<Unit> { callback ->
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor { object : UserInteractiveAuthInterceptor {
@ -181,7 +181,7 @@ class VerificationTest : InstrumentedTest {
) )
} }
testHelper.doSync<Unit> { callback -> testHelper.waitForCallback<Unit> { callback ->
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning( .initializeCrossSigning(
object : UserInteractiveAuthInterceptor { object : UserInteractiveAuthInterceptor {
@ -261,7 +261,11 @@ class VerificationTest : InstrumentedTest {
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, 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) 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), otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
) )
testHelper.waitWithLatch { latch -> testHelper.retryPeriodically {
testHelper.retryPeriodicallyWithLatch(latch) { val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId) requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
}
} }
testHelper.signOutAndClose(aliceSessionToVerify) testHelper.signOutAndClose(aliceSessionToVerify)

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.session.room.timeline package org.matrix.android.sdk.session.room.timeline
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore 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.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants 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) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -69,30 +72,36 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest {
val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30)) val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30))
bobTimeline.start() bobTimeline.start()
commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { waitFor(
val listener = object : Timeline.Listener { continueWhen = {
wrapWithTimeout(timeout = TestConstants.timeOutMillis * 10) {
suspendCancellableCoroutine<Unit> { continuation ->
val listener = object : Timeline.Listener {
override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
if (direction == Timeline.Direction.FORWARDS) { if (direction == Timeline.Direction.FORWARDS) {
return 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) action = { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) }
} else if (!state.hasMoreToLoad) { )
bobTimeline.removeListener(this)
it.countDown()
}
}
}
bobTimeline.addListener(listener)
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
}
assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS))
assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS))
val onlySentEvents = runBlocking { val onlySentEvents = bobTimeline.getSnapshot()
bobTimeline.getSnapshot()
}
.filter { .filter {
it.root.isTextMessage() it.root.isTextMessage()
}.filter { }.filter {

View file

@ -85,9 +85,7 @@ class SearchMessagesTest : InstrumentedTest {
2 2
) )
val data = commonTestHelper.runBlockingTest { val data = block.invoke(cryptoTestData)
block.invoke(cryptoTestData)
}
assertTrue(data.results?.size == 2) assertTrue(data.results?.size == 2)
assertTrue( assertTrue(

View file

@ -55,15 +55,11 @@ class SpaceCreationTest : InstrumentedTest {
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true)) val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
val roomName = "My Space" val roomName = "My Space"
val topic = "A public space for test" val topic = "A public space for test"
var spaceId: String = "" val spaceId = session.spaceService().createSpace(roomName, topic, null, true)
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
}
commonTestHelper.waitWithLatch { commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(it) { val roomSummary = session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()
session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null roomSummary?.name == roomName && roomSummary.topic == topic
}
} }
val syncedSpace = session.spaceService().getSpace(spaceId) val syncedSpace = session.spaceService().getSpace(spaceId)
@ -79,14 +75,12 @@ class SpaceCreationTest : InstrumentedTest {
assertEquals("Room type should be space", RoomType.SPACE, createContent?.type) assertEquals("Room type should be space", RoomType.SPACE, createContent?.type)
var powerLevelsContent: PowerLevelsContent? = null var powerLevelsContent: PowerLevelsContent? = null
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { powerLevelsContent = syncedSpace.asRoom()
powerLevelsContent = syncedSpace.asRoom() .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content
?.content ?.toModel<PowerLevelsContent>()
?.toModel<PowerLevelsContent>() powerLevelsContent != null
powerLevelsContent != null
}
} }
assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault) 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 roomName = "My Space"
val topic = "A public space for test" val topic = "A public space for test"
val spaceId: String val spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true)
runBlocking { // wait a bit to let the summary update it self :/
spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true) delay(400)
// 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 // Try to join from bob, it's a public space no need to invite
val joinResult: JoinSpaceResult val joinResult = bobSession.spaceService().joinSpace(spaceId)
runBlocking {
joinResult = bobSession.spaceService().joinSpace(spaceId)
}
assertEquals(JoinSpaceResult.Success, joinResult) assertEquals(JoinSpaceResult.Success, joinResult)
@ -152,43 +140,24 @@ class SpaceCreationTest : InstrumentedTest {
val syncedSpace = aliceSession.spaceService().getSpace(spaceId) val syncedSpace = aliceSession.spaceService().getSpace(spaceId)
// create a room // create a room
var firstChild: String? = null val firstChild: String = aliceSession.roomService().createRoom(CreateRoomParams().apply {
commonTestHelper.waitWithLatch { this.name = "FirstRoom"
firstChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { this.topic = "Description of first room"
this.name = "FirstRoom" this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.topic = "Description of first room" })
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
})
it.countDown()
}
commonTestHelper.waitWithLatch { syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true)
syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true)
it.countDown()
}
var secondChild: String? = null val secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply {
commonTestHelper.waitWithLatch { this.name = "SecondRoom"
secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply { this.topic = "Description of second room"
this.name = "SecondRoom" this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.topic = "Description of second room" })
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
})
it.countDown()
}
commonTestHelper.waitWithLatch { syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true)
syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true)
it.countDown()
}
// Try to join from bob, it's a public space no need to invite // Try to join from bob, it's a public space no need to invite
var joinResult: JoinSpaceResult? = null val joinResult = bobSession.spaceService().joinSpace(spaceId)
commonTestHelper.waitWithLatch {
joinResult = bobSession.spaceService().joinSpace(spaceId)
// wait a bit to let the summary update it self :/
it.countDown()
}
assertEquals(JoinSpaceResult.Success, joinResult) assertEquals(JoinSpaceResult.Success, joinResult)

View file

@ -17,8 +17,6 @@
package org.matrix.android.sdk.session.space package org.matrix.android.sdk.session.space
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder 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.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent 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.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.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams 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.model.create.RestrictedRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper 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.powerlevels.Role
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams 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.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams 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) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -60,40 +59,28 @@ class SpaceHierarchyTest : InstrumentedTest {
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceName = "My Space" val spaceName = "My Space"
val topic = "A public space for test" val topic = "A public space for test"
var spaceId = "" val spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
commonTestHelper.runBlockingTest {
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
}
val syncedSpace = session.spaceService().getSpace(spaceId) val syncedSpace = session.spaceService().getSpace(spaceId)
var roomId = "" val roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
commonTestHelper.runBlockingTest {
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
}
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") 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.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true } parents?.forEach {
parents?.forEach { Log.d("## TEST", "parent : $it")
Log.d("## TEST", "parent : $it")
}
parents?.size == 1 &&
parents.first().roomSummary?.name == spaceName &&
canonicalParents?.size == 1 &&
canonicalParents.first().roomSummary?.name == spaceName
} }
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 session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
commonTestHelper,
session, "SpaceA", session, "SpaceA",
listOf( listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A1", true /*auto-join*/, true/*canonical*/),
@ -178,7 +164,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
/* val spaceBInfo = */ createPublicSpace( /* val spaceBInfo = */ createPublicSpace(
commonTestHelper,
session, "SpaceB", session, "SpaceB",
listOf( listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B1", true /*auto-join*/, true/*canonical*/),
@ -188,7 +173,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
commonTestHelper,
session, "SpaceC", session, "SpaceC",
listOf( listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C1", true /*auto-join*/, true/*canonical*/),
@ -199,22 +183,12 @@ class SpaceHierarchyTest : InstrumentedTest {
// add C as a subspace of A // add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
// Create orphan rooms // Create orphan rooms
val orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
var orphan1 = "" val orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
commonTestHelper.runBlockingTest {
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
}
var orphan2 = ""
commonTestHelper.runBlockingTest {
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
}
val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) }) 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" }) 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 // 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" })
val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" }) spaceA.addChildren(a3, viaServers, null, false)
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) 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 session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
commonTestHelper,
session, "SpaceA", session, "SpaceA",
listOf( listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A1", true /*auto-join*/, true/*canonical*/),
@ -262,7 +235,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
commonTestHelper,
session, "SpaceC", session, "SpaceC",
listOf( listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C1", true /*auto-join*/, true/*canonical*/),
@ -273,16 +245,12 @@ class SpaceHierarchyTest : InstrumentedTest {
// add C as a subspace of A // add C as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
// add back A as subspace of C // add back A as subspace of C
commonTestHelper.runBlockingTest { val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId) spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
}
// A -> C -> A // A -> C -> A
@ -300,7 +268,6 @@ class SpaceHierarchyTest : InstrumentedTest {
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
commonTestHelper,
session, session,
"SpaceA", "SpaceA",
listOf( listOf(
@ -310,7 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceBInfo = createPublicSpace( val spaceBInfo = createPublicSpace(
commonTestHelper,
session, session,
"SpaceB", "SpaceB",
listOf( listOf(
@ -323,13 +289,10 @@ class SpaceHierarchyTest : InstrumentedTest {
// add B as a subspace of A // add B as a subspace of A
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
commonTestHelper.runBlockingTest { spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true) session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
}
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
commonTestHelper,
session, session,
"SpaceC", "SpaceC",
listOf( listOf(
@ -338,52 +301,39 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
) )
commonTestHelper.waitWithLatch { latch -> val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
waitFor(
val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) continueWhen = {
val childObserver = object : Observer<List<RoomSummary>> { session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children ->
override fun onChanged(children: List<RoomSummary>?) { println("## TEST | Space A flat children update : ${children.map { it.name }}")
// Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}") children.any { it.name == "C1" } && children.any { it.name == "C2" }
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)
} }
},
action = {
// add C as subspace of B
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
} }
} )
// C1 and C2 should be in flatten child of A now
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
}
// Test part one of the rooms // Test part one of the rooms
val bRoomId = spaceBInfo.roomIds.first() val bRoomId = spaceBInfo.roomIds.first()
commonTestHelper.waitWithLatch { latch -> waitFor(
val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) continueWhen = {
val childObserver = object : Observer<List<RoomSummary>> { // The room should have disappear from flat children
override fun onChanged(children: List<RoomSummary>?) { session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children ->
System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}") println("## TEST | Space A flat children update : ${children.map { it.name }}")
if (children?.any { it.roomId == bRoomId } == false) { !children.any { it.roomId == bRoomId }
// B1 has been added live!
latch.countDown()
flatAChildren.removeObserver(this)
} }
},
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) commonTestHelper.signOutAndClose(session)
} }
@ -392,68 +342,57 @@ class SpaceHierarchyTest : InstrumentedTest {
val roomIds: List<String> val roomIds: List<String>
) )
private fun createPublicSpace( private suspend fun createPublicSpace(
commonTestHelper: CommonTestHelper,
session: Session, session: Session,
spaceName: String, spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>> childInfo: List<Triple<String, Boolean, Boolean?>>
/** Name, auto-join, canonical*/ /** Name, auto-join, canonical*/
): TestSpaceCreationResult { ): TestSpaceCreationResult {
var spaceId = "" val spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
var roomIds: List<String> = emptyList() val syncedSpace = session.spaceService().getSpace(spaceId)
commonTestHelper.runBlockingTest { val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
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 -> val roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first }) session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
} }
roomIds.forEachIndexed { index, roomId -> roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second) syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third val canonical = childInfo[index].third
if (canonical != null) { if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers) session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
} }
} }
return TestSpaceCreationResult(spaceId, roomIds) return TestSpaceCreationResult(spaceId, roomIds)
} }
private fun createPrivateSpace( private suspend fun createPrivateSpace(
commonTestHelper: CommonTestHelper,
session: Session, session: Session,
spaceName: String, spaceName: String,
childInfo: List<Triple<String, Boolean, Boolean?>> childInfo: List<Triple<String, Boolean, Boolean?>>
/** Name, auto-join, canonical*/ /** Name, auto-join, canonical*/
): TestSpaceCreationResult { ): TestSpaceCreationResult {
var spaceId = "" val spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
var roomIds: List<String> = emptyList() val syncedSpace = session.spaceService().getSpace(spaceId)
commonTestHelper.runBlockingTest { val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false) val roomIds = childInfo.map { entry ->
val syncedSpace = session.spaceService().getSpace(spaceId) val homeServerCapabilities = session
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") .homeServerCapabilitiesService()
roomIds = .getHomeServerCapabilities()
childInfo.map { entry -> session.roomService().createRoom(CreateRoomParams().apply {
val homeServerCapabilities = session name = entry.first
.homeServerCapabilitiesService() this.featurePreset = RestrictedRoomPreset(
.getHomeServerCapabilities() homeServerCapabilities,
session.roomService().createRoom(CreateRoomParams().apply { listOf(
name = entry.first RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
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) {
roomIds.forEachIndexed { index, roomId -> session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
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) return TestSpaceCreationResult(spaceId, roomIds)
@ -464,7 +403,6 @@ class SpaceHierarchyTest : InstrumentedTest {
val session = commonTestHelper.createAccount("John", SessionTestParams(true)) val session = commonTestHelper.createAccount("John", SessionTestParams(true))
/* val spaceAInfo = */ createPublicSpace( /* val spaceAInfo = */ createPublicSpace(
commonTestHelper,
session, "SpaceA", session, "SpaceA",
listOf( listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A1", true /*auto-join*/, true/*canonical*/),
@ -473,7 +411,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceBInfo = createPublicSpace( val spaceBInfo = createPublicSpace(
commonTestHelper,
session, "SpaceB", session, "SpaceB",
listOf( listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B1", true /*auto-join*/, true/*canonical*/),
@ -483,7 +420,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceCInfo = createPublicSpace( val spaceCInfo = createPublicSpace(
commonTestHelper,
session, "SpaceC", session, "SpaceC",
listOf( listOf(
Triple("C1", true /*auto-join*/, true/*canonical*/), Triple("C1", true /*auto-join*/, true/*canonical*/),
@ -494,10 +430,8 @@ class SpaceHierarchyTest : InstrumentedTest {
val viaServers = listOf(session.sessionParams.homeServerHost ?: "") val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
// add C as subspace of B // add C as subspace of B
runBlocking { val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId) spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
}
// Thread.sleep(4_000) // Thread.sleep(4_000)
// + A // + A
@ -507,11 +441,9 @@ class SpaceHierarchyTest : InstrumentedTest {
// + C // + C
// + c1, c2 // + c1, c2
commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val rootSpaces = session.spaceService().getRootSpaceSummaries()
val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() } rootSpaces.size == 2
rootSpaces.size == 2
}
} }
} }
@ -521,7 +453,6 @@ class SpaceHierarchyTest : InstrumentedTest {
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true)) val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
val spaceAInfo = createPrivateSpace( val spaceAInfo = createPrivateSpace(
commonTestHelper,
aliceSession, "Private Space A", aliceSession, "Private Space A",
listOf( listOf(
Triple("General", true /*suggested*/, true/*canonical*/), 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.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList())
}
var bobRoomId = "" commonTestHelper.retryPeriodically {
commonTestHelper.runBlockingTest { val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId))
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" }) stateEvent != null
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
}
} }
// This should be an invalid space parent relation, because no opposite child and bob is not admin of the space // 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
// we can see the state event // but it is not valid and room is not in hierarchy
// 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)
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 // Let's now try to make alice admin of the room
commonTestHelper.waitWithLatch { val room = bobSession.getRoom(bobRoomId)!!
val room = bobSession.getRoom(bobRoomId)!! val currentPLContent = room
val currentPLContent = room .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
.toModel<PowerLevelsContent>()
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) .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content ?.content
.toModel<PowerLevelsContent>() ?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
val newPowerLevelsContent = currentPLContent powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value)
?.toContent()
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
it.countDown()
} }
commonTestHelper.waitWithLatch { latch -> aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
}
}
commonTestHelper.waitWithLatch { commonTestHelper.retryPeriodically {
aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: "")) bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
it.countDown()
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
}
} }
} }
@ -616,7 +520,6 @@ class SpaceHierarchyTest : InstrumentedTest {
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true)) val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
val spaceAInfo = createPublicSpace( val spaceAInfo = createPublicSpace(
commonTestHelper,
aliceSession, "SpaceA", aliceSession, "SpaceA",
listOf( listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/), Triple("A1", true /*auto-join*/, true/*canonical*/),
@ -625,7 +528,6 @@ class SpaceHierarchyTest : InstrumentedTest {
) )
val spaceBInfo = createPublicSpace( val spaceBInfo = createPublicSpace(
commonTestHelper,
aliceSession, "SpaceB", aliceSession, "SpaceB",
listOf( listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/), Triple("B1", true /*auto-join*/, true/*canonical*/),
@ -641,51 +543,39 @@ class SpaceHierarchyTest : InstrumentedTest {
val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId) val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId)
val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.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.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
val roomSummary = aliceSession.getRoomSummary(B1roomId) roomSummary != null &&
roomSummary != null && roomSummary.directParentNames.size == 1 &&
roomSummary.directParentNames.size == 2 && roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name)
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)
}
} }
val newAName = "FooBar" 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.retryPeriodically {
commonTestHelper.retryPeriodicallyWithLatch(latch) { val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
val roomSummary = aliceSession.getRoomSummary(B1roomId) roomSummary != null &&
roomSummary != null && roomSummary.directParentNames.size == 1 &&
roomSummary.directParentNames.size == 2 && roomSummary.directParentNames.contains(newAName)
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)
}
} }
} }
} }