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.androidx.coreTesting
androidTestImplementation libs.jetbrains.coroutinesAndroid
androidTestImplementation libs.jetbrains.coroutinesTest
// Plant Timber tree for test
androidTestImplementation libs.tests.timberJunitRule

View file

@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Change password
commonTestHelper.runBlockingTest {
session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD)
}
// Try to login with the previous password, it will fail
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)

View file

@ -44,7 +44,6 @@ class DeactivateAccountTest : InstrumentedTest {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Deactivate the account
commonTestHelper.runBlockingTest {
session.accountService().deactivateAccount(
eraseAllData = false,
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
@ -59,7 +58,6 @@ class DeactivateAccountTest : InstrumentedTest {
}
}
)
}
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
@ -74,12 +72,9 @@ class DeactivateAccountTest : InstrumentedTest {
// Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE)
val hs = commonTestHelper.createHomeServerConfig()
commonTestHelper.runBlockingTest {
commonTestHelper.matrix.authenticationService.getLoginFlow(hs)
}
var accountCreationError: Throwable? = null
commonTestHelper.runBlockingTest {
try {
commonTestHelper.matrix.authenticationService
.getRegistrationWizard()
@ -91,7 +86,6 @@ class DeactivateAccountTest : InstrumentedTest {
} catch (failure: Throwable) {
accountCreationError = failure
}
}
// Test the error
accountCreationError.let {

View file

@ -19,18 +19,16 @@ package org.matrix.android.sdk.common
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.Observer
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
@ -51,12 +49,12 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.sync.SyncState
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* This class exposes methods to be used in common cases
@ -65,22 +63,32 @@ import java.util.concurrent.TimeUnit
class CommonTestHelper internal constructor(context: Context) {
companion object {
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
return try {
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try {
withContext(Dispatchers.Default) {
block(testHelper)
}
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
}
}
}
}
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) {
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
val cryptoTestHelper = CryptoTestHelper(testHelper)
return try {
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try {
withContext(Dispatchers.Default) {
block(cryptoTestHelper, testHelper)
}
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
@ -88,9 +96,9 @@ class CommonTestHelper internal constructor(context: Context) {
}
}
}
}
internal val matrix: TestMatrix
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var accountNumber = 0
private val trackedSessions = mutableListOf<Session>()
@ -112,20 +120,18 @@ class CommonTestHelper internal constructor(context: Context) {
matrix = _matrix!!
}
fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams)
}
fun logIntoAccount(userId: String, testParams: SessionTestParams): Session {
suspend fun logIntoAccount(userId: String, testParams: SessionTestParams): Session {
return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
}
fun cleanUpOpenedSessions() {
suspend fun cleanUpOpenedSessions() {
trackedSessions.forEach {
runBlockingTest {
it.signOutService().signOut(true)
}
}
trackedSessions.clear()
}
@ -138,27 +144,10 @@ class CommonTestHelper internal constructor(context: Context) {
.build()
}
/**
* This methods init the event stream and check for initial sync
*
* @param session the session to sync
*/
fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) {
val lock = CountDownLatch(1)
coroutineScope.launch {
suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) {
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)
syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() }
}
/**
@ -166,22 +155,11 @@ class CommonTestHelper internal constructor(context: Context) {
*
* @param session the session to sync
*/
fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) {
waitWithLatch(timeout) { latch ->
suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) {
session.clearCache()
val syncLiveData = session.syncService().getSyncStateLive()
val syncObserver = object : Observer<SyncState> {
override fun onChanged(t: SyncState?) {
if (session.syncService().hasAlreadySynced()) {
syncSession(session, timeout)
session.syncService().getSyncStateLive().first(timeout) { 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 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))
timeline.start()
val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
@ -204,23 +182,16 @@ class CommonTestHelper internal constructor(context: Context) {
/**
* Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
*/
private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<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)
(1 until count + 1)
.map { "$message #$it" }
.chunked(10)
.forEach { batchedMessages ->
batchedMessages.forEach { formattedMessage ->
if (rootThreadEventId != null) {
room.relationService().replyInThread(
rootThreadEventId = rootThreadEventId,
replyInThreadText = formattedMessage
)
} else {
room.sendService().sendTextMessage(formattedMessage)
}
}
waitWithLatch(timeout) { latch ->
waitFor(
continueWhen = {
wrapWithTimeout(timeout) {
suspendCoroutine<Unit> { continuation ->
val timelineListener = object : Timeline.Listener {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
@ -240,19 +211,33 @@ class CommonTestHelper internal constructor(context: Context) {
}
if (hasSyncedAllBatchedMessages) {
timeline.removeListener(this)
latch.countDown()
continuation.resume(Unit)
}
}
}
timeline.addListener(timelineListener)
}
}
},
action = {
batchedMessages.forEach { formattedMessage ->
if (rootThreadEventId != null) {
room.relationService().replyInThread(
rootThreadEventId = rootThreadEventId,
replyInThreadText = formattedMessage
)
} else {
room.sendService().sendTextMessage(formattedMessage)
}
}
}
)
}
return sentEvents
}
fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
waitWithLatch { latch ->
retryPeriodicallyWithLatch(latch) {
suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
retryPeriodically {
val roomSummary = otherSession.getRoomSummary(roomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
@ -260,10 +245,9 @@ class CommonTestHelper internal constructor(context: Context) {
}
}
}
}
// not sure why it's taking so long :/
runBlockingTest(90_000) {
wrapWithTimeout(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
try {
otherSession.roomService().joinRoom(roomID)
@ -273,13 +257,11 @@ class CommonTestHelper internal constructor(context: Context) {
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
waitWithLatch {
retryPeriodicallyWithLatch(it) {
retryPeriodically {
val roomSummary = otherSession.getRoomSummary(roomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
/**
* Reply in a thread
@ -287,7 +269,7 @@ class CommonTestHelper internal constructor(context: Context) {
* @param message the message to send
* @param numberOfMessages the number of time the message will be sent
*/
fun replyInThreadMessage(
suspend fun replyInThreadMessage(
room: Room,
message: String,
numberOfMessages: Int,
@ -305,15 +287,7 @@ class CommonTestHelper internal constructor(context: Context) {
// PRIVATE METHODS *****************************************************************************
/**
* Creates a unique account
*
* @param userNamePrefix the user name prefix
* @param password the password
* @param testParams test params about the session
* @return the session associated with the newly created account
*/
private fun createAccount(
private suspend fun createAccount(
userNamePrefix: String,
password: String,
testParams: SessionTestParams
@ -331,15 +305,7 @@ class CommonTestHelper internal constructor(context: Context) {
}
}
/**
* Logs into an existing account
*
* @param userId the userId to log in
* @param password the password to log in
* @param testParams test params about the session
* @return the session associated with the existing account
*/
fun logIntoAccount(
suspend fun logIntoAccount(
userId: String,
password: String,
testParams: SessionTestParams
@ -351,32 +317,25 @@ class CommonTestHelper internal constructor(context: Context) {
}
}
/**
* Create an account and a dedicated session
*
* @param userName the account username
* @param password the password
* @param sessionTestParams parameters for the test
*/
private fun createAccountAndSync(
private suspend fun createAccountAndSync(
userName: String,
password: String,
sessionTestParams: SessionTestParams
): Session {
val hs = createHomeServerConfig()
runBlockingTest {
wrapWithTimeout(TestConstants.timeOutMillis) {
matrix.authenticationService.getLoginFlow(hs)
}
runBlockingTest(timeout = 60_000) {
wrapWithTimeout(60_000L) {
matrix.authenticationService
.getRegistrationWizard()
.createAccount(userName, password, null)
}
// Perform dummy step
val registrationResult = runBlockingTest(timeout = 60_000) {
val registrationResult = wrapWithTimeout(timeout = 60_000) {
matrix.authenticationService
.getRegistrationWizard()
.dummy()
@ -391,29 +350,14 @@ class CommonTestHelper internal constructor(context: Context) {
return session
}
/**
* Start an account login
*
* @param userName the account username
* @param password the password
* @param sessionTestParams session test params
*/
private fun logAccountAndSync(
userName: String,
password: String,
sessionTestParams: SessionTestParams
): Session {
private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): Session {
val hs = createHomeServerConfig()
runBlockingTest {
matrix.authenticationService.getLoginFlow(hs)
}
val session = runBlockingTest {
matrix.authenticationService
val session = matrix.authenticationService
.getLoginWizard()
.login(userName, password, "myDevice")
}
session.open()
if (sessionTestParams.withInitialSync) {
syncSession(session)
@ -428,18 +372,15 @@ class CommonTestHelper internal constructor(context: Context) {
* @param userName the account username
* @param password the password
*/
fun logAccountWithError(
suspend fun logAccountWithError(
userName: String,
password: String
): Throwable {
val hs = createHomeServerConfig()
runBlockingTest {
matrix.authenticationService.getLoginFlow(hs)
}
var requestFailure: Throwable? = null
runBlockingTest {
try {
matrix.authenticationService
.getLoginWizard()
@ -447,7 +388,6 @@ class CommonTestHelper internal constructor(context: Context) {
} catch (failure: Throwable) {
requestFailure = failure
}
}
assertNotNull(requestFailure)
return requestFailure!!
@ -482,65 +422,48 @@ class CommonTestHelper internal constructor(context: Context) {
)
}
suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
while (true) {
try {
delay(1000)
} catch (ex: CancellationException) {
// the job was canceled, just stop
return
}
if (condition()) {
latch.countDown()
return
suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) {
wrapWithTimeout(timeout) {
while (!predicate()) {
runBlocking { delay(500) }
}
}
}
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
val job = coroutineScope.launch(dispatcher) {
block(latch)
}
await(latch, timeout, job)
}
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
return runBlocking {
withTimeout(timeout) {
block()
}
}
}
// Transform a method with a MatrixCallback to a synchronous method
inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
val lock = CountDownLatch(1)
var result: T? = null
val callback = object : TestMatrixCallback<T>(lock) {
suspend fun <T> waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
return wrapWithTimeout(timeout) {
suspendCoroutine { continuation ->
val callback = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
result = data
super.onSuccess(data)
continuation.resume(data)
}
}
block(callback)
}
}
}
block.invoke(callback)
await(lock, timeout)
assertNotNull(result)
return result!!
suspend fun <T> waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): Throwable {
return wrapWithTimeout(timeout) {
suspendCoroutine { continuation ->
val callback = object : MatrixCallback<T> {
override fun onFailure(failure: Throwable) {
continuation.resume(failure)
}
}
block(callback)
}
}
}
/**
* 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)
runBlockingTest(timeout = 60_000) {
wrapWithTimeout(timeout = 60_000L) {
session.signOutService().signOut(true)
}
// no need signout will close

View file

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

View file

@ -16,19 +16,15 @@
package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.Observer
import org.amshove.kluent.fail
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
@ -46,22 +42,16 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
import org.matrix.android.sdk.api.session.securestorage.KeyRef
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID
import kotlin.coroutines.Continuation
@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/**
* @return alice session
*/
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
suspend fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = testHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply {
val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
historyVisibility = roomHistoryVisibility
name = "MyRoom"
})
}
if (encryptedRoom) {
testHelper.waitWithLatch { latch ->
val room = aliceSession.getRoom(roomId)!!
room.roomCryptoService().enableEncryption()
val roomSummaryLive = room.getRoomSummaryLive()
val roomSummaryObserver = object : Observer<Optional<RoomSummary>> {
override fun onChanged(roomSummary: Optional<RoomSummary>) {
if (roomSummary.getOrNull()?.isEncrypted.orFalse()) {
roomSummaryLive.removeObserver(this)
latch.countDown()
}
}
}
roomSummaryLive.observeForever(roomSummaryObserver)
}
waitFor(
continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } },
action = { room.roomCryptoService().enableEncryption() }
)
}
return CryptoTestData(roomId, listOf(aliceSession))
}
@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/**
* @return alice and bob sessions
*/
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
suspend fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
testHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (t?.isNotEmpty() == true) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
aliceRoom.membershipService().invite(bobSession.myUserId)
}
waitFor(
continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } },
action = { aliceRoom.membershipService().invite(bobSession.myUserId) }
)
testHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
val roomJoinedObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (bobSession.getRoom(aliceRoomId)
waitFor(
continueWhen = {
bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first {
bobSession.getRoom(aliceRoomId)
?.membershipService()
?.getRoomMember(bobSession.myUserId)
?.membership == Membership.JOIN) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
}
}
}
bobRoomSummariesLive.observeForever(roomJoinedObserver)
bobSession.roomService().joinRoom(aliceRoomId)
?.membership == Membership.JOIN
}
},
action = { bobSession.roomService().joinRoom(aliceRoomId) }
)
// Ensure bob can send messages to the room
// val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
// assertNotNull(roomFromBobPOV.powerLevels)
@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession))
}
/**
* @return Alice, Bob and Sam session
*/
fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val room = aliceSession.getRoom(aliceRoomId)!!
val samSession = createSamAccountAndInviteToTheRoom(room)
// wait the initial sync
SystemClock.sleep(1000)
return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession))
}
/**
* Create Sam account and invite him in the room. He will accept the invitation
* @Return Sam session
*/
fun createSamAccountAndInviteToTheRoom(room: Room): Session {
val samSession = testHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
testHelper.runBlockingTest {
room.membershipService().invite(samSession.myUserId, null)
}
testHelper.runBlockingTest {
samSession.roomService().joinRoom(room.roomId, null, emptyList())
}
return samSession
}
/**
* @return Alice and Bob sessions
*/
fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
@ -235,9 +165,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
return cryptoTestData
}
private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
testHelper.retryPeriodically {
val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
if (andCanDecrypt) {
timeLineEvent != null &&
@ -248,36 +177,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
}
}
}
}
fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
assertEquals(EventType.ENCRYPTED, event.type)
assertNotNull(event.content)
val eventWireContent = event.content.toContent()
assertNotNull(eventWireContent)
assertNull(eventWireContent["body"])
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
assertNotNull(eventWireContent["ciphertext"])
assertNotNull(eventWireContent["session_id"])
assertNotNull(eventWireContent["sender_key"])
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
assertNotNull(event.eventId)
assertEquals(roomId, event.roomId)
assertEquals(EventType.MESSAGE, event.getClearType())
// TODO assertTrue(event.getAge() < 10000)
val eventContent = event.toContent()
assertNotNull(eventContent)
assertEquals(clearMessage, eventContent["body"])
assertEquals(senderSession.myUserId, event.senderId)
}
fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
return MegolmBackupAuthData(
publicKey = "abcdefg",
signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop"))
@ -292,44 +193,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
)
}
fun createDM(alice: Session, bob: Session): String {
var roomId: String = ""
testHelper.waitWithLatch { latch ->
roomId = alice.roomService().createDirectRoom(bob.myUserId)
val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (t?.any { it.roomId == roomId }.orFalse()) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
}
suspend fun createDM(alice: Session, bob: Session): String {
var roomId = ""
waitFor(
continueWhen = {
bob.roomService()
.onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }
.first { it.any { it.roomId == roomId }.orFalse() }
},
action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) }
)
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)
waitFor(
continueWhen = {
bob.roomService()
.onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }
.first {
bob.getRoom(roomId)
?.membershipService()
?.getRoomMember(bob.myUserId)
?.membership == Membership.JOIN) {
bobRoomSummariesLive.removeObserver(this)
latch.countDown()
?.membership == Membership.JOIN
}
}
}
bobRoomSummariesLive.observeForever(newRoomObserver)
bob.roomService().joinRoom(roomId)
}
},
action = { bob.roomService().joinRoom(roomId) }
)
return roomId
}
fun initializeCrossSigning(session: Session) {
testHelper.doSync<Unit> {
suspend fun initializeCrossSigning(session: Session) {
testHelper.waitForCallback<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
@ -350,10 +242,9 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
/**
* Initialize cross-signing, set up megolm backup and save all in 4S
*/
fun bootstrapSecurity(session: Session) {
suspend fun bootstrapSecurity(session: Session) {
initializeCrossSigning(session)
val ssssService = session.sharedSecretStorageService()
testHelper.runBlockingTest {
val keyInfo = ssssService.generateKey(
UUID.randomUUID().toString(),
null,
@ -381,10 +272,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
)
// set up megolm backup
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
val creationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
val version = testHelper.waitForCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
@ -398,9 +289,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
)
}
}
}
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
@ -415,13 +305,11 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
roomId = roomId
).transactionId
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
} != null
}
}
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
}
@ -429,8 +317,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
var requestID: String? = null
// wait for it to be readied
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
.firstOrNull { it.localId == localId }
if (outgoingRequest?.isReady == true) {
@ -440,7 +327,6 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
false
}
}
}
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
@ -454,16 +340,13 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
}
// wait for alice to get the ready
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
@ -471,45 +354,36 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
}
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
}
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
bobPovTx!!.userHasVerifiedShortCode()
alicePovTx!!.userHasVerifiedShortCode()
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
testHelper.retryPeriodically {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
}
fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {
suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
val roomId = testHelper.runBlockingTest {
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
}
val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
val room = aliceSession.getRoom(roomId)!!
testHelper.runBlockingTest {
room.roomCryptoService().enableEncryption()
}
val sessions = mutableListOf(aliceSession)
for (index in 1 until numberOfMembers) {
val session = testHelper.createAccount("User_$index", defaultSessionParams)
testHelper.runBlockingTest(timeout = 600_000) { room.membershipService().invite(session.myUserId, null) }
room.membershipService().invite(session.myUserId, null)
println("TEST -> " + session.myUserId + " invited")
testHelper.runBlockingTest { session.roomService().joinRoom(room.roomId, null, emptyList()) }
session.roomService().joinRoom(room.roomId, null, emptyList())
println("TEST -> " + session.myUserId + " joined")
sessions.add(session)
}
@ -517,12 +391,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
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 ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
@ -535,19 +407,16 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
} catch (error: MXCryptoError) {
// nop
}
}
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 ->
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "")
fail("Should not be able to decrypt event")
@ -561,5 +430,4 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
}
}
}
}
}

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

