Merge pull request #2429 from element-hq/feature/bma/testRecoveryKey

Test recovery key
This commit is contained in:
Benoit Marty
2024-02-22 17:37:11 +01:00
committed by GitHub
34 changed files with 143 additions and 88 deletions

View File

@@ -55,9 +55,10 @@ jobs:
# app-file should point to an x86 compatible APK file, so upload the x86_64 one (much smaller than the universal APK).
app-file: app/build/outputs/apk/gplay/debug/app-gplay-x86_64-debug.apk
env: |
USERNAME=maestroelement
PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
ROOM_NAME=MyRoom
INVITEE1_MXID=@maestroelement2:matrix.org
INVITEE2_MXID=@maestroelement3:matrix.org
APP_ID=io.element.android.x.debug
MAESTRO_USERNAME=maestroelement
MAESTRO_PASSWORD=${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
MAESTRO_RECOVERY_KEY=${{ secrets.MATRIX_MAESTRO_ACCOUNT_RECOVERY_KEY }}
MAESTRO_ROOM_NAME=MyRoom
MAESTRO_INVITEE1_MXID=@maestroelement2:matrix.org
MAESTRO_INVITEE2_MXID=@maestroelement3:matrix.org
MAESTRO_APP_ID=io.element.android.x.debug

View File

@@ -22,12 +22,13 @@ From root dir of the project
```shell
maestro test \
-e APP_ID=io.element.android.x.debug \
-e USERNAME=user1 \
-e PASSWORD=123 \
-e ROOM_NAME="MyRoom" \
-e INVITEE1_MXID=user2 \
-e INVITEE2_MXID=user3 \
-e MAESTRO_APP_ID=io.element.android.x.debug \
-e MAESTRO_USERNAME=user1 \
-e MAESTRO_PASSWORD=123 \
-e MAESTRO_RECOVERY_KEY=ABC \
-e MAESTRO_ROOM_NAME="MyRoom" \
-e MAESTRO_INVITEE1_MXID=user2 \
-e MAESTRO_INVITEE2_MXID=user3 \
.maestro/allTests.yaml
```

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
## Check that all env variables required in the whole test suite are declared (to fail faster)
- runScript: ./scripts/checkEnv.js

View File

@@ -1,9 +1,10 @@
// This array contains all the required environment variable. When adding a variable, add it here also.
// If a variable is missing, an error will occur.
if (APP_ID == null) throw "Fatal: missing env variable APP_ID"
if (USERNAME == null) throw "Fatal: missing env variable USERNAME"
if (PASSWORD == null) throw "Fatal: missing env variable PASSWORD"
if (ROOM_NAME == null) throw "Fatal: missing env variable ROOM_NAME"
if (INVITEE1_MXID == null) throw "Fatal: missing env variable INVITEE1_MXID"
if (INVITEE2_MXID == null) throw "Fatal: missing env variable INVITEE2_MXID"
if (MAESTRO_APP_ID == null) throw "Fatal: missing env variable MAESTRO_APP_ID"
if (MAESTRO_USERNAME == null) throw "Fatal: missing env variable MAESTRO_USERNAME"
if (MAESTRO_PASSWORD == null) throw "Fatal: missing env variable MAESTRO_PASSWORD"
if (MAESTRO_RECOVERY_KEY == null) throw "Fatal: missing env variable MAESTRO_RECOVERY_KEY"
if (MAESTRO_ROOM_NAME == null) throw "Fatal: missing env variable MAESTRO_ROOM_NAME"
if (MAESTRO_INVITEE1_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE1_MXID"
if (MAESTRO_INVITEE2_MXID == null) throw "Fatal: missing env variable MAESTRO_INVITEE2_MXID"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- tapOn: "Continue"
- runFlow: ../assertions/assertLoginDisplayed.yaml
@@ -9,7 +9,7 @@ appId: ${APP_ID}
id: "login-continue"
- tapOn:
id: "login-email_username"
- inputText: ${USERNAME}
- inputText: ${MAESTRO_USERNAME}
- pressKey: Enter
- tapOn:
id: "login-password"
@@ -20,7 +20,7 @@ appId: ${APP_ID}
- tapOn:
id: "login-password"
- eraseText: 20
- inputText: ${PASSWORD}
- inputText: ${MAESTRO_PASSWORD}
- pressKey: Enter
- tapOn: "Continue"
- runFlow: ../assertions/assertWelcomeScreenDisplayed.yaml
@@ -28,3 +28,4 @@ appId: ${APP_ID}
- runFlow: ../assertions/assertAnalyticsDisplayed.yaml
- tapOn: "Not now"
- runFlow: ../assertions/assertHomeDisplayed.yaml
- runFlow: ./verifySession.yaml

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "home_screen-settings"

View File

@@ -0,0 +1,11 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn: "Continue"
- takeScreenshot: build/maestro/150-Verify
- tapOn: "Enter recovery key"
- tapOn:
id: "verification-recovery_key"
- inputText: ${MAESTRO_RECOVERY_KEY}
- hideKeyboard
- tapOn: "Confirm"
- runFlow: ../assertions/assertHomeDisplayed.yaml

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Help improve Element X dbg"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "All Chats"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Be in your element"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Change account provider"

View File

@@ -1,5 +1,5 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: ${ROOM_NAME}
visible: ${MAESTRO_ROOM_NAME}
timeout: 10000

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible:

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- clearState
- launchApp:

View File

@@ -1,13 +1,14 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the creation and deletion of a DM room.
- tapOn: "Create a new conversation or room"
- tapOn: "Search for someone"
- inputText: ${INVITEE1_MXID}
- inputText: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: ${INVITEE1_MXID}
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2"
- scroll
- tapOn: "Leave conversation"
- tapOn: "Leave"

View File

@@ -1,12 +1,12 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the creation and deletion of a room
- tapOn: "Create a new conversation or room"
- tapOn: "New room"
- tapOn: "Search for someone"
- inputText: ${INVITEE1_MXID}
- inputText: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: ${INVITEE1_MXID}
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- tapOn: "Next"
- tapOn: "e.g. your project name"
@@ -19,9 +19,9 @@ appId: ${APP_ID}
- tapOn: "Invite people"
# assert there's 1 member and 1 invitee
- tapOn: "Search for someone"
- inputText: ${INVITEE2_MXID}
- inputText: ${MAESTRO_INVITEE2_MXID}
- tapOn:
text: ${INVITEE2_MXID}
text: ${MAESTRO_INVITEE2_MXID}
index: 1
- tapOn: "Invite"
- tapOn: "Back"

View File

@@ -1,13 +1,13 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
# Purpose: Test the context menu of a room in the room list
- longPressOn: ${ROOM_NAME}
- longPressOn: ${MAESTRO_ROOM_NAME}
- takeScreenshot: build/maestro/310-RoomList-ContextMenu
- tapOn:
text: "Settings"
index: 0
- tapOn: "Back"
- longPressOn: ${ROOM_NAME}
- longPressOn: ${MAESTRO_ROOM_NAME}
- tapOn:
text: "Leave room"
index: 0

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList

View File

@@ -1,10 +1,10 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- runFlow: ../assertions/assertRoomListSynced.yaml
- tapOn: "search"
- inputText: ${ROOM_NAME.substring(0, 3)}
- inputText: ${MAESTRO_ROOM_NAME.substring(0, 3)}
- takeScreenshot: build/maestro/400-SearchRoom
- tapOn: ${ROOM_NAME}
- tapOn: ${MAESTRO_ROOM_NAME}
# Back from timeline
- back
- assertVisible: "MyR"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/520-Timeline
- tapOn: "Add attachment"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/530-Timeline
- tapOn: "Add attachment"

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- takeScreenshot: build/maestro/510-Timeline
- tapOn:

View File

@@ -1,7 +1,7 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
# This is the name of one room
- tapOn: ${ROOM_NAME}
- tapOn: ${MAESTRO_ROOM_NAME}
- takeScreenshot: build/maestro/500-Timeline
- runFlow: messages/text.yaml
- runFlow: messages/location.yaml

View File

@@ -1,4 +1,4 @@
appId: ${APP_ID}
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "home_screen-settings"

View File

@@ -338,6 +338,10 @@ class LoggedInFlowNode @AssistedInject constructor(
)
)
}
override fun onDone() {
backstack.pop()
}
}
verifySessionEntryPoint
.nodeBuilder(this, buildContext)

