Merge branch 'develop' of github.com:vector-im/element-android into michaelk/sonarqube_fixes

This commit is contained in:
Adam Brown 2022-05-30 09:55:37 +01:00
commit d18e7ad001
65 changed files with 1382 additions and 729 deletions

1
changelog.d/5283.wip Normal file
View File

@ -0,0 +1 @@
FTUE - Adds the redesigned Sign In screen

1
changelog.d/6073.feature Normal file
View File

@ -0,0 +1 @@
Adds up navigation in spaces

1
changelog.d/6077.sdk Normal file
View File

@ -0,0 +1 @@
Improve replay attacks and reduce duplicate message index errors

1
changelog.d/6148.bugfix Normal file
View File

@ -0,0 +1 @@
Fix decrypting redacted event from sending errors

View File

@ -199,7 +199,7 @@ dependencies {
implementation libs.apache.commonsImaging
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
testImplementation libs.tests.junit
testImplementation 'org.robolectric:robolectric:4.7.3'

View File

@ -24,8 +24,8 @@ 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.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@ -34,32 +34,22 @@ import org.matrix.android.sdk.common.TestConstants
@LargeTest
class AccountCreationTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
@Test
fun createAccountTest() {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
commonTestHelper.signOutAndClose(session)
fun createAccountTest() = runSessionTest(context()) { commonTestHelper ->
commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun createAccountAndLoginAgainTest() {
fun createAccountAndLoginAgainTest() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Log again to the same account
val session2 = commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
commonTestHelper.signOutAndClose(session)
commonTestHelper.signOutAndClose(session2)
commonTestHelper.logIntoAccount(session.myUserId, SessionTestParams(withInitialSync = true))
}
@Test
fun simpleE2eTest() {
val res = cryptoTestHelper.doE2ETestWithAliceInARoom()
res.cleanUp(commonTestHelper)
fun simpleE2eTest() = runCryptoTest(context()) { cryptoTestHelper, _ ->
cryptoTestHelper.doE2ETestWithAliceInARoom()
}
}

View File

@ -25,7 +25,7 @@ import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.failure.isInvalidPassword
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
@ -34,14 +34,12 @@ import org.matrix.android.sdk.common.TestConstants
@Ignore("This test will be ignored until it is fixed")
class ChangePasswordTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
companion object {
private const val NEW_PASSWORD = "this is a new password"
}
@Test
fun changePasswordTest() {
fun changePasswordTest() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Change password
@ -54,9 +52,6 @@ class ChangePasswordTest : InstrumentedTest {
throwable.isInvalidPassword().shouldBeTrue()
// Try to login with the new password, should work
val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
commonTestHelper.signOutAndClose(session)
commonTestHelper.signOutAndClose(session2)
commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
}
}

View File

@ -29,7 +29,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
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 kotlin.coroutines.Continuation
@ -39,10 +39,8 @@ import kotlin.coroutines.resume
@FixMethodOrder(MethodSorters.JVM)
class DeactivateAccountTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
fun deactivateAccountTest() {
fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Deactivate the account

View File

@ -23,7 +23,7 @@ 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.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 timber.log.Timber
@ -32,10 +32,8 @@ import timber.log.Timber
@FixMethodOrder(MethodSorters.JVM)
class ApiInterceptorTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
fun apiInterceptorTest() {
fun apiInterceptorTest() = runSessionTest(context()) { commonTestHelper ->
val responses = mutableListOf<String>()
val listener = object : ApiInterceptorListener {

View File

@ -54,12 +54,39 @@ import java.util.concurrent.TimeUnit
* This class exposes methods to be used in common cases
* Registration, login, Sync, Sending messages...
*/
class CommonTestHelper(context: Context) {
class CommonTestHelper private constructor(context: Context) {
companion object {
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
return try {
block(testHelper)
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
}
}
}
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
val cryptoTestHelper = CryptoTestHelper(testHelper)
return try {
block(cryptoTestHelper, testHelper)
} finally {
if (autoSignoutOnClose) {
testHelper.cleanUpOpenedSessions()
}
}
}
}
internal val matrix: TestMatrix
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var accountNumber = 0
private val trackedSessions = mutableListOf<Session>()
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
init {
@ -84,6 +111,15 @@ class CommonTestHelper(context: Context) {
return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
}
fun cleanUpOpenedSessions() {
trackedSessions.forEach {
runBlockingTest {
it.signOutService().signOut(true)
}
}
trackedSessions.clear()
}
/**
* Create a homeserver configuration, with Http connection allowed for test
*/
@ -245,7 +281,9 @@ class CommonTestHelper(context: Context) {
testParams
)
assertNotNull(session)
return session
return session.also {
trackedSessions.add(session)
}
}
/**
@ -261,7 +299,9 @@ class CommonTestHelper(context: Context) {
testParams: SessionTestParams): Session {
val session = logAccountAndSync(userId, password, testParams)
assertNotNull(session)
return session
return session.also {
trackedSessions.add(session)
}
}
/**
@ -379,8 +419,8 @@ class CommonTestHelper(context: Context) {
*/
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
assertTrue(
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
)
}
@ -436,6 +476,7 @@ class CommonTestHelper(context: Context) {
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) {
trackedSessions.remove(session)
runBlockingTest(timeout = 60_000) {
session.signOutService().signOut(true)
}

View File

@ -66,7 +66,7 @@ import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CryptoTestHelper(private val testHelper: CommonTestHelper) {
class CryptoTestHelper(val testHelper: CommonTestHelper) {
private val messagesFromAlice: List<String> = listOf("0 - Hello I'm Alice!", "4 - Go!")
private val messagesFromBob: List<String> = listOf("1 - Hello I'm Bob!", "2 - Isn't life grand?", "3 - Let's go to the opera.")

View File

@ -0,0 +1,75 @@
/*
* 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.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.events.model.Event
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.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class DecryptRedactedEventTest : InstrumentedTest {
@Test
fun doNotFailToDecryptRedactedEvent() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val roomALicePOV = aliceSession.getRoom(e2eRoomID)!!
val timelineEvent = testHelper.sendTextMessage(roomALicePOV, "Hello", 1).first()
val redactionReason = "Wrong Room"
roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
// get the event from bob
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
}
}
val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
testHelper.runBlockingTest {
try {
val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
Assert.assertEquals(
"Unexpected redacted reason",
redactionReason,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
)
Assert.assertEquals(
"Unexpected Redacted event id",
timelineEvent.eventId,
result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
)
} catch (failure: Throwable) {
Assert.fail("Should not throw when decrypting a redacted event")
}
}
}
}

View File

@ -58,7 +58,8 @@ 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.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
@ -84,9 +85,7 @@ class E2eeSanityTests : InstrumentedTest {
* Alice sends a new message, then check that the new one can be decrypted
*/
@Test
fun testSendingE2EEMessages() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testSendingE2EEMessages() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@ -200,21 +199,12 @@ class E2eeSanityTests : InstrumentedTest {
}
}
}
otherAccounts.forEach {
testHelper.signOutAndClose(it)
}
newAccount.forEach { testHelper.signOutAndClose(it) }
cryptoTestData.cleanUp(testHelper)
}
@Test
fun testKeyGossipingIsEnabledByDefault() {
val testHelper = CommonTestHelper(context())
fun testKeyGossipingIsEnabledByDefault() = runSessionTest(context()) { testHelper ->
val session = testHelper.createAccount("alice", SessionTestParams(true))
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
testHelper.signOutAndClose(session)
}
/**
@ -232,9 +222,7 @@ class E2eeSanityTests : InstrumentedTest {
* 9. Check that new session can decrypt
*/
@Test
fun testBasicBackupImport() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testBasicBackupImport() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@ -346,8 +334,6 @@ class E2eeSanityTests : InstrumentedTest {
// ensure bob can now decrypt
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
testHelper.signOutAndClose(newBobSession)
}
/**
@ -355,9 +341,7 @@ class E2eeSanityTests : InstrumentedTest {
* get them from an older one.
*/
@Test
fun testSimpleGossip() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testSimpleGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@ -451,18 +435,13 @@ class E2eeSanityTests : InstrumentedTest {
}
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(newBobSession)
}
/**
* Test that if a better key is forwarded (lower index, it is then used)
*/
@Test
fun testForwardBetterKey() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
@ -578,10 +557,6 @@ class E2eeSanityTests : InstrumentedTest {
canDecryptFirst && canDecryptSecond
}
}
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSessionWithBetterKey)
testHelper.signOutAndClose(newBobSession)
}
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
@ -612,9 +587,7 @@ class E2eeSanityTests : InstrumentedTest {
* Test that if a better key is forwared (lower index, it is then used)
*/
@Test
fun testSelfInteractiveVerificationAndGossip() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testASelfInteractiveVerificationAndGossip() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
cryptoTestHelper.bootstrapSecurity(aliceSession)
@ -753,9 +726,6 @@ class E2eeSanityTests : InstrumentedTest {
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
)
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(aliceNewSession)
}
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {

View File

@ -30,18 +30,14 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
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.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class PreShareKeysTest : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Test
fun ensure_outbound_session_happy_path() {
fun ensure_outbound_session_happy_path() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = testData.roomId
val aliceSession = testData.firstSession
@ -94,7 +90,5 @@ class PreShareKeysTest : InstrumentedTest {
bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
}
}
testData.cleanUp(testHelper)
}
}

View File

@ -39,8 +39,7 @@ import org.matrix.android.sdk.api.session.getRoom
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.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
@ -65,8 +64,6 @@ import kotlin.coroutines.resume
class UnwedgingTest : InstrumentedTest {
private lateinit var messagesReceivedByBob: List<TimelineEvent>
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Before
fun init() {
@ -87,7 +84,7 @@ class UnwedgingTest : InstrumentedTest {
* -> This is automatically fixed after SDKs restarted the olm session
*/
@Test
fun testUnwedging() {
fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -242,8 +239,6 @@ class UnwedgingTest : InstrumentedTest {
}
bobTimeline.dispose()
cryptoTestData.cleanUp(testHelper)
}
private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {

View File

@ -38,8 +38,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerif
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import kotlin.coroutines.Continuation
@ -51,11 +51,8 @@ import kotlin.coroutines.resume
@Ignore
class XSigningTest : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Test
fun test_InitializeAndStoreKeys() {
fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
testHelper.doSync<Unit> {
@ -89,7 +86,7 @@ class XSigningTest : InstrumentedTest {
}
@Test
fun test_CrossSigningCheckBobSeesTheKeys() {
fun test_CrossSigningCheckBobSeesTheKeys() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -139,12 +136,10 @@ class XSigningTest : InstrumentedTest {
)
assertFalse("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV.isTrusted())
cryptoTestData.cleanUp(testHelper)
}
@Test
fun test_CrossSigningTestAliceTrustBobNewDevice() {
fun test_CrossSigningTestAliceTrustBobNewDevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -218,9 +213,5 @@ class XSigningTest : InstrumentedTest {
val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSession)
testHelper.signOutAndClose(bobSession2)
}
}

View File

@ -35,6 +35,7 @@ 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
@ -42,35 +43,36 @@ import java.util.concurrent.CountDownLatch
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class EncryptionTest : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Test
fun test_EncryptionEvent() {
performTest(roomShouldBeEncrypted = false) { room ->
// Send an encryption Event as an Event (and not as a state event)
room.sendService().sendEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
)
}
}
@Test
fun test_EncryptionStateEvent() {
performTest(roomShouldBeEncrypted = true) { room ->
runBlocking {
// Send an encryption Event as a State Event
room.stateService().sendStateEvent(
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room ->
// Send an encryption Event as an Event (and not as a state event)
room.sendService().sendEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
)
}
}
}
private fun performTest(roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
@Test
fun test_EncryptionStateEvent() {
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room ->
runBlocking {
// Send an encryption Event as a State Event
room.stateService().sendStateEvent(
eventType = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
)
}
}
}
}
private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
val aliceSession = cryptoTestData.firstSession
@ -109,6 +111,5 @@ class EncryptionTest : InstrumentedTest {
room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
it.countDown()
}
cryptoTestData.cleanUp(testHelper)
}
}

View File

@ -42,8 +42,7 @@ import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
@ -58,9 +57,7 @@ class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@Test
fun test_DoNotSelfShareIfNotTrusted() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
@ -196,9 +193,7 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@Test
fun test_reShareIfWasIntendedToBeShared() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
@ -229,9 +224,7 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@Test
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
val aliceSession = testData.firstSession
@ -268,9 +261,7 @@ class KeyShareTests : InstrumentedTest {
* Tests that keys reshared with own verified session are done from the earliest known index
*/
@Test
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
@ -390,10 +381,7 @@ class KeyShareTests : InstrumentedTest {
* Tests that we don't cancel a request to early on first forward if the index is not good enough
*/
@Test
fun test_dontCancelToEarly() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
@ -444,7 +432,7 @@ class KeyShareTests : InstrumentedTest {
// Should get a reply from bob and not from alice
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
// 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

View File

@ -37,8 +37,7 @@ import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
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.getTimelineEvent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.MockOkHttpInterceptor
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams
@ -54,9 +53,7 @@ class WithHeldTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
@Test
fun test_WithHeldUnverifiedReason() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_WithHeldUnverifiedReason() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
// =============================
// ARRANGE
@ -155,16 +152,10 @@ class WithHeldTests : InstrumentedTest {
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
}
}
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSession)
testHelper.signOutAndClose(bobUnverifiedSession)
}
@Test
fun test_WithHeldNoOlm() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
@ -241,14 +232,10 @@ class WithHeldTests : InstrumentedTest {
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
aliceInterceptor.clearRules()
testData.cleanUp(testHelper)
testHelper.signOutAndClose(bobSecondSession)
}
@Test
fun test_WithHeldKeyRequest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
@ -295,8 +282,5 @@ class WithHeldTests : InstrumentedTest {
wc?.code == WithHeldCode.UNAUTHORISED
}
}
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSecondSession)
}
}