View file

@ -18,12 +18,16 @@ package org.matrix.android.sdk.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@ -57,12 +61,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.mustFail
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.resume
// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
@RunWith(JUnit4::class)
@ -70,8 +71,6 @@ import java.util.concurrent.CountDownLatch
@LargeTest
class E2eeSanityTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
/**
* Simple test that create an e2ee room.
* Some new members are added, and a message is sent.
@ -104,11 +103,9 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "All accounts created")
// we want to invite them in the room
otherAccounts.forEach {
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.membershipService().invite(it.myUserId)
}
}
// All user should accept invite
otherAccounts.forEach { otherSession ->
@ -129,15 +126,13 @@ class E2eeSanityTests : InstrumentedTest {
// All should be able to decrypt
otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Add a new user to the room, and check that he can't decrypt
val newAccount = listOf("adam") // , "adam", "manu")
@ -146,11 +141,9 @@ class E2eeSanityTests : InstrumentedTest {
}
newAccount.forEach {
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.membershipService().invite(it.myUserId)
}
}
newAccount.forEach {
testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
@ -159,14 +152,11 @@ class E2eeSanityTests : InstrumentedTest {
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit
testHelper.runBlockingTest {
delay(3_000)
}
// check that messages are encrypted (uisi)
newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
@ -175,7 +165,6 @@ class E2eeSanityTests : InstrumentedTest {
timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
}
}
}
// Let alice send a new message
Log.v("#E2E TEST", "Alice sends a new message")
@ -185,8 +174,7 @@ class E2eeSanityTests : InstrumentedTest {
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
@ -196,7 +184,6 @@ class E2eeSanityTests : InstrumentedTest {
}
}
}
}
@Test
fun testKeyGossipingIsEnabledByDefault() = runSessionTest(context()) { testHelper ->
@ -229,10 +216,10 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Create and start key backup for bob ...")
val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = testHelper.doSync<KeysVersion> {
val version = testHelper.waitForCallback<KeysVersion> {
bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
Log.v("#E2E TEST", "... Key backup started and enabled for bob")
@ -248,32 +235,21 @@ class E2eeSanityTests : InstrumentedTest {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
// we want more so let's discard the session
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
testHelper.runBlockingTest {
delay(1_000)
}
}
Log.v("#E2E TEST", "Bob received all and can decrypt")
// Let's wait a bit to be sure that bob has backed up the session
Log.v("#E2E TEST", "Force key backup for Bob...")
testHelper.waitWithLatch { latch ->
bobKeysBackupService.backupAllGroupSessions(
null,
TestMatrixCallback(latch, true)
)
}
testHelper.waitForCallback<Unit> { bobKeysBackupService.backupAllGroupSessions(null, it) }
Log.v("#E2E TEST", "... Key backup done for Bob")
// Now lets logout both alice and bob to ensure that we won't have any gossiping
@ -284,9 +260,7 @@ class E2eeSanityTests : InstrumentedTest {
testHelper.signOutAndClose(bobSession)
Log.v("#E2E TEST", "..Logout alice and bob...")
testHelper.runBlockingTest {
delay(1_000)
}
// Create a new session for bob
Log.v("#E2E TEST", "Create a new session for Bob")
@ -295,14 +269,11 @@ class E2eeSanityTests : InstrumentedTest {
// check that bob can't currently decrypt
Log.v("#E2E TEST", "check that bob can't currently decrypt")
sentEventIds.forEach { sentEventId ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
}
timelineEvent != null &&
timelineEvent.root.getClearType() == EventType.ENCRYPTED
}
timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED
}
}
// after initial sync events are not decrypted, so we have to try manually
@ -311,11 +282,11 @@ class E2eeSanityTests : InstrumentedTest {
// Let's now import keys from backup
newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
val keyVersionResult = testHelper.waitForCallback<KeysVersionResult?> {
kbs.getVersion(version.version, it)
}
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
val importedResult = testHelper.waitForCallback<ImportRoomKeysResult> {
kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
@ -357,15 +328,13 @@ class E2eeSanityTests : InstrumentedTest {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
@ -399,8 +368,7 @@ class E2eeSanityTests : InstrumentedTest {
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
.getTimelineEvent(sentEventId)!!
.root.content.toModel<EncryptedEventContent>()!!.sessionId
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
.first {
it.sessionId == megolmSessionId &&
@ -416,7 +384,6 @@ class E2eeSanityTests : InstrumentedTest {
WithHeldCode.UNAUTHORISED == aliceReply.code
}
}
}
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
@ -455,15 +422,13 @@ class E2eeSanityTests : InstrumentedTest {
firstMessage.let { text ->
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
@ -483,15 +448,13 @@ class E2eeSanityTests : InstrumentedTest {
secondMessage.let { text ->
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// check that both messages have same sessionId (it's just that we don't have index 0)
val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
@ -503,19 +466,15 @@ class E2eeSanityTests : InstrumentedTest {
Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
// Confirm we can decrypt one but not the other
testHelper.runBlockingTest {
mustFail(message = "Should not be able to decrypt event") {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
}
}
testHelper.runBlockingTest {
try {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
} catch (error: MXCryptoError) {
fail("Should be able to decrypt event")
}
}
// Now let's verify bobs session, and re-request keys
bobSessionWithBetterKey.cryptoService()
@ -533,20 +492,15 @@ class E2eeSanityTests : InstrumentedTest {
// old session should have shared the key at earliest known index now
// we should be able to decrypt both
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
val canDecryptFirst = try {
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
}
val canDecryptSecond = try {
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
@ -554,15 +508,14 @@ class E2eeSanityTests : InstrumentedTest {
canDecryptFirst && canDecryptSecond
}
}
}
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendService().sendTextMessage(text)
private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
var sentEventId: String? = null
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
aliceRoomPOV.sendService().sendTextMessage(text)
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also { list ->
@ -574,9 +527,7 @@ class E2eeSanityTests : InstrumentedTest {
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
}
timeline.dispose()
}
return sentEventId
}
@ -593,107 +544,36 @@ class E2eeSanityTests : InstrumentedTest {
val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val oldCompleteLatch = CountDownLatch(1)
lateinit var oldCode: String
aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
val readyInfo = pr.readyInfo
if (readyInfo != null) {
aliceSession.cryptoService().verificationService().beginKeyVerification(
VerificationMethod.SAS,
aliceSession.myUserId,
readyInfo.fromDevice,
readyInfo.transactionId
)
}
}
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "exitsingPov: $tx")
val sasTx = tx as OutgoingSasVerificationTransaction
when (sasTx.uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
// for the test we just accept?
oldCode = sasTx.getDecimalCodeRepresentation()
sasTx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
// we can release this latch?
oldCompleteLatch.countDown()
}
else -> Unit
}
}
})
val newCompleteLatch = CountDownLatch(1)
lateinit var newCode: String
aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// let's ready
aliceNewSession.cryptoService().verificationService().readyPendingVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceSession.myUserId,
pr.transactionId!!
)
}
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "newPov: $tx")
val sasTx = tx as IncomingSasVerificationTransaction
when (sasTx.uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
// no need to accept as there was a request first it will auto accept
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
sasTx.userHasVerifiedShortCode()
newCode = sasTx.getDecimalCodeRepresentation()
matchOnce = false
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
newCompleteLatch.countDown()
}
else -> Unit
}
}
})
val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId)
val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId)
// initiate self verification
aliceSession.cryptoService().verificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceNewSession.myUserId,
listOf(aliceNewSession.sessionParams.deviceId!!)
)
testHelper.await(oldCompleteLatch)
testHelper.await(newCompleteLatch)
val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode)
assertEquals("Decimal code should have matched", oldCode, newCode)
// Assert that devices are verified
val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
val newDeviceFromOldPov: CryptoDeviceInfo? =
aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
val oldDeviceFromNewPov: CryptoDeviceInfo? =
aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
// wait for secret gossiping to happen
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
}
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
}
}
assertEquals(
"MSK Private parts should be the same",
@ -725,9 +605,97 @@ class E2eeSanityTests : InstrumentedTest {
)
}
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
return scope.async {
suspendCancellableCoroutine { continuation ->
var oldCode: String? = null
val listener = object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
val readyInfo = pr.readyInfo
if (readyInfo != null) {
beginKeyVerification(
VerificationMethod.SAS,
userId,
readyInfo.fromDevice,
readyInfo.transactionId
)
}
}
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "exitsingPov: $tx")
val sasTx = tx as OutgoingSasVerificationTransaction
when (sasTx.uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
// for the test we just accept?
oldCode = sasTx.getDecimalCodeRepresentation()
sasTx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
removeListener(this)
// we can release this latch?
continuation.resume(oldCode!!)
}
else -> Unit
}
}
}
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
}
}
}
private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
return scope.async {
suspendCancellableCoroutine { continuation ->
var newCode: String? = null
val listener = object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// let's ready
readyPendingVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
userId,
pr.transactionId!!
)
}
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "newPov: $tx")
val sasTx = tx as IncomingSasVerificationTransaction
when (sasTx.uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
// no need to accept as there was a request first it will auto accept
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
sasTx.userHasVerifiedShortCode()
newCode = sasTx.getDecimalCodeRepresentation()
matchOnce = false
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
removeListener(this)
continuation.resume(newCode!!)
}
else -> Unit
}
}
}
addListener(listener)
continuation.invokeOnCancellation { removeListener(listener) }
}
}
}
private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.retryPeriodically {
otherAccounts.map {
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
@ -735,12 +703,10 @@ class E2eeSanityTests : InstrumentedTest {
}
}
}
}
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
@ -748,5 +714,4 @@ class E2eeSanityTests : InstrumentedTest {
}
}
}
}
}

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.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.wrapWithTimeout
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -101,8 +101,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
// Bob should be able to decrypt the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
@ -112,7 +111,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
// Create a new user
val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also {
@ -121,10 +119,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Log.v("#E2E TEST", "Aris user created")
// Alice invites new user to the room
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
aliceRoomPOV.membershipService().invite(arisSession.myUserId)
}
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
@ -137,8 +133,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
null
-> {
// Aris should be able to decrypt the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
@ -150,12 +145,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
RoomHistoryVisibility.INVITED,
RoomHistoryVisibility.JOINED -> {
// Aris should not even be able to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
@ -163,7 +156,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
testHelper.signOutAndClose(arisSession)
cryptoTestData.cleanUp(testHelper)
@ -238,10 +230,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
private fun testRotationDueToVisibilityChange(
initRoomHistoryVisibility: RoomHistoryVisibility,
nextRoomHistoryVisibility: RoomHistoryVisibilityContent
) {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
@ -267,8 +256,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
// Bob should be able to decrypt the message
var firstAliceMessageMegolmSessionId: String? = null
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(aliceMessageId!!)
@ -284,14 +272,12 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
var secondAliceMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(secondMessage)
@ -308,12 +294,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId)
Log.v("#E2E TEST ROTATION", "No rotation needed yet")
// Let's change the room history visibility
testHelper.runBlockingTest {
aliceRoomPOV.stateService()
.sendStateEvent(
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
@ -322,18 +306,14 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
).toContent()
)
}
// ensure that the state did synced down
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
.stateService()
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
@ -342,12 +322,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
}
}
var aliceThirdMessageSessionId: String? = null
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timelineEvent = bobRoomPov
?.timelineService()
?.getTimelineEvent(thirdMessage)
@ -360,7 +338,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
when {
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
@ -376,13 +353,12 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
}
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.retryPeriodically {
otherAccounts.map {
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
@ -390,11 +366,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
testHelper.retryPeriodically {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
@ -402,9 +376,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
}
}
}
testHelper.runBlockingTest(60_000) {
wrapWithTimeout(60_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
@ -414,11 +387,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
testHelper.retryPeriodically {
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
}

View file

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

View file

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

View file

@ -55,7 +55,7 @@ class XSigningTest : InstrumentedTest {
fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
@ -101,14 +101,14 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
@ -117,7 +117,7 @@ class XSigningTest : InstrumentedTest {
}
// 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)
assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey())
@ -154,14 +154,14 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
@ -171,12 +171,12 @@ class XSigningTest : InstrumentedTest {
// Check that alice can see bob keys
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)
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
// We will want to test that in alice POV, this new device would be trusted by cross signing
@ -185,7 +185,7 @@ class XSigningTest : InstrumentedTest {
val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
// Check that bob first session sees the new login
val data = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
val data = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
}
@ -197,12 +197,12 @@ class XSigningTest : InstrumentedTest {
assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
// Manually mark it as trusted from first session
testHelper.doSync<Unit> {
testHelper.waitForCallback<Unit> {
bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it)
}
// 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)
}

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.crypto.encryption
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder
import org.junit.Test
@ -34,19 +34,18 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestHelper
import java.util.concurrent.CountDownLatch
import org.matrix.android.sdk.common.waitFor
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class EncryptionTest : InstrumentedTest {
@Test
fun test_EncryptionEvent() {
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room ->
fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room ->
// Send an encryption Event as an Event (and not as a state event)
room.sendService().sendEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
@ -54,13 +53,10 @@ class EncryptionTest : InstrumentedTest {
)
}
}
}
@Test
fun test_EncryptionStateEvent() {
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room ->
runBlocking {
fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room ->
// Send an encryption Event as a State Event
room.stateService().sendStateEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
@ -69,19 +65,28 @@ class EncryptionTest : InstrumentedTest {
)
}
}
}
}
private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
private suspend fun performTest(cryptoTestHelper: CryptoTestHelper, roomShouldBeEncrypted: Boolean, action: suspend (Room) -> Unit) {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
val aliceSession = cryptoTestData.firstSession
val room = aliceSession.getRoom(cryptoTestData.roomId)!!
room.roomCryptoService().isEncrypted() shouldBe false
val timeline = room.timelineService().createTimeline(null, TimelineSettings(10))
val latch = CountDownLatch(1)
timeline.start()
waitFor(
continueWhen = { timeline.waitForEncryptedMessages() },
action = { action.invoke(room) }
)
timeline.dispose()
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
}
}
private suspend fun Timeline.waitForEncryptedMessages() {
suspendCancellableCoroutine<Unit> { continuation ->
val timelineListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
}
@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest {
.filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION }
if (newMessages.isNotEmpty()) {
timeline.removeListener(this)
latch.countDown()
removeListener(this)
continuation.resume(Unit)
}
}
}
timeline.start()
timeline.addListener(timelineListener)
action.invoke(room)
testHelper.await(latch)
timeline.dispose()
testHelper.waitWithLatch {
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
it.countDown()
}
addListener(timelineListener)
continuation.invokeOnCancellation { removeListener(timelineListener) }
}
}

View file

@ -63,14 +63,12 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message
val roomId = commonTestHelper.runBlockingTest {
aliceSession.roomService().createRoom(
val roomId = aliceSession.roomService().createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
}
)
}
val room = aliceSession.getRoom(roomId)
assertNotNull(room)
Thread.sleep(4_000)
@ -94,11 +92,9 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted())
commonTestHelper.runBlockingTest {
mustFail {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
}
}
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
@ -111,8 +107,7 @@ class KeyShareTests : InstrumentedTest {
var outGoingRequestId: String? = null
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
@ -120,7 +115,6 @@ class KeyShareTests : InstrumentedTest {
outgoing != null
}
}
}
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@ -131,8 +125,7 @@ class KeyShareTests : InstrumentedTest {
// The first session should see an incoming request
// the request should be refused, because the device is not trusted
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
// DEBUG LOGS
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
@ -146,10 +139,8 @@ class KeyShareTests : InstrumentedTest {
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming != null
}
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
// DEBUG LOGS
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
@ -163,13 +154,10 @@ class KeyShareTests : InstrumentedTest {
val resultCode = (reply?.result as? RequestResult.Failure)?.code
resultCode == WithHeldCode.UNVERIFIED
}
}
commonTestHelper.runBlockingTest {
mustFail {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
}
}
// Mark the device as trusted
aliceSession.cryptoService().setDeviceVerification(
@ -210,14 +198,12 @@ class KeyShareTests : InstrumentedTest {
// As it was share previously alice should accept to reshare
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
aliceReply != null && aliceReply.result is RequestResult.Success
}
}
}
/**
* Test that our own devices accept to reshare to unverified device if it was shared initialy
@ -233,13 +219,11 @@ class KeyShareTests : InstrumentedTest {
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait for alice first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
@ -247,15 +231,13 @@ class KeyShareTests : InstrumentedTest {
// As it was share previously alice should accept to reshare
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply =
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
}
}
}
/**
* Tests that keys reshared with own verified session are done from the earliest known index
@ -277,13 +259,11 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.syncSession(aliceNewSession)
// we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
val newEventId = newEvent.eventId
@ -304,8 +284,7 @@ class KeyShareTests : InstrumentedTest {
aliceNewSession.cryptoService().enableKeyGossiping(true)
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply = outgoing?.results
?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
@ -313,10 +292,8 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "own device result is $result")
result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
}
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobDeviceReply = outgoing?.results
?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
@ -324,7 +301,6 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "bob device result is $result")
result != null && result is RequestResult.Success && result.chainIndex > 0
}
}
// it's a success but still can't decrypt first message
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
@ -337,8 +313,7 @@ class KeyShareTests : InstrumentedTest {
// Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
// DEBUG LOGS
aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
@ -352,7 +327,6 @@ class KeyShareTests : InstrumentedTest {
val result = ownDeviceReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 0
}
}
// now the new session should be able to decrypt all!
cryptoTestHelper.ensureCanDecrypt(
@ -363,14 +337,12 @@ class KeyShareTests : InstrumentedTest {
)
// Additional test, can we check that bob replied successfully but with a ratcheted key
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val result = bobReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 3
}
}
commonTestHelper.signOutAndClose(aliceNewSession)
commonTestHelper.signOutAndClose(aliceSession)
@ -394,13 +366,11 @@ class KeyShareTests : InstrumentedTest {
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
val newEventId = newEvent.eventId
@ -430,15 +400,13 @@ class KeyShareTests : InstrumentedTest {
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
// Should get a reply from bob and not from alice
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val result = bobReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 3
}
}
val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
@ -450,15 +418,13 @@ class KeyShareTests : InstrumentedTest {
aliceSession.syncService().startSync(true)
// We should now get a reply from first session
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.retryPeriodically {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply =
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val result = ownDeviceReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 0
}
}
// It should be in sent then cancel
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }

View file

@ -80,11 +80,9 @@ class WithHeldTests : InstrumentedTest {
val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
// await for bob unverified session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
}
}
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
@ -95,7 +93,6 @@ class WithHeldTests : InstrumentedTest {
// Bob should not be able to decrypt because the keys is withheld
// .. might need to wait a bit for stability?
testHelper.runBlockingTest {
mustFail(
message = "This session should not be able to decrypt",
failureBlock = { failure ->
@ -107,11 +104,9 @@ class WithHeldTests : InstrumentedTest {
) {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
}
// Let's see if the reply we got from bob first session is unverified
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
.firstOrNull { it.sessionId == megolmSessionId }
?.results
@ -122,23 +117,19 @@ class WithHeldTests : InstrumentedTest {
}
?.code == WithHeldCode.UNVERIFIED
}
}
// enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
// wait until it's decrypted
ev?.root?.getClearType() == EventType.MESSAGE
}
}
// Previous message should still be undecryptable (partially withheld session)
// .. might need to wait a bit for stability?
testHelper.runBlockingTest {
mustFail(
message = "This session should not be able to decrypt",
failureBlock = { failure ->
@ -150,7 +141,6 @@ class WithHeldTests : InstrumentedTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
}
}
@Test
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
@ -177,16 +167,13 @@ class WithHeldTests : InstrumentedTest {
val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
// await for bob session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
}
}
// Previous message should still be undecryptable (partially withheld session)
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
// .. might need to wait a bit for stability?
testHelper.runBlockingTest {
mustFail(
message = "This session should not be able to decrypt",
failureBlock = { failure ->
@ -197,7 +184,6 @@ class WithHeldTests : InstrumentedTest {
}) {
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
}
}
// Ensure that alice has marked the session to be shared with bob
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
@ -216,11 +202,9 @@ class WithHeldTests : InstrumentedTest {
// Check that the
// await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
}
}
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(
bobSecondSession.myUserId,
@ -258,27 +242,21 @@ class WithHeldTests : InstrumentedTest {
var sessionId: String? = null
// Check that the
// await for bob SecondSession session to get the message
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
// try to decrypt and force key request
tryOrNull {
testHelper.runBlockingTest {
bobSecondSession.cryptoService().decryptEvent(it.root, "")
}
}
}
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null
}
}
// Check that bob second session requested the key
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
testHelper.retryPeriodically {
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
wc?.code == WithHeldCode.UNAUTHORISED
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -58,7 +58,6 @@ class ReplayAttackTest : InstrumentedTest {
val fakeEventWithTheSameIndex =
sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
testHelper.runBlockingTest {
// Lets assume we are from the main timelineId
val timelineId = "timelineId"
// Lets decrypt the original event
@ -69,7 +68,6 @@ class ReplayAttackTest : InstrumentedTest {
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
}
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
}
cryptoTestData.cleanUp(testHelper)
}
@ -93,7 +91,6 @@ class ReplayAttackTest : InstrumentedTest {
Assert.assertTrue("Message should be sent", sentEvents.size == 1)
assertEquals(sentEvents.size, 1)
testHelper.runBlockingTest {
// Lets assume we are from the main timelineId
val timelineId = "timelineId"
// Lets decrypt the original event
@ -105,5 +102,4 @@ class ReplayAttackTest : InstrumentedTest {
fail("Shouldn't throw a decryption error for same event")
}
}
}
}

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto.ssss
import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -37,12 +36,12 @@ import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.first
import org.matrix.android.sdk.common.onMain
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
@RunWith(AndroidJUnit4::class)
@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
testHelper.runBlockingTest {
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
}
var accountData: UserAccountDataEvent? = null
// Assert Account data is updated
testHelper.waitWithLatch {
val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID")
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") {
accountData = t.getOrNull()
}
it.countDown()
}
liveAccountData.observeForever(accountDataObserver)
}
val accountData = aliceSession.accountDataService()
.onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") }
.first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" }
.getOrNull()
assertNotNull("Key should be stored in account data", accountData)
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
assertNotNull("Key Content cannot be parsed", parsed)
@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest {
assertEquals("Unexpected key name", "Test Key", parsed.name)
assertNull("Key was not generated from passphrase", parsed.passphrase)
var defaultKeyAccountData: UserAccountDataEvent? = null
// 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)
}
val defaultKeyAccountData = aliceSession.accountDataService()
.onMain { getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) }
.first { it.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID }
.getOrNull()
// Set as default key
assertNotNull(defaultKeyAccountData?.content)
assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key"))
@ -112,21 +96,19 @@ class QuadSTests : InstrumentedTest {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId = "My.Key"
val info = generatedSecret(testHelper, aliceSession, keyId, true)
val info = generatedSecret(aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
// Store a secret
val clearSecret = "42".toByteArray().toBase64NoPadding()
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().storeSecret(
"secret.of.life",
clearSecret,
listOf(KeyRef(null, keySpec)) // default key
)
}
val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life")
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent)
@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest {
// Try to decrypt??
val decryptedSecret = testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().getSecret(
val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret(
"secret.of.life",
null, // default key
keySpec!!
)
}
assertEquals("Secret mismatch", clearSecret, decryptedSecret)
}
@ -159,28 +139,23 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
testHelper.runBlockingTest {
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
}
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
testHelper.runBlockingTest {
quadS.setDefaultKey(TEST_KEY_ID)
}
}
@Test
fun test_StoreSecretWithMultipleKey() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true)
val key1Info = generatedSecret(aliceSession, keyId1, true)
val keyId2 = "Key2"
val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true)
val key2Info = generatedSecret(aliceSession, keyId2, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().storeSecret(
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
@ -189,7 +164,6 @@ class QuadSTests : InstrumentedTest {
KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
)
)
}
val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret")
val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *>
@ -200,22 +174,18 @@ class QuadSTests : InstrumentedTest {
assertNotNull(encryptedContent?.get(keyId2))
// Assert that can decrypt with both keys
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().getSecret(
"my.secret",
keyId1,
RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!
)
}
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().getSecret(
"my.secret",
keyId2,
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
)
}
}
@Test
@Ignore("Test is working locally, not in GitHub actions")
@ -224,19 +194,16 @@ class QuadSTests : InstrumentedTest {
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val passphrase = "The good pass phrase"
val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true)
val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().storeSecret(
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
)
}
testHelper.runBlockingTest {
try {
aliceSession.sharedSecretStorageService().getSecret(
"my.secret",
@ -251,10 +218,8 @@ class QuadSTests : InstrumentedTest {
} catch (throwable: Throwable) {
assert(throwable is SharedSecretStorageError.BadMac)
}
}
// Now try with correct key
testHelper.runBlockingTest {
aliceSession.sharedSecretStorageService().getSecret(
"my.secret",
keyId1,
@ -266,62 +231,47 @@ class QuadSTests : InstrumentedTest {
)
)
}
}
private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent {
var accountData: UserAccountDataEvent? = null
testHelper.waitWithLatch {
val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type)
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == type) {
accountData = t.getOrNull()
it.countDown()
}
}
liveAccountData.observeForever(accountDataObserver)
}
private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
val accountData = session.accountDataService()
.onMain { getLiveUserAccountDataEvent(type) }
.first { it.getOrNull()?.type == type }
.getOrNull()
assertNotNull("Account Data type:$type should be found", accountData)
return accountData!!
}
private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
private suspend fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
val creationInfo = testHelper.runBlockingTest {
quadS.generateKey(keyId, null, keyId, emptyKeySigner)
}
val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner)
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo
}
private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
private suspend fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
val creationInfo = testHelper.runBlockingTest {
quadS.generateKeyWithPassphrase(
val creationInfo = quadS.generateKeyWithPassphrase(
keyId,
keyId,
passphrase,
emptyKeySigner,
null
)
}
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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