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) {
val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content)
awaitCallback<Unit> { callback ->
awaitCallback { callback ->
updateUserAccountDataTask.configureWith(params) {
this.retryCount = 5 // TODO Need to refactor retrying out into a helper method.
this.callback = callback

View file

@ -19,10 +19,10 @@ package im.vector.app.core.di
import android.content.Context
import arrow.core.Option
import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper
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.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
@ -50,6 +50,7 @@ class ActiveSessionHolder @Inject constructor(
private val sessionInitializer: SessionInitializer,
private val applicationContext: Context,
private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
@ -109,7 +110,9 @@ class ActiveSessionHolder @Inject constructor(
}
?: sessionInitializer.tryInitialize(readCurrentSession = { activeSessionReference.get() }) { 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 org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.sync.FilterService
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) {
val applicationContext = context.applicationContext
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.MavericksAssistedViewModelFactory
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.resources.StringProvider
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.core.utils.ensureTrailingSlash
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -64,7 +64,8 @@ class LoginViewModel @AssistedInject constructor(
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider,
private val homeServerHistoryService: HomeServerHistoryService
private val homeServerHistoryService: HomeServerHistoryService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
@AssistedFactory
@ -732,7 +733,7 @@ class LoginViewModel @AssistedInject constructor(
activeSessionHolder.setActiveSession(session)
authenticationService.reset()
session.configureAndStart(applicationContext)
configureAndStartSessionUseCase.execute(session)
setState {
copy(
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.hiltMavericksViewModelFactory
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.isMatrixId
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel
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.ensureTrailingSlash
import im.vector.app.features.VectorFeatures
@ -91,6 +91,7 @@ class OnboardingViewModel @AssistedInject constructor(
private val vectorOverrides: VectorOverrides,
private val registrationActionHandler: RegistrationActionHandler,
private val sdkIntProvider: BuildVersionSdkIntProvider,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel<OnboardingViewState, OnboardingAction, OnboardingViewEvents>(initialState) {
@AssistedFactory
@ -616,7 +617,7 @@ class OnboardingViewModel @AssistedInject constructor(
activeSessionHolder.setActiveSession(session)
authenticationService.reset()
session.configureAndStart(applicationContext)
configureAndStartSessionUseCase.execute(session)
when (authenticationDescription) {
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 com.airbnb.mvrx.test.MavericksTestRule
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.LoginMode
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.anUnrecognisedCertificateError
import im.vector.app.test.test
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Before
@ -111,6 +113,7 @@ class OnboardingViewModelTest {
private val fakeStartAuthenticationFlowUseCase = FakeStartAuthenticationFlowUseCase()
private val fakeHomeServerHistoryService = FakeHomeServerHistoryService()
private val fakeLoginWizard = FakeLoginWizard()
private val fakeConfigureAndStartSessionUseCase = mockk<ConfigureAndStartSessionUseCase>()
private var initialState = OnboardingViewState()
private lateinit var viewModel: OnboardingViewModel
@ -1093,6 +1096,7 @@ class OnboardingViewModelTest {
FakeVectorOverrides(),
fakeRegistrationActionHandler.instance,
TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.O },
fakeConfigureAndStartSessionUseCase,
).also {
viewModel = it
initialState = state
@ -1132,7 +1136,7 @@ class OnboardingViewModelTest {
private fun givenInitialisesSession(session: Session) {
fakeActiveSessionHolder.expectSetsActiveSession(session)
fakeAuthenticationService.expectReset()
fakeSession.expectStartsSyncing()
fakeSession.expectStartsSyncing(fakeConfigureAndStartSessionUseCase)
}
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.coVerify
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
@ -56,4 +57,12 @@ class FakePushersService : PushersService by mockk(relaxed = true) {
verify { enqueueAddHttpPusher(capture(httpPusherSlot)) }
return httpPusherSlot.captured
}
fun givenRefreshPushersSucceeds() {
justRun { refreshPushers() }
}
fun verifyRefreshPushers() {
verify { refreshPushers() }
}
}

View file

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