Added e2ee sanity tests

This commit is contained in:
Valere 2022-02-28 15:11:29 +01:00
parent 24c51ea41a
commit c97de48474
3 changed files with 520 additions and 245 deletions

View file

@ -23,7 +23,7 @@ object TestConstants {
const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
// Time out to use when waiting for server response.
private const val AWAIT_TIME_OUT_MILLIS = 30_000
private const val AWAIT_TIME_OUT_MILLIS = 60_000
// Time out to use when waiting for server response, when the debugger is connected. 10 minutes
private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000

View file

@ -0,0 +1,519 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import kotlinx.coroutines.delay
import org.amshove.kluent.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2eeSanityTests : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
/**
* Simple test that create an e2ee room.
* Some new members are added, and a message is sent.
* We check that the message is e2e and can be decrypted.
*
* Additional users join, we check that they can't decrypt history
*
* Alice sends a new message, then check that the new one can be decrypted
*/
@Test
fun testSendingE2EEMessages() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
// add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
}
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.invite(it.myUserId)
}
}
// All user should accept invite
otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
// check that alice see them as joined (not really necessary?)
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
Log.v("#E2E TEST", "All users have joined the room")
Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my message"
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null)
// All should be able to decrypt
otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Add a new user to the room, and check that he can't decrypt
val newAccount = listOf("adam") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
}
newAccount.forEach {
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.invite(it.myUserId)
}
}
newAccount.forEach {
waitForAndAcceptInviteInRoom(it, e2eRoomID)
}
ensureMembersHaveJoined(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) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!).also {
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timeLineEvent != null &&
timeLineEvent.root.getClearType() == EventType.ENCRYPTED &&
timeLineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
}
}
}
// Let alice send a new message
Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message"
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondSentEventId!!).also {
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timeLineEvent != null &&
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
secondMessage.equals(timeLineEvent.root.getClearContent().toModel<MessageContent>()?.body)
}
}
}
otherAccounts.forEach {
testHelper.signOutAndClose(it)
}
newAccount.forEach { testHelper.signOutAndClose(it) }
cryptoTestData.cleanUp(testHelper)
}
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendTextMessage(text)
var sentEventId: String? = null
testHelper.waitWithLatch(4 * 60_000) {
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodicallyWithLatch(it) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also {
Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}")
}
.filter { it.root.sendState == SendState.SYNCED }
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
}
timeline.dispose()
}
return sentEventId
}
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
otherAccounts.map {
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
it == Membership.JOIN
}
}
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
}
}
}
}
testHelper.runBlockingTest(60_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
/**
* Quick test for basic keybackup
* 1. Create e2e between Alice and Bob
* 2. Alice sends 3 messages, using 3 different sessions
* 3. Ensure bob can decrypt
* 4. Create backup for bob and uplaod keys
*
* 5. Sign out alice and bob to ensure no gossiping will happen
*
* 6. Let bob sign in with a new session
* 7. Ensure history is UISI
* 8. Import backup
* 9. Check that new session can decrypt
*/
@Test
fun testBasicBackupImport() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val e2eRoomID = cryptoTestData.roomId
Log.v("#E2E TEST", "Create and start keybackup for bob ...")
val keysBackupService = bobSession.cryptoService().keysBackupService()
val keyBackupPassword = "FooBarBaz"
val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
}
val version = testHelper.doSync<KeysVersion> {
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
Log.v("#E2E TEST", "... Key backup started and enabled for bob")
// Bob session should now have
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
// let's send a few message to bob
val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
// 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 ->
keysBackupService.backupAllGroupSessions(
null,
TestMatrixCallback(latch, true)
)
}
Log.v("#E2E TEST", "... Keybackup done for Bob")
// Now lets logout both alice and bob to ensure that we won't have any gossiping
val bobUserId = bobSession.myUserId
Log.v("#E2E TEST", "Logout alice and bob...")
testHelper.signOutAndClose(aliceSession)
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")
val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
// check that bob can't currently decrypt
Log.v("#E2E TEST", "check that bob can't currently decrypt")
sentEventIds.forEach { sentEventId ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId)?.also {
Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
}
timeLineEvent != null &&
timeLineEvent.root.getClearType() == EventType.ENCRYPTED
}
}
}
// after initial sync events are not decrypted, so we have to try manually
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// Let's now import keys from backup
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
keysBackupService.getVersion(version.version, it)
}
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
keyBackupPassword,
null,
null,
null, it)
}
assertEquals(3, importedResult.totalNumberOfKeys)
}
// ensure bob can now decrypt
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
}
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val event = session.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} catch (error: MXCryptoError) {
// nop
}
}
event.getClearType() == EventType.MESSAGE &&
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
}
}
}
}
/**
* Check that a new verified session that was not supposed to get the keys initially will
* get them from an older one.
*/
@Test
fun testSimpleGossip() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
cryptoTestHelper.initializeCrossSigning(bobSession)
// let's send a few message to bob
val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob")
Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
// Let's now add a new bob session
// Create a new session for bob
Log.v("#E2E TEST", "Create a new session for Bob")
val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// Try to request
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root
newBobSession.cryptoService().requestRoomKeyForEvent(event)
}
// wait a bit
testHelper.runBlockingTest {
delay(10_000)
}
// Ensure that new bob still can't decrypt (keys must have been withheld)
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
// Now mark new bob session as verified
bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
// now let new session re-request
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
}
// wait a bit
testHelper.runBlockingTest {
delay(10_000)
}
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
}
private fun ensureIsDecrypted(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
}
private fun ensureCannotDecrypt(sentEventIds: MutableList<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimeLineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
newBobSession.cryptoService().decryptEvent(event, "")
fail("Should not be able to decrypt event")
} catch (error: MXCryptoError) {
val errorType = (error as? MXCryptoError.Base)?.errorType
if (expectedError == null) {
Assert.assertTrue(errorType != null)
} else {
assertEquals(expectedError, errorType, "Message expected to be UISI")
}
}
}
}
}
}

