Merge pull request #7276 from vector-im/feature/mna/device-manager-account-data

[Device Management] Save matrix_client_information events on login/registration (PSG-769, PSG-771)
This commit is contained in:
Maxime NATUREL 2022-10-12 15:41:40 +02:00 committed by GitHub
commit 6626732b3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 820 additions and 25 deletions

1
changelog.d/7257.wip Normal file
View file

@ -0,0 +1 @@
[Device Management] Save "matrix_client_information" events on login/registration

View file

@ -66,7 +66,7 @@ internal class DefaultSessionAccountDataService @Inject constructor(
override suspend fun updateUserAccountData(type: String, content: Content) { override suspend fun updateUserAccountData(type: String, content: Content) {
val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content) val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content)
awaitCallback<Unit> { callback -> awaitCallback { callback ->
updateUserAccountDataTask.configureWith(params) { updateUserAccountDataTask.configureWith(params) {
this.retryCount = 5 // TODO Need to refactor retrying out into a helper method. this.retryCount = 5 // TODO Need to refactor retrying out into a helper method.
this.callback = callback this.callback = callback

View file

@ -19,10 +19,10 @@ package im.vector.app.core.di
import android.content.Context import android.content.Context
import arrow.core.Option import arrow.core.Option
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
@ -50,6 +50,7 @@ class ActiveSessionHolder @Inject constructor(
private val sessionInitializer: SessionInitializer, private val sessionInitializer: SessionInitializer,
private val applicationContext: Context, private val applicationContext: Context,
private val authenticationService: AuthenticationService, private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) { ) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference() private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
@ -109,7 +110,9 @@ class ActiveSessionHolder @Inject constructor(
} }
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session -> ?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { session ->
setActiveSession(session) setActiveSession(session)
session.configureAndStart(applicationContext, startSyncing = startSync) runBlocking {
configureAndStartSessionUseCase.execute(session, startSyncing = startSync)
}
} }
} }

View file

@ -24,20 +24,8 @@ import im.vector.app.core.services.VectorSyncAndroidService
import im.vector.app.features.session.VectorSessionStore import im.vector.app.features.session.VectorSessionStore
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.sync.FilterService
import timber.log.Timber import timber.log.Timber
fun Session.configureAndStart(context: Context, startSyncing: Boolean = true) {
Timber.i("Configure and start session for $myUserId. startSyncing: $startSyncing")
open()
filterService().setFilter(FilterService.FilterPreset.ElementFilter)
if (startSyncing) {
startSyncing(context)
}
pushersService().refreshPushers()
context.singletonEntryPoint().webRtcCallManager().checkForProtocolsSupportIfNeeded()
}
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
if (!syncService().hasAlreadySynced()) { if (!syncService().hasAlreadySynced()) {

View file

@ -0,0 +1,46 @@
/*
* 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.core.session
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.FilterService
import timber.log.Timber
import javax.inject.Inject
class ConfigureAndStartSessionUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val webRtcCallManager: WebRtcCallManager,
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
) {
suspend fun execute(session: Session, startSyncing: Boolean = true) {
Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing")
session.open()
session.filterService().setFilter(FilterService.FilterPreset.ElementFilter)
if (startSyncing) {
session.startSyncing(context)
}
session.pushersService().refreshPushers()
webRtcCallManager.checkForProtocolsSupportIfNeeded()
updateMatrixClientInfoUseCase.execute(session)
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.core.session.clientinfo
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toModel
import javax.inject.Inject
/**
* This use case retrieves the current account data event containing extended client info
* for a given deviceId.
*/
class GetMatrixClientInfoUseCase @Inject constructor() {
fun execute(session: Session, deviceId: String): MatrixClientInfoContent? {
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId
val content = session.accountDataService().getUserAccountDataEvent(type)?.content
return content.toModel()
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.core.session.clientinfo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class MatrixClientInfoContent(
// app name
@Json(name = "name")
val name: String? = null,
// app version
@Json(name = "version")
val version: String? = null,
// app url (optional, applicable only for web)
@Json(name = "url")
val url: String? = null,
)

View file

@ -0,0 +1,19 @@
/*
* 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.core.session.clientinfo
class NoDeviceIdError : IllegalStateException("device id is empty")

View file

@ -0,0 +1,22 @@
/*
* 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.core.session.clientinfo
/**
* Prefix for the key account data event which holds client info.
*/
const val MATRIX_CLIENT_INFO_KEY_PREFIX = "io.element.matrix_client_information."

View file

@ -0,0 +1,38 @@
/*
* 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.core.session.clientinfo
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toContent
import javax.inject.Inject
/**
* This use case sets the account data event containing extended client info.
*/
class SetMatrixClientInfoUseCase @Inject constructor() {
suspend fun execute(session: Session, clientInfo: MatrixClientInfoContent): Result<Unit> = runCatching {
val deviceId = session.sessionParams.deviceId.orEmpty()
if (deviceId.isNotEmpty()) {
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId
session.accountDataService()
.updateUserAccountData(type, clientInfo.toContent())
} else {
throw NoDeviceIdError()
}
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.core.session.clientinfo
import im.vector.app.core.resources.AppNameProvider
import im.vector.app.core.resources.BuildMeta
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import javax.inject.Inject
/**
* This use case updates if needed the account data event containing extended client info.
*/
class UpdateMatrixClientInfoUseCase @Inject constructor(
private val appNameProvider: AppNameProvider,
private val buildMeta: BuildMeta,
private val getMatrixClientInfoUseCase: GetMatrixClientInfoUseCase,
private val setMatrixClientInfoUseCase: SetMatrixClientInfoUseCase,
) {
suspend fun execute(session: Session) = runCatching {
val clientInfo = MatrixClientInfoContent(
name = appNameProvider.getAppName(),
version = buildMeta.versionName
)
val deviceId = session.sessionParams.deviceId.orEmpty()
if (deviceId.isNotEmpty()) {
val storedClientInfo = getMatrixClientInfoUseCase.execute(session, deviceId)
Timber.d("storedClientInfo=$storedClientInfo, current client info=$clientInfo")
if (clientInfo != storedClientInfo) {
Timber.d("client info need to be updated")
return setMatrixClientInfoUseCase.execute(session, clientInfo)
}
} else {
throw NoDeviceIdError()
}
}
}

View file

@ -30,9 +30,9 @@ import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.core.utils.ensureTrailingSlash
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,7 +64,8 @@ class LoginViewModel @AssistedInject constructor(
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService private val homeServerHistoryService: HomeServerHistoryService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) { ) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -732,7 +733,7 @@ class LoginViewModel @AssistedInject constructor(
activeSessionHolder.setActiveSession(session) activeSessionHolder.setActiveSession(session)
authenticationService.reset() authenticationService.reset()
session.configureAndStart(applicationContext) configureAndStartSessionUseCase.execute(session)
setState { setState {
copy( copy(
asyncLoginAction = Success(Unit) asyncLoginAction = Success(Unit)

View file

@ -26,13 +26,13 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.cancelCurrentOnSet import im.vector.app.core.extensions.cancelCurrentOnSet
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.inferNoConnectivity import im.vector.app.core.extensions.inferNoConnectivity
import im.vector.app.core.extensions.isMatrixId import im.vector.app.core.extensions.isMatrixId
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.extensions.vectorStore import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
@ -91,6 +91,7 @@ class OnboardingViewModel @AssistedInject constructor(
private val vectorOverrides: VectorOverrides, private val vectorOverrides: VectorOverrides,
private val registrationActionHandler: RegistrationActionHandler, private val registrationActionHandler: RegistrationActionHandler,
private val sdkIntProvider: BuildVersionSdkIntProvider, private val sdkIntProvider: BuildVersionSdkIntProvider,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) { ) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -616,7 +617,7 @@ class OnboardingViewModel @AssistedInject constructor(
activeSessionHolder.setActiveSession(session) activeSessionHolder.setActiveSession(session)
authenticationService.reset() authenticationService.reset()
session.configureAndStart(applicationContext) configureAndStartSessionUseCase.execute(session)
when (authenticationDescription) { when (authenticationDescription) {
is AuthenticationDescription.Register -> { is AuthenticationDescription.Register -> {

View file

@ -0,0 +1,105 @@
/*
* 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.core.session
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeWebRtcCallManager
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.sync.FilterService
class ConfigureAndStartSessionUseCaseTest {
private val fakeContext = FakeContext()
private val fakeWebRtcCallManager = FakeWebRtcCallManager()
private val fakeUpdateMatrixClientInfoUseCase = mockk<UpdateMatrixClientInfoUseCase>()
private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase(
context = fakeContext.instance,
webRtcCallManager = fakeWebRtcCallManager.instance,
updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase,
)
@Before
fun setup() {
mockkStatic("im.vector.app.core.extensions.SessionKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a session and start sync needed when configuring and starting the session then it should be configured properly`() = runTest {
// Given
val fakeSession = givenASession()
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
// When
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true)
// Then
verify { fakeSession.startSyncing(fakeContext.instance) }
fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
fakeSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
}
@Test
fun `given a session and no start sync needed when configuring and starting the session then it should be configured properly`() = runTest {
// Given
val fakeSession = givenASession()
fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds()
coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) }
// When
configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false)
// Then
verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) }
fakeSession.fakeFilterService.verifySetFilter(FilterService.FilterPreset.ElementFilter)
fakeSession.fakePushersService.verifyRefreshPushers()
fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded()
coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) }
}
private fun givenASession(): FakeSession {
val fakeSession = FakeSession()
every { fakeSession.open() } just runs
fakeSession.fakeFilterService.givenSetFilterSucceeds()
every { fakeSession.startSyncing(any()) } just runs
fakeSession.fakePushersService.givenRefreshPushersSucceeds()
return fakeSession
}
}

View file

@ -0,0 +1,62 @@
/*
* 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.core.session.clientinfo
import im.vector.app.test.fakes.FakeSession
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private const val A_DEVICE_ID = "device-id"
private const val A_CLIENT_NAME = "client-name"
private const val A_CLIENT_VERSION = "client-version"
private const val A_CLIENT_URL = "client-url"
class GetMatrixClientInfoUseCaseTest {
private val fakeSession = FakeSession()
private val getMatrixClientInfoUseCase = GetMatrixClientInfoUseCase()
@Test
fun `given a device id and existing content when getting the info then result should contain that info`() {
// Given
givenClientInfoContent(A_DEVICE_ID)
val expectedClientInfo = MatrixClientInfoContent(
name = A_CLIENT_NAME,
version = A_CLIENT_VERSION,
url = A_CLIENT_URL,
)
// When
val result = getMatrixClientInfoUseCase.execute(fakeSession, A_DEVICE_ID)
// Then
result shouldBeEqualTo expectedClientInfo
}
private fun givenClientInfoContent(deviceId: String) {
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + deviceId
val content = mapOf(
Pair("name", A_CLIENT_NAME),
Pair("version", A_CLIENT_VERSION),
Pair("url", A_CLIENT_URL),
)
fakeSession
.fakeSessionAccountDataService
.givenGetUserAccountDataEventReturns(type, content)
}
}

View file

@ -0,0 +1,106 @@
/*
* 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.core.session.clientinfo
import im.vector.app.test.fakes.FakeSession
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.toContent
private const val A_DEVICE_ID = "device-id"
class SetMatrixClientInfoUseCaseTest {
private val fakeSession = FakeSession()
private val setMatrixClientInfoUseCase = SetMatrixClientInfoUseCase()
@Test
fun `given client info and no error when setting the info then account data is correctly updated`() = runTest {
// Given
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID
val clientInfo = givenClientInfo()
val content = clientInfo.toContent()
fakeSession
.givenSessionId(A_DEVICE_ID)
fakeSession
.fakeSessionAccountDataService
.givenUpdateUserAccountDataEventSucceeds()
// When
val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo)
// Then
result.isSuccess shouldBe true
fakeSession
.fakeSessionAccountDataService
.verifyUpdateUserAccountDataEventSucceeds(type, content)
}
@Test
fun `given client info and error during update when setting the info then result is failure`() = runTest {
// Given
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID
val clientInfo = givenClientInfo()
val content = clientInfo.toContent()
fakeSession
.givenSessionId(A_DEVICE_ID)
val error = Exception()
fakeSession
.fakeSessionAccountDataService
.givenUpdateUserAccountDataEventFailsWithError(error)
// When
val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
fakeSession
.fakeSessionAccountDataService
.verifyUpdateUserAccountDataEventSucceeds(type, content)
}
@Test
fun `given client info and null device id when setting the info then result is failure`() = runTest {
// Given
val type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID
val clientInfo = givenClientInfo()
val content = clientInfo.toContent()
fakeSession
.givenSessionId(null)
// When
val result = setMatrixClientInfoUseCase.execute(fakeSession, clientInfo)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeInstanceOf NoDeviceIdError::class
fakeSession
.fakeSessionAccountDataService
.verifyUpdateUserAccountDataEventSucceeds(type, content, inverse = true)
}
private fun givenClientInfo() = MatrixClientInfoContent(
name = "name",
version = "version",
url = null,
)
}

View file

@ -0,0 +1,149 @@
/*
* 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.core.session.clientinfo
import im.vector.app.core.resources.BuildMeta
import im.vector.app.test.fakes.FakeAppNameProvider
import im.vector.app.test.fakes.FakeSession
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.Test
private const val AN_APP_NAME_1 = "app_name_1"
private const val AN_APP_NAME_2 = "app_name_2"
private const val A_VERSION_NAME_1 = "version_name_1"
private const val A_VERSION_NAME_2 = "version_name_2"
private const val A_SESSION_ID = "session-id"
class UpdateMatrixClientInfoUseCaseTest {
private val fakeSession = FakeSession()
private val fakeAppNameProvider = FakeAppNameProvider()
private val fakeBuildMeta = mockk<BuildMeta>()
private val getMatrixClientInfoUseCase = mockk<GetMatrixClientInfoUseCase>()
private val setMatrixClientInfoUseCase = mockk<SetMatrixClientInfoUseCase>()
private val updateMatrixClientInfoUseCase = UpdateMatrixClientInfoUseCase(
appNameProvider = fakeAppNameProvider,
buildMeta = fakeBuildMeta,
getMatrixClientInfoUseCase = getMatrixClientInfoUseCase,
setMatrixClientInfoUseCase = setMatrixClientInfoUseCase,
)
@Test
fun `given current client info is different than the stored one when trying to update then new client info is set`() = runTest {
// Given
givenCurrentAppName(AN_APP_NAME_1)
givenCurrentVersionName(A_VERSION_NAME_1)
givenStoredClientInfo(AN_APP_NAME_2, A_VERSION_NAME_2)
givenSetClientInfoSucceeds()
val expectedClientInfoToSet = MatrixClientInfoContent(
name = AN_APP_NAME_1,
version = A_VERSION_NAME_1,
)
// When
val result = updateMatrixClientInfoUseCase.execute(fakeSession)
// Then
result.isSuccess shouldBe true
coVerify { setMatrixClientInfoUseCase.execute(fakeSession, match { it == expectedClientInfoToSet }) }
}
@Test
fun `given error during set of new client info when trying to update then result is failure`() = runTest {
// Given
givenCurrentAppName(AN_APP_NAME_1)
givenCurrentVersionName(A_VERSION_NAME_1)
givenStoredClientInfo(AN_APP_NAME_2, A_VERSION_NAME_2)
val error = Exception()
givenSetClientInfoFails(error)
val expectedClientInfoToSet = MatrixClientInfoContent(
name = AN_APP_NAME_1,
version = A_VERSION_NAME_1,
)
// When
val result = updateMatrixClientInfoUseCase.execute(fakeSession)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeEqualTo error
coVerify { setMatrixClientInfoUseCase.execute(fakeSession, match { it == expectedClientInfoToSet }) }
}
@Test
fun `given current client info is equal to the stored one when trying to update then nothing is done`() = runTest {
// Given
givenCurrentAppName(AN_APP_NAME_1)
givenCurrentVersionName(A_VERSION_NAME_1)
givenStoredClientInfo(AN_APP_NAME_1, A_VERSION_NAME_1)
// When
val result = updateMatrixClientInfoUseCase.execute(fakeSession)
// Then
result.isSuccess shouldBe true
coVerify(inverse = true) { setMatrixClientInfoUseCase.execute(fakeSession, any()) }
}
@Test
fun `given no session id for current session when trying to update then nothing is done`() = runTest {
// Given
givenCurrentAppName(AN_APP_NAME_1)
givenCurrentVersionName(A_VERSION_NAME_1)
fakeSession.givenSessionId(null)
// When
val result = updateMatrixClientInfoUseCase.execute(fakeSession)
// Then
result.isFailure shouldBe true
result.exceptionOrNull() shouldBeInstanceOf NoDeviceIdError::class
coVerify(inverse = true) { setMatrixClientInfoUseCase.execute(fakeSession, any()) }
}
private fun givenCurrentAppName(appName: String) {
fakeAppNameProvider.givenAppName(appName)
}
private fun givenCurrentVersionName(versionName: String) {
every { fakeBuildMeta.versionName } returns versionName
}
private fun givenStoredClientInfo(appName: String, versionName: String) {
fakeSession.givenSessionId(A_SESSION_ID)
every { getMatrixClientInfoUseCase.execute(fakeSession, A_SESSION_ID) } returns MatrixClientInfoContent(
name = appName,
version = versionName,
)
}
private fun givenSetClientInfoSucceeds() {
coEvery { setMatrixClientInfoUseCase.execute(any(), any()) } returns Result.success(Unit)
}
private fun givenSetClientInfoFails(error: Throwable) {
coEvery { setMatrixClientInfoUseCase.execute(any(), any()) } returns Result.failure(error)
}
}

View file

@ -20,6 +20,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import com.airbnb.mvrx.test.MavericksTestRule import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
@ -50,6 +51,7 @@ import im.vector.app.test.fixtures.a401ServerError
import im.vector.app.test.fixtures.aHomeServerCapabilities import im.vector.app.test.fixtures.aHomeServerCapabilities
import im.vector.app.test.fixtures.anUnrecognisedCertificateError import im.vector.app.test.fixtures.anUnrecognisedCertificateError
import im.vector.app.test.test import im.vector.app.test.test
import io.mockk.mockk
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Before import org.junit.Before
@ -111,6 +113,7 @@ class OnboardingViewModelTest {
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase() private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService() private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
private val fakeLoginWizard = FakeLoginWizard() private val fakeLoginWizard = FakeLoginWizard()
private val fakeConfigureAndStartSessionUseCase = mockk<ConfigureAndStartSessionUseCase>()
private var initialState = OnboardingViewState() private var initialState = OnboardingViewState()
private lateinit var viewModel: OnboardingViewModel private lateinit var viewModel: OnboardingViewModel
@ -1093,6 +1096,7 @@ class OnboardingViewModelTest {
FakeVectorOverrides(), FakeVectorOverrides(),
fakeRegistrationActionHandler.instance, fakeRegistrationActionHandler.instance,
TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O }, TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O },
fakeConfigureAndStartSessionUseCase,
).also { ).also {
viewModel = it viewModel = it
initialState = state initialState = state
@ -1132,7 +1136,7 @@ class OnboardingViewModelTest {
private fun givenInitialisesSession(session: Session) { private fun givenInitialisesSession(session: Session) {
fakeActiveSessionHolder.expectSetsActiveSession(session) fakeActiveSessionHolder.expectSetsActiveSession(session)
fakeAuthenticationService.expectReset() fakeAuthenticationService.expectReset()
fakeSession.expectStartsSyncing() fakeSession.expectStartsSyncing(fakeConfigureAndStartSessionUseCase)
} }
private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationActionHandler.Result) { private fun givenRegistrationResultFor(action: RegisterAction, result: RegistrationActionHandler.Result) {

View file

@ -0,0 +1,35 @@
/*
* 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.test.fakes
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import org.matrix.android.sdk.api.session.sync.FilterService
class FakeFilterService : FilterService by mockk() {
fun givenSetFilterSucceeds() {
every { setFilter(any()) } just runs
}
fun verifySetFilter(filterPreset: FilterService.FilterPreset) {
verify { setFilter(filterPreset) }
}
}

View file

@ -20,6 +20,7 @@ import androidx.lifecycle.liveData
import io.mockk.Ordering import io.mockk.Ordering
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import io.mockk.verify import io.mockk.verify
@ -56,4 +57,12 @@ class FakePushersService : PushersService by mockk(relaxed = true) {
verify { enqueueAddHttpPusher(capture(httpPusherSlot)) } verify { enqueueAddHttpPusher(capture(httpPusherSlot)) }
return httpPusherSlot.captured return httpPusherSlot.captured
} }
fun givenRefreshPushersSucceeds() {
justRun { refreshPushers() }
}
fun verifyRefreshPushers() {
verify { refreshPushers() }
}
} }

View file

@ -16,9 +16,9 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.extensions.vectorStore import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.session.VectorSessionStore import im.vector.app.features.session.VectorSessionStore
import im.vector.app.test.testCoroutineDispatchers import im.vector.app.test.testCoroutineDispatchers
import io.mockk.coEvery import io.mockk.coEvery
@ -43,6 +43,8 @@ class FakeSession(
val fakeRoomService: FakeRoomService = FakeRoomService(), val fakeRoomService: FakeRoomService = FakeRoomService(),
val fakePushersService: FakePushersService = FakePushersService(), val fakePushersService: FakePushersService = FakePushersService(),
private val fakeEventService: FakeEventService = FakeEventService(), private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService(),
val fakeFilterService: FakeFilterService = FakeFilterService(),
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {
init { init {
@ -60,6 +62,8 @@ class FakeSession(
override fun roomService() = fakeRoomService override fun roomService() = fakeRoomService
override fun eventService() = fakeEventService override fun eventService() = fakeEventService
override fun pushersService() = fakePushersService override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService
override fun filterService() = fakeFilterService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) { fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery { coEvery {
@ -69,9 +73,9 @@ class FakeSession(
} }
} }
fun expectStartsSyncing() { fun expectStartsSyncing(configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase) {
coJustRun { coJustRun {
this@FakeSession.configureAndStart(any(), startSyncing = true) configureAndStartSessionUseCase.execute(this@FakeSession, startSyncing = true)
this@FakeSession.startSyncing(any()) this@FakeSession.startSyncing(any())
} }
} }
@ -80,7 +84,7 @@ class FakeSession(
every { this@FakeSession.sessionParams } returns sessionParams every { this@FakeSession.sessionParams } returns sessionParams
} }
fun givenSessionId(sessionId: String): SessionParams { fun givenSessionId(sessionId: String?): SessionParams {
val sessionParams = mockk<SessionParams>() val sessionParams = mockk<SessionParams>()
every { sessionParams.deviceId } returns sessionId every { sessionParams.deviceId } returns sessionId
givenSessionParams(sessionParams) givenSessionParams(sessionParams)

View file

@ -0,0 +1,46 @@
/*
* 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.test.fakes
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.events.model.Content
class FakeSessionAccountDataService : SessionAccountDataService by mockk() {
fun givenGetUserAccountDataEventReturns(type: String, content: Content) {
every { getUserAccountDataEvent(type) } returns UserAccountDataEvent(type, content)
}
fun givenUpdateUserAccountDataEventSucceeds() {
coEvery { updateUserAccountData(any(), any()) } just runs
}
fun givenUpdateUserAccountDataEventFailsWithError(error: Exception) {
coEvery { updateUserAccountData(any(), any()) } throws error
}
fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) {
coVerify(inverse = inverse) { updateUserAccountData(type, content) }
}
}

View file

@ -0,0 +1,37 @@
/*
* 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.test.fakes
import im.vector.app.features.call.webrtc.WebRtcCallManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
class FakeWebRtcCallManager {
val instance = mockk<WebRtcCallManager>()
fun givenCheckForProtocolsSupportIfNeededSucceeds() {
every { instance.checkForProtocolsSupportIfNeeded() } just runs
}
fun verifyCheckForProtocolsSupportIfNeeded() {
verify { instance.checkForProtocolsSupportIfNeeded() }
}
}