View File

@@ -42,6 +42,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
api(libs.statemachine)
api(projects.features.securebackup.api)

View File

@@ -53,6 +53,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@@ -169,6 +171,7 @@ private fun RecoveryKeyFormContent(
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.recoveryKey)
.autofill(
autofillTypes = listOf(AutofillType.Password),
onFill = { onChange(it) },

View File

@@ -31,5 +31,6 @@ interface VerifySessionEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onEnterRecoveryKey()
fun onDone()
}
}

View File

@@ -40,14 +40,20 @@ class VerifySelfSessionNode @AssistedInject constructor(
}
}
private fun onDone() {
plugins<VerifySessionEntryPoint.Callback>().forEach {
it.onDone()
}
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
VerifySelfSessionView(
state = state,
modifier = modifier,
onEnterRecoveryKey = { onEnterRecoveryKey() },
goBack = { navigateUp() }
onEnterRecoveryKey = ::onEnterRecoveryKey,
goBack = ::onDone,
)
}
}

View File

@@ -138,7 +138,7 @@ class RustMatrixClient(
syncService = rustSyncService,
sessionCoroutineScope = sessionCoroutineScope,
dispatchers = dispatchers,
).apply { start() }
)
private val sessionDirectoryNameProvider = SessionDirectoryNameProvider()
private val isLoggingOut = AtomicBoolean(false)

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 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 io.element.android.libraries.matrix.impl.encryption
import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.flow.Flow
import org.matrix.rustcomponents.sdk.BackupStateListener
import org.matrix.rustcomponents.sdk.EncryptionInterface
import org.matrix.rustcomponents.sdk.RecoveryStateListener
import org.matrix.rustcomponents.sdk.BackupState as RustBackupState
import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState
internal fun EncryptionInterface.backupStateFlow(): Flow<BackupState> = mxCallbackFlow {
val backupStateMapper = BackupStateMapper()
trySend(backupStateMapper.map(backupState()))
val listener = object : BackupStateListener {
override fun onUpdate(status: RustBackupState) {
trySend(backupStateMapper.map(status))
}
}
backupStateListener(listener)
}
internal fun EncryptionInterface.recoveryStateFlow(): Flow<RecoveryState> = mxCallbackFlow {
val recoveryStateMapper = RecoveryStateMapper()
trySend(recoveryStateMapper.map(recoveryState()))
val listener = object : RecoveryStateListener {
override fun onUpdate(status: RustRecoveryState) {
trySend(recoveryStateMapper.map(status))
}
}
recoveryStateListener(listener)
}