View file

@ -1,244 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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.internal.crypto
import android.util.Log
import androidx.test.filters.LargeTest
import kotlinx.coroutines.delay
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class SimpleE2EEChatTest : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
/**
* Simple test that create an e2ee room.
* Some new members are added, and a message is sent.
* We check that the message is e2e and can be decrypted.
*
* Additional users join, we check that they can't decrypt history
*
* Alice sends a new message, then check that the new one can be decrypted
*/
@Test
fun testSendingE2EEMessages() {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
// add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
}
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.invite(it.myUserId)
}
}
// All user should accept invite
otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
// check that alice see them as joined (not really necessary?)
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
Log.v("#E2E TEST", "All users have joined the room")
Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my message"
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null)
// All should be able to decrypt
otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Add a new user to the room, and check that he can't decrypt
val newAccount = listOf("adam") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
}
newAccount.forEach {
testHelper.runBlockingTest {
Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
aliceRoomPOV.invite(it.myUserId)
}
}
newAccount.forEach {
waitForAndAcceptInviteInRoom(it, e2eRoomID)
}
ensureMembersHaveJoined(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) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(sentEventId!!).also {
Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timeLineEvent != null &&
timeLineEvent.root.getClearType() == EventType.ENCRYPTED &&
timeLineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
}
}
}
// Let alice send a new message
Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message"
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimeLineEvent(secondSentEventId!!).also {
Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
}
timeLineEvent != null &&
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
secondMessage.equals(timeLineEvent.root.getClearContent().toModel<MessageContent>()?.body)
}
}
}
otherAccounts.forEach {
testHelper.signOutAndClose(it)
}
newAccount.forEach { testHelper.signOutAndClose(it) }
cryptoTestData.cleanUp(testHelper)
}
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendTextMessage(text)
var sentEventId: String? = null
testHelper.waitWithLatch(4 * 60_000) {
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodicallyWithLatch(it) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
.also {
Log.v("#E2E TEST", "Timeline snapshot is ${it.map { "${it.root.type}|${it.root.sendState}" }.joinToString(",", "[", "]")}")
}
.filter { it.root.sendState == SendState.SYNCED }
.firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
sentEventId = decryptedMsg?.eventId
decryptedMsg != null
}
timeline.dispose()
}
return sentEventId
}
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
otherAccounts.map {
aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
}.all {
it == Membership.JOIN
}
}
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
if (it) {
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
}
}
}
}
testHelper.runBlockingTest(60_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.joinRoom(e2eRoomID)
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
// it's ok we will wait after
}
}
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
roomSummary != null && roomSummary.membership == Membership.JOIN
}
}
}
}