View File

@ -45,8 +45,8 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
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
@ -67,9 +67,7 @@ class KeysBackupTest : InstrumentedTest {
* - Reset keys backup markers
*/
@Test
fun roomKeysTest_testBackupStore_ok() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun roomKeysTest_testBackupStore_ok() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -108,8 +106,7 @@ class KeysBackupTest : InstrumentedTest {
* Check that prepareKeysBackupVersionWithPassword returns valid data
*/
@Test
fun prepareKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
fun prepareKeysBackupVersionTest() = runSessionTest(context()) { testHelper ->
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
@ -131,16 +128,13 @@ class KeysBackupTest : InstrumentedTest {
assertNotNull(megolmBackupCreationInfo.recoveryKey)
stateObserver.stopAndCheckStates(null)
testHelper.signOutAndClose(bobSession)
}
/**
* Test creating a keys backup version and check that createKeysBackupVersion() returns valid data
*/
@Test
fun createKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
cryptoTestHelper.initializeCrossSigning(bobSession)
@ -199,7 +193,6 @@ class KeysBackupTest : InstrumentedTest {
}
stateObserver.stopAndCheckStates(null)
testHelper.signOutAndClose(bobSession)
}
/**
@ -207,9 +200,7 @@ class KeysBackupTest : InstrumentedTest {
* - Check the backup completes
*/
@Test
fun backupAfterCreateKeysBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun backupAfterCreateKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -244,16 +235,13 @@ class KeysBackupTest : InstrumentedTest {
KeysBackupState.ReadyToBackUp
)
)
cryptoTestData.cleanUp(testHelper)
}
/**
* Check that backupAllGroupSessions() returns valid data
*/
@Test
fun backupAllGroupSessionsTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun backupAllGroupSessionsTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -287,7 +275,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(testHelper)
}
/**
@ -299,9 +286,7 @@ class KeysBackupTest : InstrumentedTest {
* - Compare the decrypted megolm key with the original one
*/
@Test
fun testEncryptAndDecryptKeysBackupData() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testEncryptAndDecryptKeysBackupData() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
@ -336,7 +321,6 @@ class KeysBackupTest : InstrumentedTest {
keysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(testHelper)
}
/**
@ -346,9 +330,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
fun restoreKeysBackupTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun restoreKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -434,9 +416,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
fun trustKeyBackupVersionTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun trustKeyBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@ -483,7 +463,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
testData.cleanUp(testHelper)
}
/**
@ -497,9 +476,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
fun trustKeyBackupVersionWithRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun trustKeyBackupVersionWithRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@ -546,7 +523,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
testData.cleanUp(testHelper)
}
/**
@ -558,9 +534,7 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Do an e2e backup to the homeserver with a recovery key
@ -589,7 +563,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
stateObserver.stopAndCheckStates(null)
testData.cleanUp(testHelper)
}
/**
@ -603,9 +576,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must be trusted and must have with 2 signatures now
*/
@Test
fun trustKeyBackupVersionWithPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun trustKeyBackupVersionWithPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
@ -654,7 +625,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(2, keysBackupVersionTrust.signatures.size)
stateObserver.stopAndCheckStates(null)
testData.cleanUp(testHelper)
}
/**
@ -666,9 +636,7 @@ class KeysBackupTest : InstrumentedTest {
* - The backup must still be untrusted and disabled
*/
@Test
fun trustKeyBackupVersionWithWrongPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun trustKeyBackupVersionWithWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "Password"
@ -700,7 +668,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().state)
stateObserver.stopAndCheckStates(null)
testData.cleanUp(testHelper)
}
/**
@ -710,9 +677,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun restoreKeysBackupWithAWrongRecoveryKeyTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -736,8 +701,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
testData.cleanUp(testHelper)
}
/**
@ -747,9 +710,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
fun testBackupWithPassword() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testBackupWithPassword() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@ -796,8 +757,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(testHelper)
}
/**
@ -807,9 +766,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
fun restoreKeysBackupWithAWrongPasswordTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun restoreKeysBackupWithAWrongPasswordTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@ -836,8 +793,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
testData.cleanUp(testHelper)
}
/**
@ -847,9 +802,7 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful
*/
@Test
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val password = "password"
@ -869,8 +822,6 @@ class KeysBackupTest : InstrumentedTest {
}
keysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(testHelper)
}
/**
@ -880,9 +831,7 @@ class KeysBackupTest : InstrumentedTest {
* - It must fail
*/
@Test
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
@ -906,8 +855,6 @@ class KeysBackupTest : InstrumentedTest {
// onSuccess may not have been called
assertNull(importRoomKeysResult)
testData.cleanUp(testHelper)
}
/**
@ -915,9 +862,7 @@ class KeysBackupTest : InstrumentedTest {
* - Check the returned KeysVersionResult is trusted
*/
@Test
fun testIsKeysBackupTrusted() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testIsKeysBackupTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@ -951,7 +896,6 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(testHelper)
}
/**
@ -963,9 +907,7 @@ class KeysBackupTest : InstrumentedTest {
* -> That must fail and her backup state must be WrongBackUpVersion
*/
@Test
fun testBackupWhenAnotherBackupWasCreated() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testBackupWhenAnotherBackupWasCreated() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@ -1022,7 +964,6 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(testHelper)
}
/**
@ -1038,9 +979,7 @@ class KeysBackupTest : InstrumentedTest {
* -> It must success
*/
@Test
fun testBackupAfterVerifyingADevice() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun testBackupAfterVerifyingADevice() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@ -1131,8 +1070,6 @@ class KeysBackupTest : InstrumentedTest {
stateObserver.stopAndCheckStates(null)
stateObserver2.stopAndCheckStates(null)
testHelper.signOutAndClose(aliceSession2)
cryptoTestData.cleanUp(testHelper)
}
/**
@ -1140,9 +1077,7 @@ class KeysBackupTest : InstrumentedTest {
* - Delete the backup
*/
@Test
fun deleteKeysBackupTest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun deleteKeysBackupTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
// - Create a backup version
@ -1165,6 +1100,5 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(testHelper)
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.internal.crypto.replayattack
import androidx.test.filters.LargeTest
import org.amshove.kluent.internal.assertFailsWith
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
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.crypto.MXCryptoError
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class ReplayAttackTest : InstrumentedTest {
@Test
fun replayAttackAlreadyDecryptedEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob
val bobSession = cryptoTestData.secondSession
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
// Alice will send a message
val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
assertEquals(1, sentEvents.size)
val fakeEventId = sentEvents[0].eventId + "_fake"
val fakeEventWithTheSameIndex =
sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
testHelper.runBlockingTest {
// Lets assume we are from the main timelineId
val timelineId = "timelineId"
// Lets decrypt the original event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
// Lets decrypt the fake event that will have the same message index
val exception = assertFailsWith<MXCryptoError.Base> {
// An exception should be thrown while the same index would have been used for the previous decryption
aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
}
assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
}
cryptoTestData.cleanUp(testHelper)
}
@Test
fun replayAttackSameEventTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val e2eRoomID = cryptoTestData.roomId
// Alice
val aliceSession = cryptoTestData.firstSession
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
// Bob
val bobSession = cryptoTestData.secondSession
val bobRoomPOV = bobSession!!.roomService().getRoom(e2eRoomID)!!
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
// Alice will send a message
val sentEvents = testHelper.sendTextMessage(aliceRoomPOV, "Hello I will be decrypted twice", 1)
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
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
try {
// Lets try to decrypt the same event
aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
} catch (ex: Throwable) {
fail("Shouldn't throw a decryption error for same event")
}
}
}
}