View File

@@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.currentCoroutineContext
@@ -40,17 +39,12 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackupStateListener
import org.matrix.rustcomponents.sdk.BackupSteadyStateListener
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
import org.matrix.rustcomponents.sdk.Encryption
import org.matrix.rustcomponents.sdk.RecoveryStateListener
import org.matrix.rustcomponents.sdk.TaskHandle
import org.matrix.rustcomponents.sdk.BackupState as RustBackupState
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
internal class RustEncryptionService(
@@ -61,18 +55,12 @@ internal class RustEncryptionService(
) : EncryptionService {
private val service: Encryption = client.encryption()
private val backupStateMapper = BackupStateMapper()
private val recoveryStateMapper = RecoveryStateMapper()
private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper()
private val backupUploadStateMapper = BackupUploadStateMapper()
private val steadyStateExceptionMapper = SteadyStateExceptionMapper()
private var backupStateListenerTaskHandle: TaskHandle? = null
private var recoveryStateListenerTaskHandle: TaskHandle? = null
private val backupStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map))
override val backupStateStateFlow = combine(
backupStateFlow,
service.backupStateFlow(),
syncService.syncState,
) { backupState, syncState ->
if (syncState == SyncState.Running) {
@@ -82,10 +70,8 @@ internal class RustEncryptionService(
}
}.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, BackupState.WAITING_FOR_SYNC)
private val recoveryStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map))
override val recoveryStateStateFlow = combine(
recoveryStateFlow,
service.recoveryStateFlow(),
syncService.syncState,
) { recoveryState, syncState ->
if (syncState == SyncState.Running) {
@@ -111,23 +97,7 @@ internal class RustEncryptionService(
}
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
fun start() {
backupStateListenerTaskHandle = service.backupStateListener(object : BackupStateListener {
override fun onUpdate(status: RustBackupState) {
backupStateFlow.value = backupStateMapper.map(status)
}
})
recoveryStateListenerTaskHandle = service.recoveryStateListener(object : RecoveryStateListener {
override fun onUpdate(status: RustRecoveryState) {
recoveryStateFlow.value = recoveryStateMapper.map(status)
}
})
}
fun destroy() {
backupStateListenerTaskHandle?.cancelAndDestroy()
recoveryStateListenerTaskHandle?.cancelAndDestroy()
service.destroy()
}

View File

@@ -33,6 +33,11 @@ object TestTags {
val loginPassword = TestTag("login-password")
val loginContinue = TestTag("login-continue")
/**
* Verification screen.
*/
val recoveryKey = TestTag("verification-recovery_key")
/**
* Sign out screen.
*/