making a start on testing the android side of the project

This commit is contained in:
Adam Brown 2022-03-05 18:53:59 +00:00
parent b848ee97d2
commit 1f96bfbc0f
52 changed files with 835 additions and 146 deletions

View File

@ -115,6 +115,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
imageLoaderModule,
context,
buildMeta,
coroutineDispatchers,
)
}
@ -126,6 +127,7 @@ internal class FeatureModules internal constructor(
imageLoaderModule: ImageLoaderModule,
context: Context,
buildMeta: BuildMeta,
coroutineDispatchers: CoroutineDispatchers,
) {
val directoryModule by unsafeLazy {
@ -156,7 +158,16 @@ internal class FeatureModules internal constructor(
)
}
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile) }
val settingsModule by unsafeLazy { SettingsModule(storeModule.value, matrixModules.crypto, matrixModules.sync, context.contentResolver, buildMeta) }
val settingsModule by unsafeLazy {
SettingsModule(
storeModule.value,
matrixModules.crypto,
matrixModules.sync,
context.contentResolver,
buildMeta,
coroutineDispatchers
)
}
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
val notificationsModule by unsafeLazy {
NotificationsModule(
@ -195,7 +206,7 @@ internal class MatrixModules(
installEncryptionService(store.knownDevicesStore())
val olmAccountStore = OlmPersistenceWrapper(store.olmStore())
val singletonFlows = SingletonFlows()
val singletonFlows = SingletonFlows(coroutineDispatchers)
val olm = OlmWrapper(
olmStore = olmAccountStore,
singletonFlows = singletonFlows,

View File

@ -136,6 +136,12 @@ ext.kotlinFixtures = { dependencies ->
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
}
ext.androidImportFixturesWorkaround = { project, fixtures ->
project.dependencies.testImplementation(project.dependencies.testFixtures(fixtures))
project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}-test-fixtures.jar")
project.dependencies.testImplementation fixtures.files("build/libs/${fixtures.name}.jar")
}
if (launchTask.contains("codeCoverageReport".toLowerCase())) {
apply from: 'tools/coverage.gradle'
}

View File

@ -1,7 +1,7 @@
package app.dapk.st.core
sealed interface Lce<T> {
class Loading<T> : Lce<T>
data class Loading<T>(val ignored: Unit = Unit) : Lce<T>
data class Error<T>(val cause: Throwable) : Lce<T>
data class Content<T>(val value: T) : Lce<T>
}

View File

@ -1,18 +1,17 @@
package app.dapk.st.core
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
class SingletonFlows {
class SingletonFlows(
private val coroutineDispatchers: CoroutineDispatchers
) {
private val mutex = Mutex()
private val cache = mutableMapOf<String, MutableSharedFlow<*>>()
private val started = ConcurrentHashMap<String, Boolean?>()
@Suppress("unchecked_cast")
suspend fun <T> getOrPut(key: String, onStart: suspend () -> T): Flow<T> {
@ -20,7 +19,7 @@ class SingletonFlows {
null -> mutex.withLock {
cache.getOrPut(key) {
MutableSharedFlow<T>(replay = 1).also {
withContext(Dispatchers.IO) {
coroutineDispatchers.withIoContext {
async {
it.emit(onStart())
}
@ -39,9 +38,4 @@ class SingletonFlows {
suspend fun <T> update(key: String, value: T) {
(cache[key] as? MutableSharedFlow<T>)?.emit(value)
}
fun remove(key: String) {
cache.remove(key)
}
}

View File

@ -6,19 +6,24 @@ import io.mockk.coJustRun
import io.mockk.coVerifyAll
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
import kotlin.coroutines.CoroutineContext
fun runExpectTest(testBody: suspend ExpectTestScope.() -> Unit) {
val expects = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
runTest {
testBody(object : ExpectTestScope {
override val coroutineContext = this@runTest.coroutineContext
override fun verifyExpects() = coVerifyAll { expects.forEach { it.invoke(this@coVerifyAll) } }
override fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit) {
coJustRun { block(this@expectUnit) }.ignore()
expects.add { block(this@expectUnit) }
}
})
runTest { testBody(ExpectTest(coroutineContext)) }
}
class ExpectTest(override val coroutineContext: CoroutineContext) : ExpectTestScope {
private val expects = mutableListOf<suspend MockKVerificationScope.() -> Unit>()
override fun verifyExpects() = coVerifyAll { expects.forEach { it.invoke(this@coVerifyAll) } }
override fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit) {
coJustRun { block(this@expectUnit) }.ignore()
expects.add { block(this@expectUnit) }
}
}
private fun Any.ignore() = Unit

View File

@ -6,10 +6,22 @@ inline fun <T : Any, reified R> T.expect(crossinline block: suspend MockKMatcher
coEvery { block(this@expect) } returns mockk(relaxed = true)
}
fun <T, B> MockKStubScope<T, B>.delegateReturn(): Returns<T> = Returns { value ->
answers(ConstantAnswer(value))
fun <T, B> MockKStubScope<T, B>.delegateReturn() = object : Returns<T> {
override fun returns(value: T) {
answers(ConstantAnswer(value))
}
override fun throws(value: Throwable) {
this@delegateReturn.throws(value)
}
}
fun interface Returns<T> {
fun returns(value: T)
fun <T> returns(block: (T) -> Unit) = object : Returns<T> {
override fun returns(value: T) = block(value)
override fun throws(value: Throwable) = throw value
}
interface Returns<T> {
fun returns(value: T)
fun throws(value: Throwable)
}

View File

@ -0,0 +1 @@
plugins { id 'kotlin' }

View File

@ -0,0 +1,23 @@
@file:JvmName("SnapshotStateKt")
@file:JvmMultifileClass
package androidx.compose.runtime
import kotlin.reflect.KProperty
interface State<out T> {
val value: T
}
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
operator fun <T> State<T>.getValue(thisObj: Any?, property: KProperty<*>): T = throw RuntimeException("stub")
operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
throw RuntimeException("stub")
}
fun <T> mutableStateOf(value: T): MutableState<T> = throw RuntimeException("stub")

View File

@ -0,0 +1,19 @@
package androidx.lifecycle
abstract class ViewModel {
protected open fun onCleared() {}
fun clear() {
throw RuntimeException("stub")
}
fun <T> setTagIfAbsent(key: String, newValue: T): T {
throw RuntimeException("stub")
}
fun <T> getTag(key: String): T? {
throw RuntimeException("stub")
}
}

View File

@ -0,0 +1,15 @@
plugins {
id 'kotlin'
id 'java-test-fixtures'
}
dependencies {
compileOnly project(":domains:android:viewmodel-stub")
implementation Dependencies.mavenCentral.kotlinCoroutinesCore
kotlinFixtures(it)
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
testFixturesImplementation testFixtures(project(":core"))
testFixturesCompileOnly project(":domains:android:viewmodel-stub")
}

View File

@ -1,5 +1,6 @@
package app.dapk.st.core
package app.dapk.st.viewmodel
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -7,16 +8,21 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
abstract class DapkViewModel<S, VE>(
initialState: S
) : ViewModel() {
typealias MutableStateFactory <S> = (S) -> MutableState<S>
fun <S> defaultStateFactory(): MutableStateFactory<S> = { mutableStateOf(it) }
@Suppress("PropertyName")
abstract class DapkViewModel<S, VE>(initialState: S, factory: MutableStateFactory<S> = defaultStateFactory()) : ViewModel() {
protected val _events = MutableSharedFlow<VE>(extraBufferCapacity = 1)
val events: SharedFlow<VE> = _events
var state by mutableStateOf<S>(initialState)
var state by factory(initialState)
protected set
fun updateState(reducer: S.() -> S) {
state = reducer(state)
}
}
}

View File

@ -0,0 +1,17 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.amshove.kluent.internal.assertEquals
class FlowTestObserver<T>(scope: CoroutineScope, flow: Flow<T>) {
private val values = mutableListOf<T>()
private val job: Job = flow
.onEach { values.add(it) }
.launchIn(scope)
fun assertValues(values: List<T>) = assertEquals(values, this.values)
fun finish() = job.cancel()
}

View File

@ -0,0 +1,18 @@
import androidx.compose.runtime.MutableState
class TestMutableState<T>(initialState: T) : MutableState<T> {
private var _value: T = initialState
var onValue: ((T) -> Unit)? = null
override var value: T
get() = _value
set(value) {
_value = value
onValue?.invoke(value)
}
override fun component1(): T = throw RuntimeException("stub")
override fun component2(): (T) -> Unit = throw RuntimeException("stub")
}

View File

@ -0,0 +1,27 @@
import app.dapk.st.viewmodel.MutableStateFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import test.ExpectTest
class ViewModelTest {
var instance: TestMutableState<Any>? = null
fun <S> testMutableStateFactory(): MutableStateFactory<S> {
return { TestMutableState(it).also { instance = it as TestMutableState<Any> } }
}
operator fun invoke(block: suspend ViewModelTestScope.() -> Unit) {
runTest {
val expectTest = ExpectTest(coroutineContext)
val viewModelTest = ViewModelTestScopeImpl(expectTest, this@ViewModelTest)
Dispatchers.setMain(UnconfinedTestDispatcher(testScheduler))
block(viewModelTest)
viewModelTest.finish()
Dispatchers.resetMain()
}
}
}

View File

@ -0,0 +1,65 @@
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.amshove.kluent.internal.assertEquals
import test.ExpectTestScope
@Suppress("UNCHECKED_CAST")
internal class ViewModelTestScopeImpl(
private val expectTestScope: ExpectTestScope,
private val stateProvider: ViewModelTest
) : ExpectTestScope by expectTestScope, ViewModelTestScope {
private val flowObserverScope = CoroutineScope(Dispatchers.Unconfined)
private var viewEvents: FlowTestObserver<*>? = null
private val viewStates = mutableListOf<Any>()
private var capturedInitialState: Any? = null
override fun <S, VE, T : DapkViewModel<S, VE>> T.test(initialState: S?): T {
initialState?.let { stateProvider.instance?.value = it }
capturedInitialState = stateProvider.instance?.value
stateProvider.instance?.onValue = { viewStates.add(it) }
viewEvents = FlowTestObserver(flowObserverScope, this.events)
return this
}
override fun <S> assertStates(states: List<S>) {
assertEquals(states, viewStates)
}
override fun <VE> assertEvents(events: List<VE>) {
(viewEvents as? FlowTestObserver<VE>)?.assertValues(events)
}
override fun <S> assertInitialState(state: S) {
assertEquals(state, capturedInitialState)
}
override fun <S> assertStates(vararg state: (S) -> S) {
val states = state.toList().map { it as (Any) -> Any }.fold(mutableListOf<Any>()) { acc, curr ->
curr.invoke(acc.lastOrNull() ?: capturedInitialState!!).let { acc.add(it) }; acc
}
assertStates(states)
}
fun finish() {
viewStates.clear()
viewEvents?.finish()
}
}
interface ViewModelTestScope : ExpectTestScope {
fun <S, VE, T : DapkViewModel<S, VE>> T.test(initialState: S? = null): T
fun <VE> assertEvents(vararg event: VE) = this.assertEvents(event.toList())
fun <VE> assertNoEvents() = this.assertEvents<VE>(emptyList())
fun <VE> assertEvents(events: List<VE>)
fun <S> assertInitialState(state: S)
fun <S> assertStates(vararg state: S.() -> S)
fun <S> assertStates(vararg state: S) = this.assertStates(state.toList())
fun <S> assertNoStates() = this.assertStates<S>(emptyList())
fun <S> assertStates(states: List<S>)
}

View File

@ -5,6 +5,7 @@ dependencies {
implementation project(":matrix:services:message")
implementation project(":matrix:services:room")
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(":features:messenger")
implementation project(":core")
implementation project(":design-library")

View File

@ -1,9 +1,9 @@
package app.dapk.st.directory
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.directory.DirectoryScreenState.Content
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach

View File

@ -8,6 +8,7 @@ dependencies {
implementation project(":features:settings")
implementation project(":features:profile")
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(':domains:store')
implementation project(":core")
implementation project(":design-library")

View File

@ -1,7 +1,6 @@
package app.dapk.st.home
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.directory.DirectoryViewModel
import app.dapk.st.home.HomeScreenState.*
import app.dapk.st.login.LoginViewModel
@ -9,6 +8,7 @@ import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.isSignedIn
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.profile.ProfileViewModel
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.launch
class HomeViewModel(

View File

@ -3,6 +3,7 @@ applyAndroidLibraryModule(project)
dependencies {
implementation project(":domains:android:core")
implementation project(":domains:android:push")
implementation project(":domains:android:viewmodel")
implementation project(":matrix:services:auth")
implementation project(":matrix:services:profile")
implementation project(":matrix:services:crypto")

View File

@ -1,15 +1,14 @@
package app.dapk.st.login
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.logP
import app.dapk.st.login.LoginEvent.LoginComplete
import app.dapk.st.login.LoginScreenState.*
import app.dapk.st.matrix.auth.AuthService
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.push.RegisterFirebasePushTokenUseCase
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch

View File

@ -6,6 +6,7 @@ dependencies {
implementation project(":matrix:services:message")
implementation project(":matrix:services:room")
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(":core")
implementation project(":features:navigator")
implementation project(":design-library")

View File

@ -1,7 +1,6 @@
package app.dapk.st.messenger
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.matrix.common.CredentialsStore
@ -11,6 +10,7 @@ import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged

View File

@ -6,6 +6,7 @@ dependencies {
implementation project(":features:settings")
implementation project(':domains:store')
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(":design-library")
implementation project(":core")
}

View File

@ -1,10 +1,9 @@
package app.dapk.st.profile
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.sync.SyncService
import kotlinx.coroutines.flow.count
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch

View File

@ -6,6 +6,13 @@ dependencies {
implementation project(":features:navigator")
implementation project(':domains:store')
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(":design-library")
implementation project(":core")
kotlinTest(it)
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
}

View File

@ -0,0 +1,18 @@
package app.dapk.st.settings
import app.dapk.st.core.BuildMeta
internal class SettingsItemFactory(private val buildMeta: BuildMeta) {
fun root() = listOf(
SettingItem.Header("General"),
SettingItem.Text(SettingItem.Id.Encryption, "Encryption"),
SettingItem.Text(SettingItem.Id.EventLog, "Event log"),
SettingItem.Header("Account"),
SettingItem.Text(SettingItem.Id.SignOut, "Sign out"),
SettingItem.Header("About"),
SettingItem.Text(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
SettingItem.Text(SettingItem.Id.Ignored, "Version", buildMeta.versionName),
)
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.settings
import android.content.ContentResolver
import app.dapk.st.core.BuildMeta
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.ProvidableModule
import app.dapk.st.domain.StoreModule
import app.dapk.st.matrix.crypto.CryptoService
@ -14,19 +15,20 @@ class SettingsModule(
private val syncService: SyncService,
private val contentResolver: ContentResolver,
private val buildMeta: BuildMeta,
private val coroutineDispatchers: CoroutineDispatchers,
) : ProvidableModule {
fun settingsViewModel() = SettingsViewModel(
internal fun settingsViewModel() = SettingsViewModel(
storeModule.credentialsStore(),
storeModule.cacheCleaner(),
contentResolver,
cryptoService,
syncService,
UriFilenameResolver(contentResolver),
buildMeta
UriFilenameResolver(contentResolver, coroutineDispatchers),
SettingsItemFactory(buildMeta),
)
fun eventLogViewModel(): EventLoggerViewModel {
internal fun eventLogViewModel(): EventLoggerViewModel {
return EventLoggerViewModel(storeModule.eventLogStore())
}
}

View File

@ -46,7 +46,7 @@ import app.dapk.st.settings.SettingsEvent.*
import app.dapk.st.settings.eventlogger.EventLogActivity
@Composable
fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, navigator: Navigator) {
internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, navigator: Navigator) {
viewModel.ObserveEvents(onSignOut)
LaunchedEffect(true) {
viewModel.start()

View File

@ -14,7 +14,7 @@ sealed interface Page {
object Security : Page
data class ImportRoomKey(
val selectedFile: NamedUri? = null,
val importProgress: Lce<Boolean>? = null,
val importProgress: Lce<Unit>? = null,
) : Page
object Routes {

View File

@ -3,8 +3,6 @@ package app.dapk.st.settings
import android.content.ContentResolver
import android.net.Uri
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.BuildMeta
import app.dapk.st.core.DapkViewModel
import app.dapk.st.core.Lce
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner
@ -13,38 +11,33 @@ import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.settings.SettingItem.Id.*
import app.dapk.st.settings.SettingsEvent.*
import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory
import kotlinx.coroutines.launch
class SettingsViewModel(
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
internal class SettingsViewModel(
private val credentialsStore: CredentialsStore,
private val cacheCleaner: StoreCleaner,
private val contentResolver: ContentResolver,
private val cryptoService: CryptoService,
private val syncService: SyncService,
private val uriFilenameResolver: UriFilenameResolver,
private val buildMeta: BuildMeta,
private val settingsItemFactory: SettingsItemFactory,
factory: MutableStateFactory<SettingsScreenState> = defaultStateFactory(),
) : DapkViewModel<SettingsScreenState, SettingsEvent>(
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading())))
initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))),
factory = factory,
) {
fun start() {
viewModelScope.launch {
val root = Page.Root(
Lce.Content(
listOf(
SettingItem.Header("General"),
SettingItem.Text(Encryption, "Encryption"),
SettingItem.Text(EventLog, "Event log"),
SettingItem.Header("Account"),
SettingItem.Text(SignOut, "Sign out"),
SettingItem.Header("About"),
SettingItem.Text(PrivacyPolicy, "Privacy policy"),
SettingItem.Text(Ignored, "Version", buildMeta.versionName),
)
)
)
val root = Page.Root(Lce.Content(settingsItemFactory.root()))
val rootPage = SpiderPage(Page.Routes.root, "Settings", null, root)
updateState { copy(page = rootPage) }
println("state updated")
}
}
@ -55,28 +48,39 @@ class SettingsViewModel(
fun onClick(item: SettingItem) {
when (item.id) {
SignOut -> {
viewModelScope.launch { credentialsStore.clear() }
_events.tryEmit(SignedOut)
viewModelScope.launch {
credentialsStore.clear()
_events.emit(SignedOut)
println("emitted")
}
}
AccessToken -> {
require(item is SettingItem.AccessToken)
_events.tryEmit(CopyToClipboard("Token copied", item.accessToken))
viewModelScope.launch {
require(item is SettingItem.AccessToken)
_events.emit(CopyToClipboard("Token copied", item.accessToken))
}
}
ClearCache -> {
viewModelScope.launch {
cacheCleaner.cleanCache(removeCredentials = false)
_events.tryEmit(Toast(message = "Cache deleted"))
_events.emit(Toast(message = "Cache deleted"))
}
}
EventLog -> {
_events.tryEmit(OpenEventLog)
viewModelScope.launch {
_events.emit(OpenEventLog)
}
}
Encryption -> {
updateState {
copy(page = SpiderPage(Page.Routes.encryption, "Encryption", Page.Routes.root, Page.Security))
}
}
PrivacyPolicy -> _events.tryEmit(OpenUrl("https://ouchadam.github.io/small-talk/privacy/"))
PrivacyPolicy -> {
viewModelScope.launch {
_events.emit(OpenUrl(PRIVACY_POLICY_URL))
}
}
Ignored -> {
// do nothing
}
@ -92,24 +96,24 @@ class SettingsViewModel(
roomsToRefresh?.let { syncService.forceManualRefresh(roomsToRefresh) }
}
}.fold(
onSuccess = { updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Content(true)) } },
onSuccess = { updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Content(Unit)) } },
onFailure = { updatePageState<Page.ImportRoomKey> { copy(importProgress = Lce.Error(it)) } }
)
}
}
fun goToImportRoom() {
updateState {
copy(page = SpiderPage(Page.Routes.importRoomKeys, "Import room keys", Page.Routes.encryption, Page.ImportRoomKey()))
}
goTo(SpiderPage(Page.Routes.importRoomKeys, "Import room keys", Page.Routes.encryption, Page.ImportRoomKey()))
}
fun fileSelected(file: Uri) {
val namedFile = NamedUri(
name = uriFilenameResolver.readFilenameFromUri(file),
uri = file
)
updatePageState<Page.ImportRoomKey> { copy(selectedFile = namedFile) }
viewModelScope.launch {
val namedFile = NamedUri(
name = uriFilenameResolver.readFilenameFromUri(file),
uri = file
)
updatePageState<Page.ImportRoomKey> { copy(selectedFile = namedFile) }
}
}
@Suppress("UNCHECKED_CAST")

View File

@ -3,10 +3,15 @@ package app.dapk.st.settings
import android.content.ContentResolver
import android.net.Uri
import android.provider.OpenableColumns
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.withIoContext
class UriFilenameResolver(private val contentResolver: ContentResolver) {
class UriFilenameResolver(
private val contentResolver: ContentResolver,
private val coroutineDispatchers: CoroutineDispatchers
) {
fun readFilenameFromUri(uri: Uri): String {
suspend fun readFilenameFromUri(uri: Uri): String {
val fallback = uri.path?.substringAfterLast('/') ?: throw IllegalStateException("expecting a file uri but got $uri")
return when (uri.scheme) {
"content" -> readResolvedDisplayName(uri) ?: fallback
@ -14,15 +19,17 @@ class UriFilenameResolver(private val contentResolver: ContentResolver) {
}
}
private fun readResolvedDisplayName(uri: Uri): String? {
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
when {
cursor.moveToFirst() -> {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
.takeIf { it != -1 }
?.let { cursor.getString(it) }
private suspend fun readResolvedDisplayName(uri: Uri): String? {
return coroutineDispatchers.withIoContext {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
when {
cursor.moveToFirst() -> {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
.takeIf { it != -1 }
?.let { cursor.getString(it) }
}
else -> null
}
else -> null
}
}
}

View File

@ -1,9 +1,9 @@
package app.dapk.st.settings.eventlogger
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.core.Lce
import app.dapk.st.domain.eventlog.EventLogPersistence
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach

View File

@ -0,0 +1,27 @@
package app.dapk.st.settings
import android.database.Cursor
import io.mockk.every
import io.mockk.mockk
class FakeCursor {
val instance = mockk<Cursor>()
init {
every { instance.close() } answers {}
}
fun givenEmpty() {
every { instance.count } returns 0
every { instance.moveToFirst() } returns false
}
fun givenString(columnName: String, content: String?) {
val columnId = columnName.hashCode()
every { instance.moveToFirst() } returns true
every { instance.isNull(columnId) } returns (content == null)
every { instance.getColumnIndex(columnName) } returns columnId
every { instance.getString(columnId) } returns content
}
}

View File

@ -0,0 +1,28 @@
package app.dapk.st.settings
import app.dapk.st.core.BuildMeta
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
class SettingsItemFactoryTest {
private val buildMeta = BuildMeta(versionName = "a-version-name")
private val settingsItemFactory = SettingsItemFactory(buildMeta)
@Test
fun `when creating root items, then is expected`() {
val result = settingsItemFactory.root()
result shouldBeEqualTo listOf(
aSettingHeaderItem("General"),
aSettingTextItem(SettingItem.Id.Encryption, "Encryption"),
aSettingTextItem(SettingItem.Id.EventLog, "Event log"),
aSettingHeaderItem("Account"),
aSettingTextItem(SettingItem.Id.SignOut, "Sign out"),
aSettingHeaderItem("About"),
aSettingTextItem(SettingItem.Id.PrivacyPolicy, "Privacy policy"),
aSettingTextItem(SettingItem.Id.Ignored, "Version", buildMeta.versionName),
)
}
}

View File

@ -0,0 +1,273 @@
package app.dapk.st.settings
import ViewModelTest
import android.content.ContentResolver
import android.database.Cursor
import android.net.Uri
import app.dapk.st.core.Lce
import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.sync.SyncService
import fake.FakeCredentialsStore
import fixture.aRoomId
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
import test.delegateReturn
import java.io.InputStream
private const val APP_PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
private val A_LIST_OF_ROOT_ITEMS = listOf(aSettingTextItem())
private val A_URI = FakeUri()
private const val A_FILENAME = "a-filename.jpg"
private val AN_INITIAL_IMPORT_ROOM_KEYS_PAGE = aImportRoomKeysPage()
private val A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION = aImportRoomKeysPage(
state = Page.ImportRoomKey(selectedFile = NamedUri(A_FILENAME, A_URI.instance))
)
private val A_LIST_OF_ROOM_IDS = listOf(aRoomId())
private val AN_INPUT_STREAM = FakeInputStream()
private const val A_PASSPHRASE = "passphrase"
private val AN_ERROR = RuntimeException()
internal class SettingsViewModelTest {
private val runViewModelTest = ViewModelTest()
private val fakeCredentialsStore = FakeCredentialsStore()
private val fakeStoreCleaner = FakeStoreCleaner()
private val fakeContentResolver = FakeContentResolver()
private val fakeCryptoService = FakeCryptoService()
private val fakeSyncService = FakeSyncService()
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
private val viewModel = SettingsViewModel(
fakeCredentialsStore,
fakeStoreCleaner,
fakeContentResolver.instance,
fakeCryptoService,
fakeSyncService,
fakeUriFilenameResolver.instance,
fakeSettingsItemFactory.instance,
runViewModelTest.testMutableStateFactory(),
)
@Test
fun `when creating view model then initial state is loading Root`() = runViewModelTest {
viewModel.test()
assertInitialState(
SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading())))
)
}
@Test
fun `when starting, then emits root page with content`() = runViewModelTest {
fakeSettingsItemFactory.givenRoot().returns(A_LIST_OF_ROOT_ITEMS)
viewModel.test().start()
assertStates(
SettingsScreenState(
SpiderPage(
Page.Routes.root,
"Settings",
null,
Page.Root(Lce.Content(A_LIST_OF_ROOT_ITEMS))
)
)
)
assertNoEvents<SettingsEvent>()
}
@Test
fun `when sign out clicked, then clears credentials`() = runViewModelTest {
fakeCredentialsStore.expectUnit { it.clear() }
val aSignOutItem = aSettingTextItem(id = SettingItem.Id.SignOut)
viewModel.test().onClick(aSignOutItem)
assertNoStates<SettingsScreenState>()
assertEvents(SettingsEvent.SignedOut)
verifyExpects()
}
@Test
fun `when event log clicked, then opens event log`() = runViewModelTest {
val anEventLogItem = aSettingTextItem(id = SettingItem.Id.EventLog)
viewModel.test().onClick(anEventLogItem)
assertNoStates<SettingsScreenState>()
assertEvents(SettingsEvent.OpenEventLog)
}
@Test
fun `when encryption clicked, then emits encryption page`() = runViewModelTest {
val anEncryptionItem = aSettingTextItem(id = SettingItem.Id.Encryption)
viewModel.test().onClick(anEncryptionItem)
assertNoEvents<SettingsEvent>()
assertStates(
SettingsScreenState(
SpiderPage(
route = Page.Routes.encryption,
label = "Encryption",
parent = Page.Routes.root,
state = Page.Security
)
)
)
}
@Test
fun `when privacy policy clicked, then opens privacy policy url`() = runViewModelTest {
val aPrivacyPolicyItem = aSettingTextItem(id = SettingItem.Id.PrivacyPolicy)
viewModel.test().onClick(aPrivacyPolicyItem)
assertNoStates<SettingsScreenState>()
assertEvents(SettingsEvent.OpenUrl(APP_PRIVACY_POLICY_URL))
}
@Test
fun `when going to import room, then emits import room keys page`() = runViewModelTest {
viewModel.test().goToImportRoom()
assertStates(
SettingsScreenState(
SpiderPage(
route = Page.Routes.importRoomKeys,
label = "Import room keys",
parent = Page.Routes.encryption,
state = Page.ImportRoomKey()
)
)
)
assertNoEvents<SettingsEvent>()
}
@Test
fun `given on import room keys page, when selecting file, then emits selection`() = runViewModelTest {
fakeUriFilenameResolver.givenFilename(A_URI.instance).returns(A_FILENAME)
viewModel.test(initialState = SettingsScreenState(AN_INITIAL_IMPORT_ROOM_KEYS_PAGE)).fileSelected(A_URI.instance)
assertStates(
SettingsScreenState(
AN_INITIAL_IMPORT_ROOM_KEYS_PAGE.copy(
state = Page.ImportRoomKey(
selectedFile = NamedUri(A_FILENAME, A_URI.instance)
)
)
)
)
assertNoEvents<SettingsEvent>()
}
@Test
fun `given success when importing room keys, then emits progress`() = runViewModelTest {
fakeSyncService.expectUnit { it.forceManualRefresh(A_LIST_OF_ROOM_IDS) }
fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance)
fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(A_LIST_OF_ROOM_IDS)
viewModel
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))
.importFromFileKeys(A_URI.instance, A_PASSPHRASE)
assertStates<SettingsScreenState>(
{ copy(page = page.updateState<Page.ImportRoomKey> { copy(importProgress = Lce.Loading()) }) },
{ copy(page = page.updateState<Page.ImportRoomKey> { copy(importProgress = Lce.Content(Unit)) }) },
)
assertNoEvents<SettingsEvent>()
verifyExpects()
}
@Test
fun `given error when importing room keys, then emits error`() = runViewModelTest {
fakeContentResolver.givenFile(A_URI.instance).throws(AN_ERROR)
viewModel
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))
.importFromFileKeys(A_URI.instance, A_PASSPHRASE)
assertStates<SettingsScreenState>(
{ copy(page = page.updateState<Page.ImportRoomKey> { copy(importProgress = Lce.Loading()) }) },
{ copy(page = page.updateState<Page.ImportRoomKey> { copy(importProgress = Lce.Error(AN_ERROR)) }) },
)
assertNoEvents<SettingsEvent>()
}
}
class FakeInputStream {
val instance = mockk<InputStream>()
}
fun aSettingTextItem(
id: SettingItem.Id = SettingItem.Id.Ignored,
content: String = "text-content",
subtitle: String? = null
) = SettingItem.Text(id, content, subtitle)
fun aSettingHeaderItem(
label: String = "header-label",
) = SettingItem.Header(label)
class FakeContentResolver {
val instance = mockk<ContentResolver>()
fun givenFile(uri: Uri) = every { instance.openInputStream(uri) }.delegateReturn()
fun givenUriResult(uri: Uri) = every { instance.query(uri, null, null, null, null) }.delegateReturn()
}
internal class FakeSettingsItemFactory {
val instance = mockk<SettingsItemFactory>()
fun givenRoot() = every { instance.root() }.delegateReturn()
}
class FakeStoreCleaner : StoreCleaner by mockk()
class FakeCryptoService : CryptoService by mockk() {
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
}
class FakeSyncService : SyncService by mockk()
class FakeUriFilenameResolver {
val instance = mockk<UriFilenameResolver>()
fun givenFilename(uri: Uri) = coEvery { instance.readFilenameFromUri(uri) }
}
class FakeUri {
val instance = mockk<Uri>()
fun givenNonHierarchical() {
givenContent(schema = "mail", path = null)
}
fun givenContent(schema: String, path: String?) {
every { instance.scheme } returns schema
every { instance.path } returns path
}
}
@Suppress("UNCHECKED_CAST")
private inline fun <reified S : Page> SpiderPage<out Page>.updateState(crossinline block: S.() -> S): SpiderPage<Page> {
require(this.state is S)
return (this as SpiderPage<S>).copy(state = block(this.state)) as SpiderPage<Page>
}
fun aImportRoomKeysPage(
state: Page.ImportRoomKey = Page.ImportRoomKey()
) = SpiderPage(
route = Page.Routes.importRoomKeys,
label = "Import room keys",
parent = Page.Routes.encryption,
state = state
)

View File

@ -0,0 +1,68 @@
package app.dapk.st.settings
import android.provider.OpenableColumns
import app.dapk.st.core.CoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import kotlin.test.assertFailsWith
private const val A_LAST_SEGMENT = "a-file-name.foo"
private const val A_DISPLAY_NAME = "file-display-name.foo"
class UriFilenameResolverTest {
private val fakeUri = FakeUri()
private val fakeContentResolver = FakeContentResolver()
private val uriFilenameResolver = UriFilenameResolver(fakeContentResolver.instance, CoroutineDispatchers())
@Test
fun `given a non hierarchical Uri when querying file name then throws`() = runTest {
assertFailsWith<IllegalStateException> {
fakeUri.givenNonHierarchical()
uriFilenameResolver.readFilenameFromUri(fakeUri.instance)
}
}
@Test
fun `given a non content schema Uri when querying file name then returns last segment`() = runTest {
fakeUri.givenContent(schema = "file", path = "path/to/$A_LAST_SEGMENT")
val result = uriFilenameResolver.readFilenameFromUri(fakeUri.instance)
result shouldBeEqualTo A_LAST_SEGMENT
}
@Test
fun `given content schema Uri with no backing content when querying file name then returns last segment`() = runTest {
fakeUri.givenContent(schema = "content", path = "path/to/$A_LAST_SEGMENT")
fakeContentResolver.givenUriResult(fakeUri.instance).returns(null)
val result = uriFilenameResolver.readFilenameFromUri(fakeUri.instance)
result shouldBeEqualTo A_LAST_SEGMENT
}
@Test
fun `given content schema Uri with empty backing content when querying file name then returns last segment`() = runTest {
fakeUri.givenContent(schema = "content", path = "path/to/$A_LAST_SEGMENT")
val emptyCursor = FakeCursor().also { it.givenEmpty() }
fakeContentResolver.givenUriResult(fakeUri.instance).returns(emptyCursor.instance)
val result = uriFilenameResolver.readFilenameFromUri(fakeUri.instance)
result shouldBeEqualTo A_LAST_SEGMENT
}
@Test
fun `given content schema Uri with backing content when querying file name then returns display name column`() = runTest {
fakeUri.givenContent(schema = "content", path = "path/to/$A_DISPLAY_NAME")
val aCursor = FakeCursor().also { it.givenString(OpenableColumns.DISPLAY_NAME, A_DISPLAY_NAME) }
fakeContentResolver.givenUriResult(fakeUri.instance).returns(aCursor.instance)
val result = uriFilenameResolver.readFilenameFromUri(fakeUri.instance)
result shouldBeEqualTo A_DISPLAY_NAME
}
}

View File

@ -3,6 +3,7 @@ applyAndroidLibraryModule(project)
dependencies {
implementation project(":matrix:services:crypto")
implementation project(":domains:android:core")
implementation project(":domains:android:viewmodel")
implementation project(":design-library")
implementation project(":core")
}

View File

@ -1,9 +1,9 @@
package app.dapk.st.verification
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DapkViewModel
import app.dapk.st.matrix.crypto.CryptoService
import app.dapk.st.matrix.crypto.Verification
import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.launch
class VerificationViewModel(

View File

@ -0,0 +1,10 @@
package fixture
import app.dapk.st.matrix.common.DecryptionResult
import app.dapk.st.matrix.common.JsonString
fun aDecryptionSuccessResult(
payload: JsonString = aJsonString(),
isVerified: Boolean = false,
) = DecryptionResult.Success(payload, isVerified)

View File

@ -0,0 +1,21 @@
package fixture
import app.dapk.st.matrix.common.*
fun anEncryptedMegOlmV1Message(
cipherText: CipherText = aCipherText(),
deviceId: DeviceId = aDeviceId(),
senderKey: String = "a-sender-key",
sessionId: SessionId = aSessionId(),
) = EncryptedMessageContent.MegOlmV1(cipherText, deviceId, senderKey, sessionId)
fun anEncryptedOlmV1Message(
senderId: UserId = aUserId(),
cipherText: Map<Curve25519, EncryptedMessageContent.CipherTextInfo> = emptyMap(),
senderKey: Curve25519 = aCurve25519(),
) = EncryptedMessageContent.OlmV1(senderId, cipherText, senderKey)
fun aCipherTextInfo(
body: CipherText = aCipherText(),
type: Int = 1,
) = EncryptedMessageContent.CipherTextInfo(body, type)

View File

@ -0,0 +1,14 @@
package fixture
import app.dapk.st.matrix.common.AlgorithmName
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.SessionId
import app.dapk.st.matrix.common.SharedRoomKey
fun aSharedRoomKey(
algorithmName: AlgorithmName = anAlgorithmName(),
roomId: RoomId = aRoomId(),
sessionId: SessionId = aSessionId(),
sessionKey: String = "a-session-key",
isExported: Boolean = false,
) = SharedRoomKey(algorithmName, roomId, sessionId, sessionKey, isExported)

View File

@ -9,7 +9,7 @@ import app.dapk.st.matrix.crypto.Olm
private val ALGORITHM_MEGOLM = AlgorithmName("m.megolm.v1.aes-sha2")
typealias EncryptMessageWithMegolmUseCase = suspend (DeviceCredentials, MessageToEncrypt) -> Crypto.EncryptionResult
internal typealias EncryptMessageWithMegolmUseCase = suspend (DeviceCredentials, MessageToEncrypt) -> Crypto.EncryptionResult
internal class EncryptMessageWithMegolmUseCaseImpl(
private val olm: Olm,

View File

@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.crypto
import app.dapk.st.matrix.crypto.Olm
import app.dapk.st.matrix.device.DeviceService
typealias MaybeCreateAndUploadOneTimeKeysUseCase = suspend (ServerKeyCount) -> Unit
internal typealias MaybeCreateAndUploadOneTimeKeysUseCase = suspend (ServerKeyCount) -> Unit
internal class MaybeCreateAndUploadOneTimeKeysUseCaseImpl(
private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase,

View File

@ -6,7 +6,7 @@ import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.common.crypto
import app.dapk.st.matrix.device.DeviceService
typealias UpdateKnownOlmSessionUseCase = suspend (List<UserId>, SyncToken?) -> Unit
internal typealias UpdateKnownOlmSessionUseCase = suspend (List<UserId>, SyncToken?) -> Unit
internal class UpdateKnownOlmSessionUseCaseImpl(
private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase,

View File

@ -4,13 +4,13 @@ import app.dapk.st.matrix.common.*
import fake.FakeMatrixLogger
import fake.FakeOlm
import fixture.*
import internalfake.FakeEncryptMessageWithMegolmUseCase
import internalfake.FakeFetchAccountCryptoUseCase
import io.mockk.coEvery
import io.mockk.mockk
import internalfake.FakeMaybeCreateAndUploadOneTimeKeysUseCase
import internalfake.FakeUpdateKnownOlmSessionUseCase
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import test.delegateReturn
import test.runExpectTest
private val A_LIST_OF_SHARED_ROOM_KEYS = listOf(aSharedRoomKey())
@ -27,7 +27,6 @@ private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession()
private val AN_OLM_PAYLOAD = anEncryptedOlmV1Message(cipherText = mapOf(AN_ACCOUNT_CRYPTO_SESSION.senderKey to aCipherTextInfo()))
private val A_DECRYPTION_RESULT = aDecryptionSuccessResult()
internal class OlmCryptoTest {
private val fakeOlm = FakeOlm()
@ -100,44 +99,3 @@ internal class OlmCryptoTest {
result shouldBeEqualTo A_DECRYPTION_RESULT
}
}
fun aDecryptionSuccessResult(
payload: JsonString = aJsonString(),
isVerified: Boolean = false,
) = DecryptionResult.Success(payload, isVerified)
fun anEncryptedMegOlmV1Message(
cipherText: CipherText = aCipherText(),
deviceId: DeviceId = aDeviceId(),
senderKey: String = "a-sender-key",
sessionId: SessionId = aSessionId(),
) = EncryptedMessageContent.MegOlmV1(cipherText, deviceId, senderKey, sessionId)
fun anEncryptedOlmV1Message(
senderId: UserId = aUserId(),
cipherText: Map<Curve25519, EncryptedMessageContent.CipherTextInfo> = emptyMap(),
senderKey: Curve25519 = aCurve25519(),
) = EncryptedMessageContent.OlmV1(senderId, cipherText, senderKey)
fun aCipherTextInfo(
body: CipherText = aCipherText(),
type: Int = 1,
) = EncryptedMessageContent.CipherTextInfo(body, type)
class FakeEncryptMessageWithMegolmUseCase : EncryptMessageWithMegolmUseCase by mockk() {
fun givenEncrypt(credentials: DeviceCredentials, message: MessageToEncrypt) = coEvery {
this@FakeEncryptMessageWithMegolmUseCase.invoke(credentials, message)
}.delegateReturn()
}
class FakeUpdateKnownOlmSessionUseCase : UpdateKnownOlmSessionUseCase by mockk()
class FakeMaybeCreateAndUploadOneTimeKeysUseCase : MaybeCreateAndUploadOneTimeKeysUseCase by mockk()
fun aSharedRoomKey(
algorithmName: AlgorithmName = anAlgorithmName(),
roomId: RoomId = aRoomId(),
sessionId: SessionId = aSessionId(),
sessionKey: String = "a-session-key",
isExported: Boolean = false,
) = SharedRoomKey(algorithmName, roomId, sessionId, sessionKey, isExported)

View File

@ -0,0 +1,14 @@
package internalfake
import app.dapk.st.matrix.common.DeviceCredentials
import app.dapk.st.matrix.crypto.internal.EncryptMessageWithMegolmUseCase
import app.dapk.st.matrix.crypto.internal.MessageToEncrypt
import io.mockk.coEvery
import io.mockk.mockk
import test.delegateReturn
class FakeEncryptMessageWithMegolmUseCase : EncryptMessageWithMegolmUseCase by mockk() {
fun givenEncrypt(credentials: DeviceCredentials, message: MessageToEncrypt) = coEvery {
this@FakeEncryptMessageWithMegolmUseCase.invoke(credentials, message)
}.delegateReturn()
}

View File

@ -0,0 +1,6 @@
package internalfake
import app.dapk.st.matrix.crypto.internal.MaybeCreateAndUploadOneTimeKeysUseCase
import io.mockk.mockk
class FakeMaybeCreateAndUploadOneTimeKeysUseCase : MaybeCreateAndUploadOneTimeKeysUseCase by mockk()

View File

@ -0,0 +1,6 @@
package internalfake
import app.dapk.st.matrix.crypto.internal.UpdateKnownOlmSessionUseCase
import io.mockk.mockk
class FakeUpdateKnownOlmSessionUseCase : UpdateKnownOlmSessionUseCase by mockk()

View File

@ -10,6 +10,7 @@ import io.mockk.slot
import org.amshove.kluent.shouldBeEqualTo
import test.Returns
import test.delegateReturn
import test.returns
class FakeOlm : Olm by mockk() {
@ -22,7 +23,7 @@ class FakeOlm : Olm by mockk() {
fun givenCreatesAccount(credentials: UserCredentials): Returns<Olm.AccountCryptoSession> {
val slot = slot<suspend (Olm.AccountCryptoSession) -> Unit>()
val mockKStubScope = coEvery { ensureAccountCrypto(credentials, capture(slot)) }
return Returns { value ->
return returns { value ->
mockKStubScope coAnswers {
slot.captured.invoke(value)
value
@ -39,7 +40,7 @@ class FakeOlm : Olm by mockk() {
fun givenMissingOlmSessions(newDevices: List<DeviceKeys>): Returns<List<Olm.DeviceCryptoSession>> {
val slot = slot<suspend (List<DeviceKeys>) -> List<Olm.DeviceCryptoSession>>()
val mockKStubScope = coEvery { olmSessions(newDevices, capture(slot)) }
return Returns { value ->
return returns { value ->
mockKStubScope coAnswers {
slot.captured.invoke(newDevices).also {
value shouldBeEqualTo it
@ -55,7 +56,7 @@ class FakeOlm : Olm by mockk() {
): Returns<DeviceService.OneTimeKeys> {
val slot = slot<suspend (DeviceService.OneTimeKeys) -> Unit>()
val mockKStubScope = coEvery { with(accountCryptoSession) { generateOneTimeKeys(countToCreate, credentials, capture(slot)) } }
return Returns { value ->
return returns { value ->
mockKStubScope coAnswers {
slot.captured.invoke(value)
}

View File

@ -25,6 +25,8 @@ include ':domains:android:imageloader'
include ':domains:android:work'
include ':domains:android:tracking'
include ':domains:android:push'
include ':domains:android:viewmodel-stub'
include ':domains:android:viewmodel'
include ':domains:store'
include ':domains:olm-stub'
include ':domains:olm'

View File

@ -84,7 +84,7 @@ class TestMatrix(
val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore())
val olm = OlmWrapper(
olmStore = olmAccountStore,
singletonFlows = SingletonFlows(),
singletonFlows = SingletonFlows(coroutineDispatchers),
jsonCanonicalizer = JsonCanonicalizer(),
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
errorTracker = errorTracker,