View File

@ -40,6 +40,7 @@ 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.internal.crypto.secrets.DefaultSharedSecretStorageService
@ -55,8 +56,7 @@ class QuadSTests : InstrumentedTest {
}
@Test
fun test_Generate4SKey() {
val testHelper = CommonTestHelper(context())
fun test_Generate4SKey() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@ -108,12 +108,11 @@ class QuadSTests : InstrumentedTest {
}
@Test
fun test_StoreSecret() {
val testHelper = CommonTestHelper(context())
fun test_StoreSecret() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId = "My.Key"
val info = generatedSecret(aliceSession, keyId, true)
val info = generatedSecret(testHelper, aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
@ -127,7 +126,7 @@ class QuadSTests : InstrumentedTest {
)
}
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent)
@ -149,12 +148,10 @@ class QuadSTests : InstrumentedTest {
}
assertEquals("Secret mismatch", clearSecret, decryptedSecret)
testHelper.signOutAndClose(aliceSession)
}
@Test
fun test_SetDefaultLocalEcho() {
val testHelper = CommonTestHelper(context())
fun test_SetDefaultLocalEcho() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
@ -170,19 +167,16 @@ class QuadSTests : InstrumentedTest {
testHelper.runBlockingTest {
quadS.setDefaultKey(TEST_KEY_ID)
}
testHelper.signOutAndClose(aliceSession)
}
@Test
fun test_StoreSecretWithMultipleKey() {
val testHelper = CommonTestHelper(context())
fun test_StoreSecretWithMultipleKey() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val key1Info = generatedSecret(aliceSession, keyId1, true)
val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true)
val keyId2 = "Key2"
val key2Info = generatedSecret(aliceSession, keyId2, true)
val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
@ -221,19 +215,16 @@ class QuadSTests : InstrumentedTest {
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
)
}
testHelper.signOutAndClose(aliceSession)
}
@Test
@Ignore("Test is working locally, not in GitHub actions")
fun test_GetSecretWithBadPassphrase() {
val testHelper = CommonTestHelper(context())
fun test_GetSecretWithBadPassphrase() = runSessionTest(context()) { testHelper ->
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val passphrase = "The good pass phrase"
val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
@ -275,13 +266,9 @@ class QuadSTests : InstrumentedTest {
)
)
}
testHelper.signOutAndClose(aliceSession)
}
private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
val testHelper = CommonTestHelper(context())
private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent {
var accountData: UserAccountDataEvent? = null
testHelper.waitWithLatch {
val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type)
@ -297,29 +284,27 @@ class QuadSTests : InstrumentedTest {
return accountData!!
}
private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
val testHelper = CommonTestHelper(context())
val creationInfo = testHelper.runBlockingTest {
quadS.generateKey(keyId, null, keyId, emptyKeySigner)
}
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo
}
private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService()
val testHelper = CommonTestHelper(context())
val creationInfo = testHelper.runBlockingTest {
quadS.generateKeyWithPassphrase(
@ -331,12 +316,12 @@ class QuadSTests : InstrumentedTest {
)
}
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) {
testHelper.runBlockingTest {
quadS.setDefaultKey(keyId)
}
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo

View File

@ -44,8 +44,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransa
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.toModel
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
import org.matrix.android.sdk.internal.crypto.model.rest.toValue
@ -57,9 +56,7 @@ import java.util.concurrent.CountDownLatch
class SASTest : InstrumentedTest {
@Test
fun test_aliceStartThenAliceCancel() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_aliceStartThenAliceCancel() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -136,15 +133,11 @@ class SASTest : InstrumentedTest {
assertNull(bobVerificationService.getExistingTransaction(aliceSession.myUserId, txID))
assertNull(aliceVerificationService.getExistingTransaction(bobSession.myUserId, txID))
cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_protocols_must_include_curve25519() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_key_agreement_protocols_must_include_curve25519() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@ -196,15 +189,11 @@ class SASTest : InstrumentedTest {
testHelper.await(cancelLatch)
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod, cancelReason)
cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_macs_Must_include_hmac_sha256() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_key_agreement_macs_Must_include_hmac_sha256() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@ -237,15 +226,11 @@ class SASTest : InstrumentedTest {
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
cryptoTestData.cleanUp(testHelper)
}
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_key_agreement_short_code_include_decimal() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_key_agreement_short_code_include_decimal() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fail("Not passing for the moment")
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
@ -278,8 +263,6 @@ class SASTest : InstrumentedTest {
val cancelReq = canceledToDeviceEvent!!.content.toModel<KeyVerificationCancel>()!!
assertEquals("Request should be cancelled with m.unknown_method", CancelCode.UnknownMethod.value, cancelReq.code)
cryptoTestData.cleanUp(testHelper)
}
private fun fakeBobStart(bobSession: Session,
@ -315,9 +298,7 @@ class SASTest : InstrumentedTest {
// any two devices may only have at most one key verification in flight at a time.
// If a device has two verifications in progress with the same device, then it should cancel both verifications.
@Test
fun test_aliceStartTwoRequests() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_aliceStartTwoRequests() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -358,9 +339,7 @@ class SASTest : InstrumentedTest {
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_aliceAndBobAgreement() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_aliceAndBobAgreement() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -414,14 +393,10 @@ class SASTest : InstrumentedTest {
accepted!!.shortAuthenticationStrings.forEach {
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
}
cryptoTestData.cleanUp(testHelper)
}
@Test
fun test_aliceAndBobSASCode() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_aliceAndBobSASCode() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -474,14 +449,10 @@ class SASTest : InstrumentedTest {
"Should have same SAS", aliceTx.getShortCodeRepresentation(SasMode.DECIMAL),
bobTx.getShortCodeRepresentation(SasMode.DECIMAL)
)
cryptoTestData.cleanUp(testHelper)
}
@Test
fun test_happyPath() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_happyPath() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -554,13 +525,10 @@ class SASTest : InstrumentedTest {
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
cryptoTestData.cleanUp(testHelper)
}
@Test
fun test_ConcurrentStart() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
fun test_ConcurrentStart() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -647,7 +615,5 @@ class SASTest : InstrumentedTest {
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
}
cryptoTestData.cleanUp(testHelper)
}
}

View File

@ -32,8 +32,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch
@ -156,9 +156,7 @@ class VerificationTest : InstrumentedTest {
private fun doTest(aliceSupportedMethods: List<VerificationMethod>,
bobSupportedMethods: List<VerificationMethod>,
expectedResultForAlice: ExpectedResult,
expectedResultForBob: ExpectedResult) {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
expectedResultForBob: ExpectedResult) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
@ -253,14 +251,11 @@ class VerificationTest : InstrumentedTest {
pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode
pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode
}
cryptoTestData.cleanUp(testHelper)
}
@Test
fun test_selfVerificationAcceptedCancelsItForOtherSessions() {
fun test_selfVerificationAcceptedCancelsItForOtherSessions() = runSessionTest(context()) { testHelper ->
val defaultSessionParams = SessionTestParams(true)
val testHelper = CommonTestHelper(context())
val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)

View File

@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.getRoom
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
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@ -44,9 +43,7 @@ import java.util.concurrent.CountDownLatch
class ThreadMessagingTest : InstrumentedTest {
@Test
fun reply_in_thread_should_create_a_thread() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun reply_in_thread_should_create_a_thread() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -104,9 +101,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
fun reply_in_thread_should_create_a_thread_from_other_user() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun reply_in_thread_should_create_a_thread_from_other_user() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -179,9 +174,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
fun reply_in_thread_to_timeline_message_multiple_times() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun reply_in_thread_to_timeline_message_multiple_times() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -244,9 +237,7 @@ class ThreadMessagingTest : InstrumentedTest {
}
@Test
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun thread_summary_advanced_validation_after_multiple_messages_in_multiple_threads() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession

View File

@ -38,8 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType
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.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@ -47,9 +46,7 @@ import java.util.concurrent.CountDownLatch
class PollAggregationTest : InstrumentedTest {
@Test
fun testAllPollUseCases() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun testAllPollUseCases() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -138,7 +135,6 @@ class PollAggregationTest : InstrumentedTest {
aliceSession.stopSync()
aliceTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {

View File

@ -34,8 +34,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
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
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.checkSendOrder
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@ -53,9 +52,7 @@ class TimelineForwardPaginationTest : InstrumentedTest {
* This test ensure that if we click to permalink, we will be able to go back to the live
*/
@Test
fun forwardPaginationTest() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun forwardPaginationTest() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val numberOfMessagesToSend = 90
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
@ -177,7 +174,5 @@ class TimelineForwardPaginationTest : InstrumentedTest {
}
aliceTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View File

@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
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
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.checkSendOrder
import timber.log.Timber
import java.util.concurrent.CountDownLatch
@ -48,9 +47,7 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
*/
@Test
fun previousLastForwardTest() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun previousLastForwardTest() = CommonTestHelper.runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
val aliceSession = cryptoTestData.firstSession
@ -242,7 +239,5 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
}
bobTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View File

@ -33,8 +33,7 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
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
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.TestConstants
@RunWith(JUnit4::class)
@ -44,9 +43,7 @@ import org.matrix.android.sdk.common.TestConstants
class TimelineSimpleBackPaginationTest : InstrumentedTest {
@Test
fun timeline_backPaginate_shouldReachEndOfTimeline() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
fun timeline_backPaginate_shouldReachEndOfTimeline() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val numberOfMessagesToSent = 200
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
@ -104,6 +101,5 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest {
assertEquals(numberOfMessagesToSent, onlySentEvents.size)
bobTimeline.dispose()
cryptoTestData.cleanUp(commonTestHelper)
}
}

View File

@ -30,8 +30,7 @@ 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.model.message.MessageContent
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.CommonTestHelper.Companion.runCryptoTest
import java.util.concurrent.CountDownLatch
/** !! Not working with the new timeline
@ -47,15 +46,12 @@ class TimelineWithManyMembersTest : InstrumentedTest {
private const val NUMBER_OF_MEMBERS = 6
}
private val commonTestHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
/**
* Ensures when someone sends a message to a crowded room, everyone can decrypt the message.
*/
@Test
fun everyone_should_decrypt_message_in_a_crowded_room() {
fun everyone_should_decrypt_message_in_a_crowded_room() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS)
val sessionForFirstMember = cryptoTestData.firstSession

View File

@ -26,9 +26,8 @@ import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.search.SearchResult
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.CryptoTestData
import org.matrix.android.sdk.common.CryptoTestHelper
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -74,9 +73,7 @@ class SearchMessagesTest : InstrumentedTest {
}
}
private fun doTest(block: suspend (CryptoTestData) -> SearchResult) {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
@ -99,7 +96,5 @@ class SearchMessagesTest : InstrumentedTest {
(it.event.content?.get("body") as? String)?.startsWith(MESSAGE).orFalse()
}.orFalse()
)
cryptoTestData.cleanUp(commonTestHelper)
}
}

View File

@ -41,7 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@RunWith(JUnit4::class)
@ -50,8 +50,7 @@ import org.matrix.android.sdk.common.SessionTestParams
class SpaceCreationTest : InstrumentedTest {
@Test
fun createSimplePublicSpace() {
val commonTestHelper = CommonTestHelper(context())
fun createSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
val roomName = "My Space"
val topic = "A public space for test"
@ -97,13 +96,11 @@ class SpaceCreationTest : InstrumentedTest {
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility)
commonTestHelper.signOutAndClose(session)
}
@Test
fun testJoinSimplePublicSpace() {
val commonTestHelper = CommonTestHelper(context())
@Ignore
fun testJoinSimplePublicSpace() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
@ -135,9 +132,7 @@ class SpaceCreationTest : InstrumentedTest {
}
@Test
@Ignore
fun testSimplePublicSpaceWithChildren() {
val commonTestHelper = CommonTestHelper(context())
fun testSimplePublicSpaceWithChildren() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
@ -206,8 +201,5 @@ class SpaceCreationTest : InstrumentedTest {
// ).size
//
// assertEquals("Unexpected number of joined children", 1, childCount)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
}

View File

@ -48,6 +48,7 @@ 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
@RunWith(JUnit4::class)
@ -55,8 +56,7 @@ import org.matrix.android.sdk.common.SessionTestParams
class SpaceHierarchyTest : InstrumentedTest {
@Test
fun createCanonicalChildRelation() {
val commonTestHelper = CommonTestHelper(context())
fun createCanonicalChildRelation() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceName = "My Space"
@ -173,8 +173,7 @@ class SpaceHierarchyTest : InstrumentedTest {
// }
@Test
fun testFilteringBySpace() {
val commonTestHelper = CommonTestHelper(context())
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@ -185,12 +184,12 @@ class SpaceHierarchyTest : InstrumentedTest {
)
/* val spaceBInfo = */ createPublicSpace(
session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
session, "SpaceB", listOf(
Triple("B1", true /*auto-join*/, true/*canonical*/),
Triple("B2", true, true),
Triple("B3", true, true)
)
)
val spaceCInfo = createPublicSpace(
session, "SpaceC", listOf(
@ -256,8 +255,7 @@ class SpaceHierarchyTest : InstrumentedTest {
@Test
@Ignore("This test will be ignored until it is fixed")
fun testBreakCycle() {
val commonTestHelper = CommonTestHelper(context())
fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@ -302,8 +300,7 @@ class SpaceHierarchyTest : InstrumentedTest {
}
@Test
fun testLiveFlatChildren() {
val commonTestHelper = CommonTestHelper(context())
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
val spaceAInfo = createPublicSpace(
@ -395,25 +392,26 @@ class SpaceHierarchyTest : InstrumentedTest {
childInfo: List<Triple<String, Boolean, Boolean?>>
/** Name, auto-join, canonical*/
): TestSpaceCreationResult {
val commonTestHelper = CommonTestHelper(context())
var spaceId = ""
var roomIds: List<String> = emptyList()
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
runSessionTest(context()) { commonTestHelper ->
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
roomIds = childInfo.map { entry ->
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
}
latch.countDown()
}
latch.countDown()
}
return TestSpaceCreationResult(spaceId, roomIds)
}
@ -423,51 +421,51 @@ class SpaceHierarchyTest : InstrumentedTest {
childInfo: List<Triple<String, Boolean, Boolean?>>
/** Name, auto-join, canonical*/
): TestSpaceCreationResult {
val commonTestHelper = CommonTestHelper(context())
var spaceId = ""
var roomIds: List<String> = emptyList()
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds =
childInfo.map { entry ->
val homeServerCapabilities = session
.homeServerCapabilitiesService()
.getHomeServerCapabilities()
session.roomService().createRoom(CreateRoomParams().apply {
name = entry.first
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
)
)
})
runSessionTest(context()) { commonTestHelper ->
commonTestHelper.waitWithLatch { latch ->
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
val syncedSpace = session.spaceService().getSpace(spaceId)
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
roomIds =
childInfo.map { entry ->
val homeServerCapabilities = session
.homeServerCapabilitiesService()
.getHomeServerCapabilities()
session.roomService().createRoom(CreateRoomParams().apply {
name = entry.first
this.featurePreset = RestrictedRoomPreset(
homeServerCapabilities,
listOf(
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
)
)
})
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
roomIds.forEachIndexed { index, roomId ->
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
val canonical = childInfo[index].third
if (canonical != null) {
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
}
latch.countDown()
}
latch.countDown()
}
return TestSpaceCreationResult(spaceId, roomIds)
}
@Test
fun testRootSpaces() {
val commonTestHelper = CommonTestHelper(context())
fun testRootSpaces() = runSessionTest(context()) { commonTestHelper ->
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
/* val spaceAInfo = */ createPublicSpace(
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
session, "SpaceA", listOf(
Triple("A1", true /*auto-join*/, true/*canonical*/),
Triple("A2", true, true)
)
)
val spaceBInfo = createPublicSpace(
session, "SpaceB", listOf(
@ -506,13 +504,10 @@ class SpaceHierarchyTest : InstrumentedTest {
}
assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
commonTestHelper.signOutAndClose(session)
}
@Test
fun testParentRelation() {
val commonTestHelper = CommonTestHelper(context())
fun testParentRelation() = runSessionTest(context()) { commonTestHelper ->
val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
@ -604,8 +599,5 @@ class SpaceHierarchyTest : InstrumentedTest {
bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
}
}
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
}

View File

@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
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.content.OlmEventContent
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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@ -42,7 +43,7 @@ import javax.inject.Inject
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
private val loggerTag = LoggerTag("EventDecryptor", LoggerTag.CRYPTO)
@SessionScope
internal class EventDecryptor @Inject constructor(
@ -110,6 +111,16 @@ internal class EventDecryptor @Inject constructor(
if (eventContent == null) {
Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else if (event.isRedacted()) {
// we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm
return MXEventDecryptionResult(
clearEvent = mapOf(
"room_id" to event.roomId.orEmpty(),
"type" to EventType.MESSAGE,
"content" to emptyMap<String, Any>(),
"unsigned" to event.unsignedData.toContent()
)
)
} else {
val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)

View File

@ -96,8 +96,9 @@ internal class MXOlmDevice @Inject constructor(
// So, store these message indexes per timeline id.
//
// The first level keys are timeline ids.
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap()
// The second level values is a Map that represents:
// "<senderKey>|<session_id>|<roomId>|<message_index>" --> eventId
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, String>> = HashMap()
init {
// Retrieve the account from the store
@ -755,67 +756,72 @@ internal class MXOlmDevice @Inject constructor(
* @param body the base64-encoded body of the encrypted message.
* @param roomId the room in which the message was received.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @param eventId the eventId of the message that will be decrypted
* @param sessionId the session identifier.
* @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown.
* @return the decrypting result. Null if the sessionId is unknown.
*/
@Throws(MXCryptoError::class)
suspend fun decryptGroupMessage(body: String,
roomId: String,
timeline: String?,
eventId: String,
sessionId: String,
senderKey: String): OlmDecryptionResult {
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
val wrapper = sessionHolder.wrapper
val inboundGroupSession = wrapper.olmInboundGroupSession
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId == wrapper.roomId) {
val decryptResult = try {
sessionHolder.mutex.withLock {
inboundGroupSession.decryptMessage(body)
}
} catch (e: OlmException) {
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
throw MXCryptoError.OlmError(e)
}
if (timeline?.isNotBlank() == true) {
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
if (timelineSet.contains(messageIndexKey)) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
}
timelineSet.add(messageIndexKey)
}
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
return OlmDecryptionResult(
payload,
wrapper.keysClaimed,
senderKey,
wrapper.forwardingCurve25519KeyChain
)
} else {
if (roomId != wrapper.roomId) {
// Check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
}
val decryptResult = try {
sessionHolder.mutex.withLock {
inboundGroupSession.decryptMessage(body)
}
} catch (e: OlmException) {
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
throw MXCryptoError.OlmError(e)
}
val messageIndexKey = senderKey + "|" + sessionId + "|" + roomId + "|" + decryptResult.mIndex
Timber.tag(loggerTag.value).v("##########################################################")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() timeline: $timeline")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() senderKey: $senderKey")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() sessionId: $sessionId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() roomId: $roomId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() eventId: $eventId")
Timber.tag(loggerTag.value).v("## decryptGroupMessage() mIndex: ${decryptResult.mIndex}")
if (timeline?.isNotBlank() == true) {
val replayAttackMap = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableMapOf() }
if (replayAttackMap.contains(messageIndexKey) && replayAttackMap[messageIndexKey] != eventId) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
}
replayAttackMap[messageIndexKey] = eventId
}
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
adapter.fromJson(payloadString)
} catch (e: Exception) {
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
}
return OlmDecryptionResult(
payload,
wrapper.keysClaimed,
senderKey,
wrapper.forwardingCurve25519KeyChain
)
}
/**

View File

@ -78,6 +78,7 @@ internal class MXMegolmDecryption(
encryptedEventContent.ciphertext,
event.roomId,
timeline,
eventId = event.eventId.orEmpty(),
encryptedEventContent.sessionId,
encryptedEventContent.senderKey
)

View File

@ -520,9 +520,10 @@ internal class RoomSyncHandler @Inject constructor(
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
val timelineId = generateTimelineId(roomId)
// Event from sync does not have roomId, so add it to the event first
// note: runBlocking should be used here while we are in realm single thread executor, to avoid thread switching
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), "") }
val result = runBlocking { cryptoService.decryptEvent(event.copy(roomId = roomId), timelineId) }
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@ -537,6 +538,10 @@ internal class RoomSyncHandler @Inject constructor(
}
}
private fun generateTimelineId(roomId: String): String {
return "RoomSyncHandler$roomId"
}
data class EphemeralResult(
val typingUserIds: List<String> = emptyList()
)

View File

@ -382,7 +382,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.48'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.49'
// FlowBinding
implementation libs.github.flowBinding

View File

@ -60,6 +60,11 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.onboardingCombinedRegister,
factory = VectorFeatures::isOnboardingCombinedRegisterEnabled
),
createBooleanFeature(
label = "FTUE Combined login",
key = DebugFeatureKeys.onboardingCombinedLogin,
factory = VectorFeatures::isOnboardingCombinedLoginEnabled
),
)
)
}

View File

@ -57,6 +57,9 @@ class DebugVectorFeatures(
override fun isOnboardingCombinedRegisterEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedRegister)
?: vectorFeatures.isOnboardingCombinedRegisterEnabled()
override fun isOnboardingCombinedLoginEnabled(): Boolean = read(DebugFeatureKeys.onboardingCombinedLogin)
?: vectorFeatures.isOnboardingCombinedLoginEnabled()
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
?: vectorFeatures.isScreenSharingEnabled()
@ -113,6 +116,7 @@ object DebugFeatureKeys {
val onboardingUseCase = booleanPreferencesKey("onboarding-splash-carousel")
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
val onboardingCombinedLogin = booleanPreferencesKey("onboarding-combined-login")
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
val screenSharing = booleanPreferencesKey("screen-sharing")
}

View File

@ -72,6 +72,8 @@ class AppStateHandler @Inject constructor(
val selectedRoomGroupingFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
fun getCurrentRoomGroupingMethod(): RoomGroupingMethod? {
// XXX we should somehow make it live :/ just a work around
// For example just after creating a space and switching to it the
@ -87,12 +89,16 @@ class AppStateHandler @Inject constructor(
}
}
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false) {
fun setCurrentSpace(spaceId: String?, session: Session? = null, persistNow: Boolean = false, isForwardNavigation: Boolean = true) {
val currentSpace = (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.space()
val uSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
if (selectedSpaceDataSource.currentValue?.orNull() is RoomGroupingMethod.BySpace &&
spaceId == selectedSpaceDataSource.currentValue?.orNull()?.space()?.roomId) return
if (currentSpace != null && spaceId == currentSpace.roomId) return
val spaceSum = spaceId?.let { uSession.getRoomSummary(spaceId) }
if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId)
}
if (persistNow) {
uiStateRepository.storeGroupingMethod(true, uSession.sessionId)
uiStateRepository.storeSelectedSpace(spaceSum?.roomId, uSession.sessionId)
@ -151,6 +157,8 @@ class AppStateHandler @Inject constructor(
}.launchIn(session.coroutineScope)
}
fun getSpaceBackstack() = spaceBackstack
fun safeActiveSpaceId(): String? {
return (selectedSpaceDataSource.currentValue?.orNull() as? RoomGroupingMethod.BySpace)?.spaceSummary?.roomId
}

View File

@ -101,6 +101,9 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthAccountCreatedFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCaptchaFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedLoginFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedRegisterFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthEmailEntryFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthLegacyStyleCaptchaFragment
@ -521,6 +524,21 @@ interface FragmentModule {
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthCombinedLoginFragment::class)
fun bindFtueAuthCombinedLoginFragment(fragment: FtueAuthCombinedLoginFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthCombinedRegisterFragment::class)
fun bindFtueAuthCombinedRegisterFragment(fragment: FtueAuthCombinedRegisterFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthCombinedServerSelectionFragment::class)
fun bindFtueAuthCombinedServerSelectionFragment(fragment: FtueAuthCombinedServerSelectionFragment): Fragment
@Binds
@IntoMap
@FragmentKey(UserListFragment::class)

View File

@ -26,6 +26,7 @@ interface VectorFeatures {
fun isOnboardingUseCaseEnabled(): Boolean
fun isOnboardingPersonalizeEnabled(): Boolean
fun isOnboardingCombinedRegisterEnabled(): Boolean
fun isOnboardingCombinedLoginEnabled(): Boolean
fun isScreenSharingEnabled(): Boolean
enum class OnboardingVariant {
@ -42,5 +43,6 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isOnboardingUseCaseEnabled() = true
override fun isOnboardingPersonalizeEnabled() = false
override fun isOnboardingCombinedRegisterEnabled() = false
override fun isOnboardingCombinedLoginEnabled() = false
override fun isScreenSharingEnabled(): Boolean = true
}

View File

@ -199,43 +199,13 @@ class HomeActivity :
when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
views.drawerLayout.closeDrawer(GravityCompat.START)
// Temporary
// When switching from space to group or group to space, we need to reload the fragment
// To be removed when dropping legacy groups
if (sharedAction.clearFragment) {
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
} else {
// nop
}
// we might want to delay that to avoid having the drawer animation lagging
// would be probably better to let the drawer do that? in the on closed callback?
}
is HomeActivitySharedAction.OpenSpacePreview -> {
startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
}
is HomeActivitySharedAction.AddSpace -> {
createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
}
is HomeActivitySharedAction.ShowSpaceSettings -> {
// open bottom sheet
SpaceSettingsMenuBottomSheet
.newInstance(sharedAction.spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
override fun onShareSpaceSelected(spaceId: String) {
ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
}
})
.show(supportFragmentManager, "SPACE_SETTINGS")
}
is HomeActivitySharedAction.OpenSpaceInvite -> {
SpaceInviteBottomSheet.newInstance(sharedAction.spaceId)
.show(supportFragmentManager, "SPACE_INVITE")
}
HomeActivitySharedAction.SendSpaceFeedBack -> {
bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
}
is HomeActivitySharedAction.OpenGroup -> openGroup(sharedAction.shouldClearFragment)
is HomeActivitySharedAction.OpenSpacePreview -> startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
is HomeActivitySharedAction.AddSpace -> createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId)
is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId)
HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
HomeActivitySharedAction.CloseGroup -> closeGroup()
}
}
.launchIn(lifecycleScope)
@ -272,6 +242,37 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
}
private fun openGroup(shouldClearFragment: Boolean) {
views.drawerLayout.closeDrawer(GravityCompat.START)
// When switching from space to group or group to space, we need to reload the fragment
if (shouldClearFragment) {
replaceFragment(views.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true)
} else {
// do nothing
}
}
private fun showSpaceSettings(spaceId: String) {
// open bottom sheet
SpaceSettingsMenuBottomSheet
.newInstance(spaceId, object : SpaceSettingsMenuBottomSheet.InteractionListener {
override fun onShareSpaceSelected(spaceId: String) {
ShareSpaceBottomSheet.show(supportFragmentManager, spaceId)
}
})
.show(supportFragmentManager, "SPACE_SETTINGS")
}
private fun openSpaceInvite(spaceId: String) {
SpaceInviteBottomSheet.newInstance(spaceId)
.show(supportFragmentManager, "SPACE_INVITE")
}
private fun closeGroup() {
views.drawerLayout.openDrawer(GravityCompat.START)
}
private fun handleShowAnalyticsOptIn() {
navigator.openAnalyticsOptIn(this)
}

View File

@ -24,7 +24,8 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
data class OpenGroup(val clearFragment: Boolean) : HomeActivitySharedAction()
data class OpenGroup(val shouldClearFragment: Boolean) : HomeActivitySharedAction()
object CloseGroup : HomeActivitySharedAction()
object AddSpace : HomeActivitySharedAction()
data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction()
data class OpenSpaceInvite(val spaceId: String) : HomeActivitySharedAction()

View File

@ -33,6 +33,7 @@ import im.vector.app.R
import im.vector.app.RoomGroupingMethod
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
@ -69,7 +70,8 @@ class HomeDetailFragment @Inject constructor(
private val appStateHandler: AppStateHandler
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
KeysBackupBanner.Delegate,
CurrentCallsView.Callback {
CurrentCallsView.Callback,
OnBackPressed {
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
@ -130,12 +132,8 @@ class HomeDetailFragment @Inject constructor(
viewModel.onEach(HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod ->
when (roomGroupingMethod) {
is RoomGroupingMethod.ByLegacyGroup -> {
onGroupChange(roomGroupingMethod.groupSummary)
}
is RoomGroupingMethod.BySpace -> {
onSpaceChange(roomGroupingMethod.spaceSummary)
}
is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
}
}
@ -157,7 +155,6 @@ class HomeDetailFragment @Inject constructor(
unknownDeviceDetectorSharedViewModel.onEach { state ->
state.unknownSessions.invoke()?.let { unknownDevices ->
// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
val uid = "review_login"
alertManager.cancelAlert(uid)
@ -190,6 +187,27 @@ class HomeDetailFragment @Inject constructor(
}
}
private fun navigateBack() {
try {
val lastSpace = appStateHandler.getSpaceBackstack().removeLast()
setCurrentSpace(lastSpace)
} catch (e: NoSuchElementException) {
navigateUpOneSpace()
}
}
private fun setCurrentSpace(spaceId: String?) {
appStateHandler.setCurrentSpace(spaceId, isForwardNavigation = false)
sharedActionViewModel.post(HomeActivitySharedAction.CloseGroup)
}
private fun navigateUpOneSpace() {
val parentId = getCurrentSpace()?.flattenParentIds?.lastOrNull()
setCurrentSpace(parentId)
}
private fun getCurrentSpace() = (appStateHandler.getCurrentRoomGroupingMethod() as? RoomGroupingMethod.BySpace)?.spaceSummary
private fun handleCallStarted() {
dismissLoadingDialog()
val fragmentTag = HomeTab.DialPad.toFragmentTag()
@ -203,20 +221,16 @@ class HomeDetailFragment @Inject constructor(
override fun onResume() {
super.onResume()
// update notification tab if needed
updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
callManager.checkForProtocolsSupportIfNeeded()
refreshSpaceState()
}
// Current space/group is not live so at least refresh toolbar on resume
appStateHandler.getCurrentRoomGroupingMethod()?.let { roomGroupingMethod ->
when (roomGroupingMethod) {
is RoomGroupingMethod.ByLegacyGroup -> {
onGroupChange(roomGroupingMethod.groupSummary)
}
is RoomGroupingMethod.BySpace -> {
onSpaceChange(roomGroupingMethod.spaceSummary)
}
}
private fun refreshSpaceState() {
when (val roomGroupingMethod = appStateHandler.getCurrentRoomGroupingMethod()) {
is RoomGroupingMethod.ByLegacyGroup -> onGroupChange(roomGroupingMethod.groupSummary)
is RoomGroupingMethod.BySpace -> onSpaceChange(roomGroupingMethod.spaceSummary)
else -> Unit
}
}
@ -260,12 +274,12 @@ class HomeDetailFragment @Inject constructor(
viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer)
colorInt = colorProvider.getColorFromAttribute(R.attr.colorPrimary)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let {
(weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { activity ->
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
activity.navigator.openSettings(activity, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
@ -324,11 +338,11 @@ class HomeDetailFragment @Inject constructor(
withState(viewModel) {
when (it.roomGroupingMethod) {
is RoomGroupingMethod.ByLegacyGroup -> {
// nothing do far
// do nothing
}
is RoomGroupingMethod.BySpace -> {
it.roomGroupingMethod.spaceSummary?.let {
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(it.roomId))
it.roomGroupingMethod.spaceSummary?.let { spaceSummary ->
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
}
}
}
@ -348,17 +362,6 @@ class HomeDetailFragment @Inject constructor(
viewModel.handle(HomeDetailAction.SwitchTab(tab))
true
}
// val menuView = bottomNavigationView.getChildAt(0) as BottomNavigationMenuView
// bottomNavigationView.getOrCreateBadge()
// menuView.forEachIndexed { index, view ->
// val itemView = view as BottomNavigationItemView
// val badgeLayout = LayoutInflater.from(requireContext()).inflate(R.layout.vector_home_badge_unread_layout, menuView, false)
// val unreadCounterBadgeView: UnreadCounterBadgeView = badgeLayout.findViewById(R.id.actionUnreadCounterBadgeView)
// itemView.addView(badgeLayout)
// unreadCounterBadgeViews.add(index, unreadCounterBadgeView)
// }
}
private fun updateUIForTab(tab: HomeTab) {
@ -436,7 +439,6 @@ class HomeDetailFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) {
// Timber.v(it.toString())
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_people).render(it.notificationCountPeople, it.notificationHighlightPeople)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
@ -496,4 +498,11 @@ class HomeDetailFragment @Inject constructor(
}
return this
}
override fun onBackPressed(toolbarButton: Boolean) = if (getCurrentSpace() != null) {
navigateBack()
true
} else {
false
}
}

View File

@ -159,3 +159,9 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
}
}
fun SocialLoginButtonsView.render(ssoProviders: List<SsoIdentityProvider>?, mode: SocialLoginButtonsView.Mode, listener: (String?) -> Unit) {
this.mode = mode
this.ssoIdentityProviders = ssoProviders?.sorted()
this.listener = SocialLoginButtonsView.InteractionListener { listener(it) }
}

View File

@ -19,7 +19,7 @@ package im.vector.app.features.onboarding
import im.vector.app.R
import im.vector.app.core.extensions.andThen
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.onboarding.OnboardingAction.LoginOrRegister
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction.LoginDirect
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@ -33,8 +33,8 @@ class DirectLoginUseCase @Inject constructor(
private val uriFactory: UriFactory
) {
suspend fun execute(action: LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
return fetchWellKnown(action.username, homeServerConnectionConfig)
suspend fun execute(action: LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?): Result<Session> {
return fetchWellKnown(action.matrixId, homeServerConnectionConfig)
.andThen { wellKnown -> createSessionFor(wellKnown, action, homeServerConnectionConfig) }
}
@ -42,13 +42,13 @@ class DirectLoginUseCase @Inject constructor(
authenticationService.getWellKnownData(matrixId, config)
}
private suspend fun createSessionFor(data: WellknownResult, action: LoginOrRegister, config: HomeServerConnectionConfig?) = when (data) {
is WellknownResult.Prompt -> loginDirect(action, data, config)
private suspend fun createSessionFor(data: WellknownResult, action: LoginDirect, config: HomeServerConnectionConfig?) = when (data) {
is WellknownResult.Prompt -> loginDirect(action, data, config)
is WellknownResult.FailPrompt -> handleFailPrompt(data, action, config)
else -> onWellKnownError()
else -> onWellKnownError()
}
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginOrRegister, config: HomeServerConnectionConfig?): Result<Session> {
private suspend fun handleFailPrompt(data: WellknownResult.FailPrompt, action: LoginDirect, config: HomeServerConnectionConfig?): Result<Session> {
// Relax on IS discovery if homeserver is valid
val isMissingInformationToLogin = data.homeServerUrl == null || data.wellKnown == null
return when {
@ -57,12 +57,12 @@ class DirectLoginUseCase @Inject constructor(
}
}
private suspend fun loginDirect(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
private suspend fun loginDirect(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt, config: HomeServerConnectionConfig?): Result<Session> {
val alteredHomeServerConnectionConfig = config?.updateWith(wellKnownPrompt) ?: fallbackConfig(action, wellKnownPrompt)
return runCatching {
authenticationService.directAuthentication(
alteredHomeServerConnectionConfig,
action.username,
action.matrixId,
action.password,
action.initialDeviceName
)
@ -74,8 +74,8 @@ class DirectLoginUseCase @Inject constructor(
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
)
private fun fallbackConfig(action: LoginOrRegister, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
homeServerUri = uriFactory.parse("https://${action.username.getServerName()}"),
private fun fallbackConfig(action: LoginDirect, wellKnownPrompt: WellknownResult.Prompt) = HomeServerConnectionConfig(
homeServerUri = uriFactory.parse("https://${action.matrixId.getServerName()}"),
homeServerUriBase = uriFactory.parse(wellKnownPrompt.homeServerUrl),
identityServerUri = wellKnownPrompt.identityServerUrl?.let { uriFactory.parse(it) }
)

View File

@ -46,9 +46,12 @@ sealed interface OnboardingAction : VectorViewModelAction {
data class ResetPassword(val email: String, val newPassword: String) : OnboardingAction
object ResetPasswordMailConfirmed : OnboardingAction
// Login or Register, depending on the signMode
data class LoginOrRegister(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
data class Register(val username: String, val password: String, val initialDeviceName: String) : OnboardingAction
sealed interface AuthenticateAction : OnboardingAction {
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
}
object StopEmailValidationCheck : OnboardingAction
data class PostRegisterAction(val registerAction: RegisterAction) : OnboardingAction

View File

@ -37,6 +37,7 @@ sealed class OnboardingViewEvents : VectorViewEvents {
object OpenUseCaseSelection : OnboardingViewEvents()
object OpenServerSelection : OnboardingViewEvents()
object OpenCombinedRegister : OnboardingViewEvents()
object OpenCombinedLogin : OnboardingViewEvents()
object EditServerSelection : OnboardingViewEvents()
data class OnServerSelectionDone(val serverType: ServerType) : OnboardingViewEvents()
object OnLoginFlowRetrieved : OnboardingViewEvents()

View File

@ -42,6 +42,7 @@ import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.login.ServerType
import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
import im.vector.app.features.onboarding.StartAuthenticationFlowUseCase.StartAuthenticationResult
import im.vector.app.features.onboarding.ftueauth.MatrixOrgRegistrationStagesComparator
import kotlinx.coroutines.Job
@ -139,8 +140,7 @@ class OnboardingViewModel @AssistedInject constructor(
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
is OnboardingAction.InitWith -> handleInitWith(action)
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
is OnboardingAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action }
is OnboardingAction.Register -> handleRegisterWith(action).also { lastAction = action }
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is OnboardingAction.ResetPassword -> handleResetPassword(action)
@ -165,6 +165,14 @@ class OnboardingViewModel @AssistedInject constructor(
block(action)
}
private fun handleAuthenticateAction(action: AuthenticateAction) {
when (action) {
is AuthenticateAction.Register -> handleRegisterWith(action)
is AuthenticateAction.Login -> handleLogin(action)
is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null)
}
}
private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) {
if (resetConfig) {
loginConfig = null
@ -188,16 +196,21 @@ class OnboardingViewModel @AssistedInject constructor(
}
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
val nextOnboardingStep = when (onboardingFlow) {
OnboardingFlow.SignUp -> if (vectorFeatures.isOnboardingUseCaseEnabled()) {
OnboardingViewEvents.OpenUseCaseSelection
} else {
OnboardingViewEvents.OpenServerSelection
when (onboardingFlow) {
OnboardingFlow.SignUp -> {
_viewEvents.post(
if (vectorFeatures.isOnboardingUseCaseEnabled()) {
OnboardingViewEvents.OpenUseCaseSelection
} else {
OnboardingViewEvents.OpenServerSelection
}
)
}
OnboardingFlow.SignIn,
OnboardingFlow.SignInSignUp -> OnboardingViewEvents.OpenServerSelection
OnboardingFlow.SignIn -> if (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
handle(OnboardingAction.HomeServerChange.SelectHomeServer(defaultHomeserverUrl))
} else _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
OnboardingFlow.SignInSignUp -> _viewEvents.post(OnboardingViewEvents.OpenServerSelection)
}
_viewEvents.post(nextOnboardingStep)
}
private fun handleUserAcceptCertificate(action: OnboardingAction.UserAcceptCertificate) {
@ -209,7 +222,7 @@ class OnboardingViewModel @AssistedInject constructor(
?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) }
?.let { startAuthenticationFlow(finalLastAction, it) }
}
is OnboardingAction.LoginOrRegister ->
is AuthenticateAction.LoginDirect ->
handleDirectLogin(
finalLastAction,
HomeServerConnectionConfig.Builder()
@ -307,7 +320,7 @@ class OnboardingViewModel @AssistedInject constructor(
private fun OnboardingViewState.hasSelectedMatrixOrg() = selectedHomeserver.userFacingUrl == matrixOrgUrl
private fun handleRegisterWith(action: OnboardingAction.Register) {
private fun handleRegisterWith(action: AuthenticateAction.Register) {
reAuthHelper.data = action.password
handleRegisterAction(
RegisterAction.CreateAccount(
@ -482,16 +495,7 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleLoginOrRegister(action: OnboardingAction.LoginOrRegister) = withState { state ->
when (state.signMode) {
SignMode.Unknown -> error("Developer error, invalid sign mode")
SignMode.SignIn -> handleLogin(action)
SignMode.SignUp -> handleRegisterWith(OnboardingAction.Register(action.username, action.password, action.initialDeviceName))
SignMode.SignInWithMatrixId -> handleDirectLogin(action, null)
}
}
private fun handleDirectLogin(action: OnboardingAction.LoginOrRegister, homeServerConnectionConfig: HomeServerConnectionConfig?) {
private fun handleDirectLogin(action: AuthenticateAction.LoginDirect, homeServerConnectionConfig: HomeServerConnectionConfig?) {
setState { copy(isLoading = true) }
currentJob = viewModelScope.launch {
directLoginUseCase.execute(action, homeServerConnectionConfig).fold(
@ -504,7 +508,7 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleLogin(action: OnboardingAction.LoginOrRegister) {
private fun handleLogin(action: AuthenticateAction.Login) {
val safeLoginWizard = loginWizard
if (safeLoginWizard == null) {
@ -648,7 +652,11 @@ class OnboardingViewModel @AssistedInject constructor(
when (trigger) {
is OnboardingAction.HomeServerChange.EditHomeServer -> {
when (awaitState().onboardingFlow) {
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) { _ ->
OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) {
updateServerSelection(config, serverTypeOverride, authResult)
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
}
OnboardingFlow.SignIn -> {
updateServerSelection(config, serverTypeOverride, authResult)
_viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
}
@ -661,7 +669,10 @@ class OnboardingViewModel @AssistedInject constructor(
when (awaitState().onboardingFlow) {
OnboardingFlow.SignIn -> {
updateSignMode(SignMode.SignIn)
_viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
when (vectorFeatures.isOnboardingCombinedLoginEnabled()) {
true -> _viewEvents.post(OnboardingViewEvents.OpenCombinedLogin)
false -> _viewEvents.post(OnboardingViewEvents.OnSignModeSelected(SignMode.SignIn))
}
}
OnboardingFlow.SignUp -> {
updateSignMode(SignMode.SignUp)

View File

@ -0,0 +1,161 @@
/*
* Copyright 2019 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 im.vector.app.features.onboarding.ftueauth
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import im.vector.app.R
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.launchIn
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import javax.inject.Inject
class FtueAuthCombinedLoginFragment @Inject constructor(
private val loginFieldsValidation: LoginFieldsValidation,
private val loginErrorParser: LoginErrorParser
) : AbstractSSOFtueAuthFragment<FragmentFtueCombinedLoginBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedLoginBinding {
return FragmentFtueCombinedLoginBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
views.loginRoot.realignPercentagesToParent()
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
views.loginPasswordInput.setOnImeDoneListener { submit() }
}
private fun setupSubmitButton() {
views.loginSubmit.setOnClickListener { submit() }
observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun submit() {
cleanupUi()
loginFieldsValidation.validate(views.loginInput.content(), views.loginPasswordInput.content())
.onUsernameOrIdError { views.loginInput.error = it }
.onPasswordError { views.loginPasswordInput.error = it }
.onValid { usernameOrId, password ->
val initialDeviceName = getString(R.string.login_default_session_public_name)
viewModel.handle(OnboardingAction.AuthenticateAction.Login(usernameOrId, password, initialDeviceName))
}
}
private fun cleanupUi() {
views.loginSubmit.hideKeyboard()
views.loginInput.error = null
views.loginPasswordInput.error = null
}
override fun resetViewModel() {
viewModel.handle(OnboardingAction.ResetAuthenticationAttempt)
}
override fun onError(throwable: Throwable) {
// Trick to display the error without text.
views.loginInput.error = " "
loginErrorParser.parse(throwable, views.loginPasswordInput.content())
.onUnknown { super.onError(it) }
.onUsernameOrIdError { views.loginInput.error = it }
.onPasswordError { views.loginPasswordInput.error = it }
}
override fun updateWithState(state: OnboardingViewState) {
setupUi(state)
setupAutoFill()
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
views.selectedServerDescription.text = state.selectedHomeserver.description
if (state.isLoading) {
// Ensure password is hidden
views.loginPasswordInput.editText().hidePassword()
}
}
private fun setupUi(state: OnboardingViewState) {
when (state.selectedHomeserver.preferredLoginMode) {
is LoginMode.SsoAndPassword -> {
showUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
}
is LoginMode.Sso -> {
hideUsernamePassword()
renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
}
else -> {
showUsernamePassword()
hideSsoProviders()
}
}
}
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtonsHeader.isVisible = views.ssoGroup.isVisible && views.loginEntryGroup.isVisible
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,
providerId = id
)?.let { openInCustomTab(it) }
}
}
private fun hideSsoProviders() {
views.ssoGroup.isVisible = false
views.ssoButtons.ssoIdentityProviders = null
}
private fun hideUsernamePassword() {
views.loginEntryGroup.isVisible = false
}
private fun showUsernamePassword() {
views.loginEntryGroup.isVisible = true
}
private fun setupAutoFill() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
views.loginInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME)
views.loginPasswordInput.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD)
}
}
}

View File

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants
import androidx.core.text.isDigitsOnly
import androidx.core.view.isVisible
@ -31,22 +30,22 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
import im.vector.app.core.extensions.hasContentFlow
import im.vector.app.core.extensions.hasSurroundingSpaces
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
@ -66,36 +65,16 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
super.onViewCreated(view, savedInstanceState)
setupSubmitButton()
views.createAccountRoot.realignPercentagesToParent()
views.editServerButton.debouncedClicks {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection))
}
views.createAccountPasswordInput.editText().setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
}
private fun setupSubmitButton() {
views.createAccountSubmit.setOnClickListener { submit() }
observeInputFields()
.onEach {
views.createAccountPasswordInput.error = null
views.createAccountInput.error = null
views.createAccountSubmit.isEnabled = it
}
observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun observeInputFields() = combine(
views.createAccountInput.hasContentFlow { it.trim() },
views.createAccountPasswordInput.hasContentFlow(),
transform = { isLoginNotEmpty, isPasswordNotEmpty -> isLoginNotEmpty && isPasswordNotEmpty }
)
private fun submit() {
withState(viewModel) { state ->
cleanupUi()
@ -119,7 +98,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
}
if (error == 0) {
viewModel.handle(OnboardingAction.Register(login, password, getString(R.string.login_default_session_public_name)))
viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name)))
}
}
}
@ -185,9 +164,7 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
private fun renderSsoProviders(deviceId: String?, ssoProviders: List<SsoIdentityProvider>?) {
views.ssoGroup.isVisible = ssoProviders?.isNotEmpty() == true
views.ssoButtons.mode = SocialLoginButtonsView.Mode.MODE_CONTINUE
views.ssoButtons.ssoIdentityProviders = ssoProviders?.sorted()
views.ssoButtons.listener = SocialLoginButtonsView.InteractionListener { id ->
views.ssoButtons.render(ssoProviders, SocialLoginButtonsView.Mode.MODE_CONTINUE) { id ->
viewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = deviceId,

View File

@ -26,6 +26,7 @@ import androidx.autofill.HintConstants
import androidx.core.text.isDigitsOnly
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
@ -119,40 +120,43 @@ class FtueAuthLoginFragment @Inject constructor() : AbstractSSOFtueAuthFragment<
}
private fun submit() {
cleanupUi()
withState(viewModel) { state ->
cleanupUi()
val login = views.loginField.text.toString()
val password = views.passwordField.text.toString()
val login = views.loginField.text.toString()
val password = views.passwordField.text.toString()
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.loginFieldTil.error = getString(
if (isSignupMode) {
R.string.error_empty_field_choose_user_name
} else {
R.string.error_empty_field_enter_user_name
}
)
error++
}
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
error++
}
if (password.isEmpty()) {
views.passwordFieldTil.error = getString(
if (isSignupMode) {
R.string.error_empty_field_choose_password
} else {
R.string.error_empty_field_your_password
}
)
error++
}
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
views.loginFieldTil.error = getString(
if (isSignupMode) {
R.string.error_empty_field_choose_user_name
} else {
R.string.error_empty_field_enter_user_name
}
)
error++
}
if (isSignupMode && isNumericOnlyUserIdForbidden && login.isDigitsOnly()) {
views.loginFieldTil.error = getString(R.string.error_forbidden_digits_only_username)
error++
}
if (password.isEmpty()) {
views.passwordFieldTil.error = getString(
if (isSignupMode) {
R.string.error_empty_field_choose_password
} else {
R.string.error_empty_field_your_password
}
)
error++
}
if (error == 0) {
viewModel.handle(OnboardingAction.LoginOrRegister(login, password, getString(R.string.login_default_session_public_name)))
if (error == 0) {
val initialDeviceName = getString(R.string.login_default_session_public_name)
viewModel.handle(state.signMode.toAuthenticateAction(login, password, initialDeviceName))
}
}
}

View File

@ -227,10 +227,15 @@ class FtueAuthVariant(
option = commonOption
)
}
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
}
}
private fun onStartCombinedLogin() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java)
}
private fun onRegistrationFlow(viewEvents: OnboardingViewEvents.RegistrationFlowResult) {
when {
registrationShouldFallback(viewEvents) -> displayFallbackWebDialog()

View File

@ -0,0 +1,51 @@
/*
* 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 im.vector.app.features.onboarding.ftueauth
import android.widget.Button
import com.google.android.material.textfield.TextInputLayout
import im.vector.app.core.extensions.hasContentFlow
import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.OnboardingAction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
return when (this) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> OnboardingAction.AuthenticateAction.Register(username = login, password, initialDeviceName)
SignMode.SignIn -> OnboardingAction.AuthenticateAction.Login(username = login, password, initialDeviceName)
SignMode.SignInWithMatrixId -> OnboardingAction.AuthenticateAction.LoginDirect(matrixId = login, password, initialDeviceName)
}
}
/**
* A flow to monitor content changes from both username/id and password fields,
* clearing errors and enabling/disabling the submission button on non empty content changes.
*/
fun observeContentChangesAndResetErrors(username: TextInputLayout, password: TextInputLayout, submit: Button): Flow<*> {
return combine(
username.hasContentFlow { it.trim() },
password.hasContentFlow(),
transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent }
).onEach {
username.error = null
password.error = null
submit.isEnabled = it
}
}

View File

@ -0,0 +1,69 @@
/*
* 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 im.vector.app.features.onboarding.ftueauth
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.onboarding.ftueauth.LoginErrorParser.LoginErrorResult
import org.matrix.android.sdk.api.failure.isInvalidPassword
import org.matrix.android.sdk.api.failure.isInvalidUsername
import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
import javax.inject.Inject
class LoginErrorParser @Inject constructor(
private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider,
) {
fun parse(throwable: Throwable, password: String): LoginErrorResult {
return when {
throwable.isInvalidUsername() -> {
LoginErrorResult(throwable, usernameOrIdError = errorFormatter.toHumanReadable(throwable))
}
throwable.isLoginEmailUnknown() -> {
LoginErrorResult(throwable, usernameOrIdError = stringProvider.getString(R.string.login_login_with_email_error))
}
throwable.isInvalidPassword() && password.hasSurroundingSpaces() -> {
LoginErrorResult(throwable, passwordError = stringProvider.getString(R.string.auth_invalid_login_param_space_in_password))
}
else -> {
LoginErrorResult(throwable)
}
}
}
private fun String.hasSurroundingSpaces() = trim() != this
data class LoginErrorResult(val cause: Throwable, val usernameOrIdError: String? = null, val passwordError: String? = null)
}
fun LoginErrorResult.onUnknown(action: (Throwable) -> Unit): LoginErrorResult {
when {
usernameOrIdError == null && passwordError == null -> action(cause)
}
return this
}
fun LoginErrorResult.onUsernameOrIdError(action: (String) -> Unit): LoginErrorResult {
usernameOrIdError?.let(action)
return this
}
fun LoginErrorResult.onPasswordError(action: (String) -> Unit): LoginErrorResult {
passwordError?.let(action)
return this
}

View File

@ -0,0 +1,63 @@
/*
* 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 im.vector.app.features.onboarding.ftueauth
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import javax.inject.Inject
class LoginFieldsValidation @Inject constructor(
private val stringProvider: StringProvider
) {
fun validate(usernameOrId: String, password: String): LoginValidationResult {
return LoginValidationResult(usernameOrId, password, validateUsernameOrId(usernameOrId), validatePassword(password))
}
private fun validateUsernameOrId(usernameOrId: String): String? {
val accountError = when {
usernameOrId.isEmpty() -> stringProvider.getString(R.string.error_empty_field_enter_user_name)
else -> null
}
return accountError
}
private fun validatePassword(password: String): String? {
val passwordError = when {
password.isEmpty() -> stringProvider.getString(R.string.error_empty_field_your_password)
else -> null
}
return passwordError
}
}
fun LoginValidationResult.onValid(action: (String, String) -> Unit): LoginValidationResult {
when {
usernameOrIdError == null && passwordError == null -> action(usernameOrId, password)
}
return this
}
fun LoginValidationResult.onUsernameOrIdError(action: (String) -> Unit): LoginValidationResult {
usernameOrIdError?.let(action)
return this
}
fun LoginValidationResult.onPasswordError(action: (String) -> Unit): LoginValidationResult {
passwordError?.let(action)
return this
}

View File

@ -0,0 +1,24 @@
/*
* 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 im.vector.app.features.onboarding.ftueauth
data class LoginValidationResult(
val usernameOrId: String,
val password: String,
val usernameOrIdError: String?,
val passwordError: String?
)

View File

@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/LoginFormScrollView"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:fillViewport="true"
android:paddingTop="0dp"
android:paddingBottom="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginRoot"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/loginGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/loginGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/headerSpacing"
android:layout_width="match_parent"
android:layout_height="52dp"
app:layout_constraintBottom_toTopOf="@id/loginHeaderTitle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/loginHeaderTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/ftue_auth_welcome_back_title"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/headerSpacing" />
<Space
android:id="@+id/titleContentSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/chooseYourServerHeader"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/loginHeaderTitle" />
<TextView
android:id="@+id/chooseYourServerHeader"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:text="@string/ftue_auth_create_account_choose_server_header"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/selectedServerName"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing" />
<TextView
android:id="@+id/selectedServerName"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader" />
<TextView
android:id="@+id/selectedServerDescription"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textColor="?vctr_content_tertiary"
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/selectedServerName" />
<Button
android:id="@+id/editServerButton"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/ftue_auth_create_account_edit_server_selection"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="@id/selectedServerDescription"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintTop_toTopOf="@id/chooseYourServerHeader" />
<Space
android:id="@+id/serverSelectionSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginInput"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/selectedServerDescription" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_content_quaternary"
app:layout_constraintBottom_toBottomOf="@id/serverSelectionSpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toTopOf="@id/serverSelectionSpacing" />
<androidx.constraintlayout.widget.Group
android:id="@+id/loginEntryGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="loginInput,loginPasswordInput,entrySpacing,actionSpacing,loginSubmit" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginInput"
style="@style/Widget.Vector.TextInputLayout.Username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/username"
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/serverSelectionSpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionNext"
android:inputType="text"
android:maxLines="1"
android:nextFocusForward="@id/loginPasswordInput" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:id="@+id/entrySpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginPasswordInput"
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/loginInput" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginPasswordInput"
style="@style/Widget.Vector.TextInputLayout.Password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/entrySpacing">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<Space
android:id="@+id/actionSpacing"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/loginSubmit"
app:layout_constraintHeight_percent="0.02"
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
<Button
android:id="@+id/loginSubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_signup_submit"
android:textAllCaps="true"
app:layout_constraintBottom_toTopOf="@id/ssoButtonsHeader"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/actionSpacing" />
<androidx.constraintlayout.widget.Group
android:id="@+id/ssoGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="ssoButtonsHeader,ssoButtons"
app:layout_constraintBottom_toTopOf="@id/ssoButtonsHeader"
app:layout_constraintTop_toBottomOf="@id/loginSubmit"
tools:visibility="visible" />
<TextView
android:id="@+id/ssoButtonsHeader"
style="@style/Widget.Vector.TextView.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/ftue_auth_create_account_sso_section_header"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/ssoButtons"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginSubmit" />
<im.vector.app.features.login.SocialLoginButtonsView
android:id="@+id/ssoButtons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
tools:signMode="signup" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -153,4 +153,4 @@
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/home_bottom_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -19,6 +19,8 @@
<string name="ftue_auth_create_account_matrix_dot_org_server_description">Join millions for free on the largest public server</string>
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
<string name="ftue_auth_welcome_back_title">Welcome back!</string>
<string name="ftue_auth_choose_server_title">Choose your server</string>
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? Server is like a home for all your data.</string>
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>

View File

@ -32,13 +32,13 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.WellKnown
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
private val A_DIRECT_LOGIN_ACTION = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
private val A_WELLKNOWN_SUCCESS_RESULT = WellknownResult.Prompt("https://homeserverurl.com", identityServerUrl = null, WellKnown())
private val A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT = WellknownResult.FailPrompt("https://homeserverurl.com", WellKnown())
private val A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT = WellknownResult.FailPrompt(null, null)
private val NO_HOMESERVER_CONFIG: HomeServerConnectionConfig? = null
private val A_FALLBACK_CONFIG: HomeServerConnectionConfig = HomeServerConnectionConfig(
homeServerUri = FakeUri("https://${A_LOGIN_OR_REGISTER_ACTION.username.getServerName()}").instance,
homeServerUri = FakeUri("https://${A_DIRECT_LOGIN_ACTION.matrixId.getServerName()}").instance,
homeServerUriBase = FakeUri(A_WELLKNOWN_SUCCESS_RESULT.homeServerUrl).instance,
identityServerUri = null
)
@ -54,11 +54,11 @@ class DirectLoginUseCaseTest {
@Test
fun `when logging in directly, then returns success with direct session result`() = runTest {
fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.success(fakeSession)
}
@ -66,14 +66,14 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown fails with content, when logging in directly, then returns success with direct session result`() = runTest {
fakeAuthenticationService.givenWellKnown(
A_LOGIN_OR_REGISTER_ACTION.username,
A_DIRECT_LOGIN_ACTION.matrixId,
config = NO_HOMESERVER_CONFIG,
result = A_WELLKNOWN_FAILED_WITH_CONTENT_RESULT
)
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.success(fakeSession)
}
@ -81,14 +81,14 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown fails without content, when logging in directly, then returns well known error`() = runTest {
fakeAuthenticationService.givenWellKnown(
A_LOGIN_OR_REGISTER_ACTION.username,
A_DIRECT_LOGIN_ACTION.matrixId,
config = NO_HOMESERVER_CONFIG,
result = A_WELLKNOWN_FAILED_WITHOUT_CONTENT_RESULT
)
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthentication(A_FALLBACK_CONFIG, username, password, initialDeviceName, result = fakeSession)
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result should { this.isFailure }
result should { this.exceptionOrNull() is Exception }
@ -97,20 +97,20 @@ class DirectLoginUseCaseTest {
@Test
fun `given wellknown throws, when logging in directly, then returns failure result with original cause`() = runTest {
fakeAuthenticationService.givenWellKnownThrows(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
fakeAuthenticationService.givenWellKnownThrows(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, cause = AN_ERROR)
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.failure(AN_ERROR)
}
@Test
fun `given direct authentication throws, when logging in directly, then returns failure result with original cause`() = runTest {
fakeAuthenticationService.givenWellKnown(A_LOGIN_OR_REGISTER_ACTION.username, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
val (username, password, initialDeviceName) = A_LOGIN_OR_REGISTER_ACTION
fakeAuthenticationService.givenWellKnown(A_DIRECT_LOGIN_ACTION.matrixId, config = NO_HOMESERVER_CONFIG, result = A_WELLKNOWN_SUCCESS_RESULT)
val (username, password, initialDeviceName) = A_DIRECT_LOGIN_ACTION
fakeAuthenticationService.givenDirectAuthenticationThrows(A_FALLBACK_CONFIG, username, password, initialDeviceName, cause = AN_ERROR)
val result = useCase.execute(A_LOGIN_OR_REGISTER_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
val result = useCase.execute(A_DIRECT_LOGIN_ACTION, homeServerConnectionConfig = NO_HOMESERVER_CONFIG)
result shouldBeEqualTo Result.failure(AN_ERROR)
}

View File

@ -59,7 +59,7 @@ private val A_RESULT_IGNORED_REGISTER_ACTION = RegisterAction.SendAgainThreePid
private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canChangeDisplayName = true, canChangeAvatar = true)
private val AN_IGNORED_FLOW_RESULT = FlowResult(missingStages = emptyList(), completedStages = emptyList())
private val ANY_CONTINUING_REGISTRATION_RESULT = RegistrationResult.NextStep(AN_IGNORED_FLOW_RESULT)
private val A_LOGIN_OR_REGISTER_ACTION = OnboardingAction.LoginOrRegister("@a-user:id.org", "a-password", "a-device-name")
private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a-user:id.org", "a-password", "a-device-name")
private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
@ -142,11 +142,11 @@ class OnboardingViewModelTest {
@Test
fun `given has sign in with matrix id sign mode, when handling login or register action, then logs in directly`() = runTest {
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
fakeDirectLoginUseCase.givenSuccessResult(A_LOGIN_OR_REGISTER_ACTION, config = null, result = fakeSession)
fakeDirectLoginUseCase.givenSuccessResult(A_DIRECT_LOGIN, config = null, result = fakeSession)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
viewModel.handle(A_DIRECT_LOGIN)
test
.assertStatesChanges(
@ -161,11 +161,11 @@ class OnboardingViewModelTest {
@Test
fun `given has sign in with matrix id sign mode, when handling login or register action fails, then emits error`() = runTest {
viewModelWith(initialState.copy(signMode = SignMode.SignInWithMatrixId))
fakeDirectLoginUseCase.givenFailureResult(A_LOGIN_OR_REGISTER_ACTION, config = null, cause = AN_ERROR)
fakeDirectLoginUseCase.givenFailureResult(A_DIRECT_LOGIN, config = null, cause = AN_ERROR)
givenInitialisesSession(fakeSession)
val test = viewModel.test()
viewModel.handle(A_LOGIN_OR_REGISTER_ACTION)
viewModel.handle(A_DIRECT_LOGIN)
test
.assertStatesChanges(

View File

@ -17,7 +17,7 @@
package im.vector.app.test.fakes
import im.vector.app.features.onboarding.DirectLoginUseCase
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
import io.mockk.coEvery
import io.mockk.mockk
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@ -25,11 +25,11 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
class FakeDirectLoginUseCase {
val instance = mockk<DirectLoginUseCase>()
fun givenSuccessResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, result: FakeSession) {
fun givenSuccessResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, result: FakeSession) {
coEvery { instance.execute(action, config) } returns Result.success(result)
}
fun givenFailureResult(action: OnboardingAction.LoginOrRegister, config: HomeServerConnectionConfig?, cause: Throwable) {
fun givenFailureResult(action: AuthenticateAction.LoginDirect, config: HomeServerConnectionConfig?, cause: Throwable) {
coEvery { instance.execute(action, config) } returns Result.failure(cause)
}
}