more tests
This commit is contained in:
parent
634fc15829
commit
ecb24ece11
|
@ -157,7 +157,7 @@ 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 profileModule by unsafeLazy { ProfileModule(matrixModules.profile) }
|
||||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync) }
|
||||
val notificationsModule by unsafeLazy {
|
||||
NotificationsModule(
|
||||
matrixModules.push,
|
||||
|
|
|
@ -8,4 +8,5 @@ dependencies {
|
|||
testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||
testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||
testFixturesImplementation 'io.mockk:mockk:1.12.2'
|
||||
testFixturesImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package test
|
||||
|
||||
import io.mockk.MockKMatcherScope
|
||||
import io.mockk.MockKVerificationScope
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerifyAll
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
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) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any.ignore() = Unit
|
||||
|
||||
interface ExpectTestScope : CoroutineScope {
|
||||
fun verifyExpects()
|
||||
fun <T> T.expectUnit(block: suspend MockKMatcherScope.(T) -> Unit)
|
||||
}
|
|
@ -12,4 +12,4 @@ fun <T, B> MockKStubScope<T, B>.delegateReturn(): Returns<T> = Returns { value -
|
|||
|
||||
fun interface Returns<T> {
|
||||
fun returns(value: T)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ ext.Dependencies.with {
|
|||
|
||||
google = new DependenciesContainer()
|
||||
google.with {
|
||||
androidGradlePlugin = "com.android.tools.build:gradle:7.1.1"
|
||||
androidGradlePlugin = "com.android.tools.build:gradle:7.1.2"
|
||||
|
||||
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
|
||||
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
|
||||
|
|
|
@ -49,6 +49,7 @@ data class SpiderPage<T>(
|
|||
val label: String,
|
||||
val parent: Route<*>?,
|
||||
val state: T,
|
||||
val hasToolbar: Boolean = true,
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
|
|
|
@ -231,10 +231,10 @@ class OlmWrapper(
|
|||
return SignedJson(olmAccount.signMessage(jsonCanonicalizer.canonicalize(json)))
|
||||
}
|
||||
|
||||
override suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Long, body: CipherText): DecryptionResult {
|
||||
override suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Int, body: CipherText): DecryptionResult {
|
||||
interactWithOlm()
|
||||
val olmMessage = OlmMessage().apply {
|
||||
this.mType = type
|
||||
this.mType = type.toLong()
|
||||
this.mCipherText = body.value
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,12 @@ package app.dapk.st.directory
|
|||
sealed interface DirectoryScreenState {
|
||||
|
||||
object EmptyLoading : DirectoryScreenState
|
||||
data class Error(val cause: Throwable) : DirectoryScreenState
|
||||
data class Content(
|
||||
val overviewState: DirectoryState,
|
||||
// val appState: AppState,
|
||||
// val navigationState: NavigationState,
|
||||
// val isRefreshing: Boolean = false,
|
||||
) : DirectoryScreenState
|
||||
}
|
||||
|
||||
sealed interface NavigationState {}
|
||||
|
||||
sealed interface DirectoryEvent { data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
||||
sealed interface DirectoryEvent {
|
||||
data class OpenDownloadUrl(val url: String) : DirectoryEvent
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
|
||||
class NotificationDisplayer(
|
||||
context: Context,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) {
|
||||
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
fun showNotificationMessage(tag: String?, id: Int, notification: Notification) {
|
||||
notificationManager.notify(tag, id, notification)
|
||||
}
|
||||
|
||||
fun cancelNotificationMessage(tag: String?, id: Int) {
|
||||
notificationManager.cancel(tag, id)
|
||||
}
|
||||
|
||||
fun cancelAllNotifications() {
|
||||
try {
|
||||
notificationManager.cancelAll()
|
||||
} catch (e: Exception) {
|
||||
errorTracker.track(e, "Failed to cancel all notifications")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import android.R
|
||||
import android.app.*
|
||||
import android.app.Notification.InboxStyle
|
||||
import android.content.Context
|
||||
|
@ -105,7 +104,7 @@ class NotificationsUseCase(
|
|||
|
||||
return Notification.Builder(context, channelId)
|
||||
.setStyle(summaryInboxStyle)
|
||||
.setSmallIcon(R.drawable.ic_menu_send)
|
||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||
.setGroupSummary(true)
|
||||
.setGroup(GROUP_ID)
|
||||
|
@ -155,7 +154,7 @@ class NotificationsUseCase(
|
|||
.setStyle(messageStyle)
|
||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||
.setShortcutId(roomOverview.roomId.value)
|
||||
.setSmallIcon(R.drawable.ic_menu_send)
|
||||
.setSmallIcon(R.drawable.ic_notification_small_icon)
|
||||
.setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) })
|
||||
.setAutoCancel(true)
|
||||
.build(),
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.imageloader.IconLoader
|
||||
|
||||
class RoomGroupMessageCreator(
|
||||
private val iconLoader: IconLoader,
|
||||
// private val bitmapLoader: BitmapLoader,
|
||||
// private val stringProvider: StringProvider,
|
||||
// private val notificationUtils: NotificationUtils,
|
||||
// private val appContext: Context
|
||||
) {
|
||||
|
||||
// fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
|
||||
// val firstKnownRoomEvent = events[0]
|
||||
// val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: ""
|
||||
// val roomIsGroup = !firstKnownRoomEvent.roomIsDirect
|
||||
//
|
||||
// val style = Notification.MessagingStyle(
|
||||
// Person.Builder()
|
||||
// .setName(userDisplayName)
|
||||
// .setIcon(iconLoader.load(userAvatarUrl))
|
||||
// .setKey(firstKnownRoomEvent.matrixID)
|
||||
// .build()
|
||||
// ).also {
|
||||
// it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||
// it.isGroupConversation = roomIsGroup
|
||||
// it.addMessagesFromEvents(events)
|
||||
// }
|
||||
//
|
||||
// val tickerText = if (roomIsGroup) {
|
||||
// stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
|
||||
// } else {
|
||||
// stringProvider.getString(R.string.notification_ticker_text_dm, events.last().senderName, events.last().description)
|
||||
// }
|
||||
//
|
||||
// val largeBitmap = getRoomBitmap(events)
|
||||
//
|
||||
// val lastMessageTimestamp = events.last().timestamp
|
||||
// val smartReplyErrors = events.filter { it.isSmartReplyError() }
|
||||
// val messageCount = (events.size - smartReplyErrors.size)
|
||||
// val meta = RoomNotification.Message.Meta(
|
||||
// summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, roomIsDirect = !roomIsGroup),
|
||||
// messageCount = messageCount,
|
||||
// latestTimestamp = lastMessageTimestamp,
|
||||
// roomId = roomId,
|
||||
// shouldBing = events.any { it.noisy }
|
||||
// )
|
||||
// return RoomNotification.Message(
|
||||
// notificationUtils.buildMessagesListNotification(
|
||||
// style,
|
||||
// RoomEventGroupInfo(roomId, roomName, isDirect = !roomIsGroup),
|
||||
// largeIcon = largeBitmap,
|
||||
// lastMessageTimestamp,
|
||||
// userDisplayName,
|
||||
// tickerText
|
||||
// ),
|
||||
// meta
|
||||
// )
|
||||
// }
|
||||
|
||||
// private fun Notification.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
|
||||
// events.forEach { event ->
|
||||
// val senderPerson = if (event.outGoingMessage) {
|
||||
// null
|
||||
// } else {
|
||||
// Person.Builder()
|
||||
// .setName(event.senderName)
|
||||
// .setIcon(iconLoader.getUserIcon(event.senderAvatarPath))
|
||||
// .setKey(event.senderId)
|
||||
// .build()
|
||||
// }
|
||||
// when {
|
||||
// event.isSmartReplyError() -> addMessage(stringProvider.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
|
||||
// else -> {
|
||||
// val message = Notification.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message ->
|
||||
// event.imageUri?.let {
|
||||
// message.setData("image/", it)
|
||||
// }
|
||||
// }
|
||||
// addMessage(message)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
// return when (events.size) {
|
||||
// 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||
// else -> {
|
||||
// stringProvider.getQuantityString(
|
||||
// R.plurals.notification_compat_summary_line_for_room,
|
||||
// events.size,
|
||||
// roomName,
|
||||
// events.size
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDirect: Boolean): Span {
|
||||
// return if (roomIsDirect) {
|
||||
// buildSpannedString {
|
||||
// bold { append("${event.senderName}: ") }
|
||||
// append(event.description)
|
||||
// }
|
||||
// } else {
|
||||
// buildSpannedString {
|
||||
// bold { append("$roomName: ${event.senderName} ") }
|
||||
// append(event.description)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
|
||||
// // Use the last event (most recent?)
|
||||
// return events.lastOrNull()
|
||||
// ?.roomAvatarPath
|
||||
// ?.let { bitmapLoader.getRoomBitmap(it) }
|
||||
// }
|
||||
}
|
||||
|
||||
//data class RoomMessage(
|
||||
//
|
||||
//)
|
|
@ -0,0 +1,23 @@
|
|||
<vector android:height="50dp" android:viewportHeight="500"
|
||||
android:viewportWidth="500" android:width="50dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M105.31,85.19L414.82,85.19L414.82,153.59L105.31,153.59z"
|
||||
android:strokeColor="#70D48D" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="0"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M105.31,326.99L414.82,326.99L414.82,395.39L105.31,395.39z"
|
||||
android:strokeColor="#70D48D" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="0"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M104.76,190.64L140.68,190.64L140.68,223.13L104.76,223.13z"
|
||||
android:strokeColor="#70D48D" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="0"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M105.31,222.83L414.82,222.83L414.82,255.32L105.31,255.32z"
|
||||
android:strokeColor="#70D48D" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="0"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M379.51,254.18L415.41,254.18L415.41,286.67L379.51,286.67z"
|
||||
android:strokeColor="#70D48D" android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" android:strokeWidth="0"/>
|
||||
</vector>
|
|
@ -2,13 +2,15 @@ package app.dapk.st.profile
|
|||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
|
||||
class ProfileModule(
|
||||
private val profileService: ProfileService,
|
||||
private val syncService: SyncService,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun profileViewModel(): ProfileViewModel {
|
||||
return ProfileViewModel(profileService)
|
||||
return ProfileViewModel(profileService, syncService)
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,7 @@ import app.dapk.st.core.LifecycleEffect
|
|||
import app.dapk.st.core.StartObserving
|
||||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.design.components.CircleishAvatar
|
||||
import app.dapk.st.design.components.Spider
|
||||
import app.dapk.st.design.components.TextRow
|
||||
import app.dapk.st.design.components.percentOfHeight
|
||||
import app.dapk.st.settings.SettingsActivity
|
||||
|
@ -85,6 +86,11 @@ fun ProfileScreen(viewModel: ProfileViewModel) {
|
|||
title = "Homeserver",
|
||||
content = state.me.homeServerUrl.value,
|
||||
)
|
||||
|
||||
TextRow(
|
||||
title = "Invitations",
|
||||
content = "${state.invitationsCount} pending",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@ import app.dapk.st.matrix.room.ProfileService
|
|||
|
||||
sealed interface ProfileScreenState {
|
||||
object Loading : ProfileScreenState
|
||||
data class Content(val me: ProfileService.Me) : ProfileScreenState
|
||||
data class Content(
|
||||
val me: ProfileService.Me,
|
||||
val invitationsCount: Int,
|
||||
) : ProfileScreenState
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -3,18 +3,23 @@ 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 kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ProfileViewModel(
|
||||
private val profileService: ProfileService,
|
||||
private val syncService: SyncService,
|
||||
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
||||
initialState = ProfileScreenState.Loading
|
||||
) {
|
||||
|
||||
fun start() {
|
||||
viewModelScope.launch {
|
||||
val invitationsCount = syncService.invites().firstOrNull()?.size ?: 0
|
||||
val me = profileService.me(forceRefresh = true)
|
||||
state = ProfileScreenState.Content(me)
|
||||
state = ProfileScreenState.Content(me, invitationsCount = invitationsCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,4 +11,5 @@ fun aCipherText(value: String = "cipher-content") = CipherText(value)
|
|||
fun aCurve25519(value: String = "curve-value") = Curve25519(value)
|
||||
fun aEd25519(value: String = "ed-value") = Ed25519(value)
|
||||
fun anAlgorithmName(value: String = "an-algorithm") = AlgorithmName(value)
|
||||
fun aJsonString(value: String = "{}") = JsonString(value)
|
||||
fun aJsonString(value: String = "{}") = JsonString(value)
|
||||
fun aSyncToken(value: String = "a-sync-token") = SyncToken(value)
|
|
@ -141,12 +141,11 @@ fun MatrixServiceInstaller.installCryptoService(
|
|||
|
||||
val olmCrypto = OlmCrypto(
|
||||
olm,
|
||||
deviceService,
|
||||
logger,
|
||||
registerOlmSessionUseCase,
|
||||
encryptMegolmUseCase,
|
||||
accountCryptoUseCase,
|
||||
MaybeCreateAndUploadOneTimeKeysUseCaseImpl(accountCryptoUseCase, olm, credentialsStore, deviceService, logger)
|
||||
UpdateKnownOlmSessionUseCaseImpl(accountCryptoUseCase, deviceService, registerOlmSessionUseCase, logger),
|
||||
MaybeCreateAndUploadOneTimeKeysUseCaseImpl(accountCryptoUseCase, olm, credentialsStore, deviceService, logger),
|
||||
logger
|
||||
)
|
||||
val verificationHandler = VerificationHandler(deviceService, credentialsStore, logger, JsonCanonicalizer(), olm)
|
||||
val roomKeyImporter = RoomKeyImporter(coroutineDispatchers)
|
||||
|
|
|
@ -24,7 +24,7 @@ interface Olm {
|
|||
publishKeys: suspend (DeviceService.OneTimeKeys) -> Unit
|
||||
)
|
||||
|
||||
suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Long, body: CipherText): DecryptionResult
|
||||
suspend fun decryptOlm(olmAccount: AccountCryptoSession, senderKey: Curve25519, type: Int, body: CipherText): DecryptionResult
|
||||
suspend fun decryptMegOlm(sessionId: SessionId, cipherText: CipherText): DecryptionResult
|
||||
suspend fun verifyExternalUser(keys: Ed25519?, recipeientKeys: Ed25519?): Boolean
|
||||
suspend fun olmSessions(devices: List<DeviceKeys>, onMissing: suspend (List<DeviceKeys>) -> List<DeviceCryptoSession>): List<DeviceCryptoSession>
|
||||
|
|
|
@ -3,16 +3,14 @@ package app.dapk.st.matrix.crypto.internal
|
|||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.crypto.Crypto
|
||||
import app.dapk.st.matrix.crypto.Olm
|
||||
import app.dapk.st.matrix.device.DeviceService
|
||||
|
||||
internal class OlmCrypto(
|
||||
private val olm: Olm,
|
||||
private val deviceService: DeviceService,
|
||||
private val logger: MatrixLogger,
|
||||
private val registerOlmSessionUseCase: RegisterOlmSessionUseCase,
|
||||
private val encryptMessageWithMegolmUseCase: EncryptMessageWithMegolmUseCase,
|
||||
private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase,
|
||||
private val maybeCreateAndUploadOneTimeKeysUseCase: MaybeCreateAndUploadOneTimeKeysUseCase
|
||||
private val updateKnownOlmSessionUseCase: UpdateKnownOlmSessionUseCase,
|
||||
private val maybeCreateAndUploadOneTimeKeysUseCase: MaybeCreateAndUploadOneTimeKeysUseCase,
|
||||
private val logger: MatrixLogger
|
||||
) {
|
||||
|
||||
suspend fun importRoomKeys(keys: List<SharedRoomKey>) {
|
||||
|
@ -20,34 +18,29 @@ internal class OlmCrypto(
|
|||
olm.import(keys)
|
||||
}
|
||||
|
||||
suspend fun decrypt(payload: EncryptedMessageContent): DecryptionResult {
|
||||
return when (payload) {
|
||||
is EncryptedMessageContent.MegOlmV1 -> {
|
||||
olm.decryptMegOlm(payload.sessionId, payload.cipherText)
|
||||
}
|
||||
is EncryptedMessageContent.OlmV1 -> {
|
||||
val account = fetchAccountCryptoUseCase.invoke()
|
||||
logger.crypto("decrypt olm: $payload")
|
||||
payload.cipherText[account.senderKey]?.let {
|
||||
olm.decryptOlm(account, payload.senderKey, it.type.toLong(), it.body)
|
||||
} ?: DecryptionResult.Failed("Missing cipher for sender : ${account.senderKey}")
|
||||
}
|
||||
}
|
||||
suspend fun decrypt(payload: EncryptedMessageContent) = when (payload) {
|
||||
is EncryptedMessageContent.MegOlmV1 -> olm.decryptMegOlm(payload.sessionId, payload.cipherText)
|
||||
is EncryptedMessageContent.OlmV1 -> decryptOlm(payload)
|
||||
}
|
||||
|
||||
private suspend fun decryptOlm(payload: EncryptedMessageContent.OlmV1): DecryptionResult {
|
||||
logger.crypto("decrypt olm: $payload")
|
||||
val account = fetchAccountCryptoUseCase.invoke()
|
||||
return payload.cipherFor(account)?.let { olm.decryptOlm(account, payload.senderKey, it.type, it.body) }
|
||||
?: DecryptionResult.Failed("Missing cipher for sender : ${account.senderKey}")
|
||||
}
|
||||
|
||||
suspend fun encryptMessage(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult {
|
||||
val messageToEncrypt = MessageToEncrypt(roomId, messageJson)
|
||||
return encryptMessageWithMegolmUseCase.invoke(credentials, messageToEncrypt)
|
||||
return encryptMessageWithMegolmUseCase.invoke(credentials, MessageToEncrypt(roomId, messageJson))
|
||||
}
|
||||
|
||||
suspend fun updateOlmSessions(userId: List<UserId>, syncToken: SyncToken?) {
|
||||
logger.crypto("updating olm sessions for ${userId.map { it.value }}")
|
||||
val account = fetchAccountCryptoUseCase.invoke()
|
||||
val keys = deviceService.fetchDevices(userId, syncToken).filterNot { it.deviceId == account.deviceKeys.deviceId }
|
||||
registerOlmSessionUseCase.invoke(keys, account)
|
||||
updateKnownOlmSessionUseCase.invoke(userId, syncToken)
|
||||
}
|
||||
|
||||
suspend fun maybeCreateMoreKeys(currentServerKeyCount: ServerKeyCount) {
|
||||
maybeCreateAndUploadOneTimeKeysUseCase.invoke(currentServerKeyCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun EncryptedMessageContent.OlmV1.cipherFor(account: Olm.AccountCryptoSession) = this.cipherText[account.senderKey]
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package app.dapk.st.matrix.crypto.internal
|
||||
|
||||
import app.dapk.st.matrix.common.MatrixLogger
|
||||
import app.dapk.st.matrix.common.SyncToken
|
||||
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 class UpdateKnownOlmSessionUseCaseImpl(
|
||||
private val fetchAccountCryptoUseCase: FetchAccountCryptoUseCase,
|
||||
private val deviceService: DeviceService,
|
||||
private val registerOlmSessionUseCase: RegisterOlmSessionUseCase,
|
||||
private val logger: MatrixLogger,
|
||||
) : UpdateKnownOlmSessionUseCase {
|
||||
|
||||
override suspend fun invoke(userIds: List<UserId>, syncToken: SyncToken?) {
|
||||
logger.crypto("updating olm sessions for ${userIds.map { it.value }}")
|
||||
val account = fetchAccountCryptoUseCase.invoke()
|
||||
val keys = deviceService.fetchDevices(userIds, syncToken).filterNot { it.deviceId == account.deviceKeys.deviceId }
|
||||
if (keys.isNotEmpty()) {
|
||||
registerOlmSessionUseCase.invoke(keys, account)
|
||||
} else {
|
||||
logger.crypto("no valid devices keys found to update")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -29,7 +29,7 @@ class FetchMegolmSessionUseCaseTest {
|
|||
private val fetchMegolmSessionUseCase = FetchMegolmSessionUseCaseImpl(
|
||||
fakeOlm,
|
||||
deviceService,
|
||||
FakeFetchAccountCryptoUseCase().also { it.givenAccount(AN_ACCOUNT_CRYPTO_SESSION) },
|
||||
FakeFetchAccountCryptoUseCase().also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) },
|
||||
roomMembersProvider,
|
||||
fakeRegisterOlmSessionUseCase,
|
||||
fakeShareRoomKeyUseCase,
|
||||
|
|
|
@ -27,7 +27,7 @@ class MaybeCreateAndUploadOneTimeKeysUseCaseTest {
|
|||
}
|
||||
|
||||
private val maybeCreateAndUploadOneTimeKeysUseCase = MaybeCreateAndUploadOneTimeKeysUseCaseImpl(
|
||||
FakeFetchAccountCryptoUseCase().also { it.givenAccount(AN_ACCOUNT_CRYPTO_SESSION) },
|
||||
FakeFetchAccountCryptoUseCase().also { it.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION) },
|
||||
fakeOlm,
|
||||
fakeCredentialsStore,
|
||||
fakeDeviceService,
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
package app.dapk.st.matrix.crypto.internal
|
||||
|
||||
import app.dapk.st.matrix.common.*
|
||||
import fake.FakeMatrixLogger
|
||||
import fake.FakeOlm
|
||||
import fixture.*
|
||||
import internalfake.FakeFetchAccountCryptoUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
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())
|
||||
private val A_DEVICE_CREDENTIALS = aDeviceCredentials()
|
||||
private val A_ROOM_ID = aRoomId()
|
||||
private val A_MESSAGE_JSON_TO_ENCRYPT = aJsonString("message!")
|
||||
private val AN_EXPECTED_MESSAGE_TO_ENCRYPT = aMessageToEncrypt(A_ROOM_ID, A_MESSAGE_JSON_TO_ENCRYPT)
|
||||
private val AN_ENCRYPTION_RESULT = anEncryptionResult()
|
||||
private val A_LIST_OF_USER_IDS_TO_UPDATE = listOf(aUserId())
|
||||
private val A_SYNC_TOKEN = aSyncToken()
|
||||
private val A_SERVER_KEY_COUNT = ServerKeyCount(100)
|
||||
private val A_MEGOLM_PAYLOAD = anEncryptedMegOlmV1Message()
|
||||
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()
|
||||
private val fakeEncryptMessageWithMegolmUseCase = FakeEncryptMessageWithMegolmUseCase()
|
||||
private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase()
|
||||
private val fakeUpdateKnownOlmSessionUseCase = FakeUpdateKnownOlmSessionUseCase()
|
||||
private val fakeMaybeCreateAndUploadOneTimeKeysUseCase = FakeMaybeCreateAndUploadOneTimeKeysUseCase()
|
||||
|
||||
private val olmCrypto = OlmCrypto(
|
||||
fakeOlm,
|
||||
fakeEncryptMessageWithMegolmUseCase,
|
||||
fakeFetchAccountCryptoUseCase,
|
||||
fakeUpdateKnownOlmSessionUseCase,
|
||||
fakeMaybeCreateAndUploadOneTimeKeysUseCase,
|
||||
FakeMatrixLogger()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `when importing room keys, then delegates to olm`() = runExpectTest {
|
||||
fakeOlm.expectUnit { it.import(A_LIST_OF_SHARED_ROOM_KEYS) }
|
||||
|
||||
olmCrypto.importRoomKeys(A_LIST_OF_SHARED_ROOM_KEYS)
|
||||
|
||||
verifyExpects()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when encrypting message, then delegates to megolm`() = runTest {
|
||||
fakeEncryptMessageWithMegolmUseCase.givenEncrypt(A_DEVICE_CREDENTIALS, AN_EXPECTED_MESSAGE_TO_ENCRYPT).returns(AN_ENCRYPTION_RESULT)
|
||||
|
||||
val result = olmCrypto.encryptMessage(A_ROOM_ID, A_DEVICE_CREDENTIALS, A_MESSAGE_JSON_TO_ENCRYPT)
|
||||
|
||||
result shouldBeEqualTo AN_ENCRYPTION_RESULT
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when updating olm sessions, then delegates to use case`() = runExpectTest {
|
||||
fakeUpdateKnownOlmSessionUseCase.expectUnit { it.invoke(A_LIST_OF_USER_IDS_TO_UPDATE, A_SYNC_TOKEN) }
|
||||
|
||||
olmCrypto.updateOlmSessions(A_LIST_OF_USER_IDS_TO_UPDATE, A_SYNC_TOKEN)
|
||||
|
||||
verifyExpects()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when maybe creating more keys, then delegates to use case`() = runExpectTest {
|
||||
fakeMaybeCreateAndUploadOneTimeKeysUseCase.expectUnit { it.invoke(A_SERVER_KEY_COUNT) }
|
||||
|
||||
olmCrypto.maybeCreateMoreKeys(A_SERVER_KEY_COUNT)
|
||||
|
||||
verifyExpects()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given megolm payload, when decrypting, then delegates to olm`() = runTest {
|
||||
fakeOlm.givenDecrypting(A_MEGOLM_PAYLOAD).returns(A_DECRYPTION_RESULT)
|
||||
|
||||
val result = olmCrypto.decrypt(A_MEGOLM_PAYLOAD)
|
||||
|
||||
result shouldBeEqualTo A_DECRYPTION_RESULT
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given olm payload, when decrypting, then delegates to olm`() = runTest {
|
||||
fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION)
|
||||
fakeOlm.givenDecrypting(AN_OLM_PAYLOAD, AN_ACCOUNT_CRYPTO_SESSION).returns(A_DECRYPTION_RESULT)
|
||||
|
||||
val result = olmCrypto.decrypt(AN_OLM_PAYLOAD)
|
||||
|
||||
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)
|
|
@ -0,0 +1,51 @@
|
|||
package app.dapk.st.matrix.crypto.internal
|
||||
|
||||
import fake.FakeDeviceService
|
||||
import fake.FakeMatrixLogger
|
||||
import fixture.*
|
||||
import internalfake.FakeFetchAccountCryptoUseCase
|
||||
import internalfake.FakeRegisterOlmSessionUseCase
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
private val USERS_TO_UPDATE = listOf(aUserId())
|
||||
private val A_SYNC_TOKEN = aSyncToken()
|
||||
private val AN_ACCOUNT_CRYPTO_SESSION = anAccountCryptoSession(deviceKeys = aDeviceKeys(deviceId = aDeviceId("unique-device-id")))
|
||||
private val A_DEVICE_KEYS = listOf(aDeviceKeys())
|
||||
private val OWN_DEVICE_KEYS = listOf(AN_ACCOUNT_CRYPTO_SESSION.deviceKeys)
|
||||
private val IGNORED_REGISTERED_SESSION = listOf(aDeviceCryptoSession())
|
||||
|
||||
internal class UpdateKnownOlmSessionUseCaseTest {
|
||||
|
||||
private val fakeFetchAccountCryptoUseCase = FakeFetchAccountCryptoUseCase()
|
||||
private val fakeDeviceService = FakeDeviceService()
|
||||
private val fakeRegisterOlmSessionUseCase = FakeRegisterOlmSessionUseCase()
|
||||
|
||||
private val updateKnownOlmSessionUseCase = UpdateKnownOlmSessionUseCaseImpl(
|
||||
fakeFetchAccountCryptoUseCase,
|
||||
fakeDeviceService,
|
||||
fakeRegisterOlmSessionUseCase,
|
||||
FakeMatrixLogger()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `when updating know olm sessions, then registers device keys`() = runTest {
|
||||
fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION)
|
||||
fakeDeviceService.givenFetchesDevices(USERS_TO_UPDATE, A_SYNC_TOKEN).returns(A_DEVICE_KEYS)
|
||||
fakeRegisterOlmSessionUseCase.givenRegistersSessions(A_DEVICE_KEYS, AN_ACCOUNT_CRYPTO_SESSION).returns(IGNORED_REGISTERED_SESSION)
|
||||
|
||||
updateKnownOlmSessionUseCase.invoke(USERS_TO_UPDATE, A_SYNC_TOKEN)
|
||||
|
||||
fakeRegisterOlmSessionUseCase.verifyRegistersKeys(A_DEVICE_KEYS, AN_ACCOUNT_CRYPTO_SESSION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given device keys contains own device, when updating known olm session, then skips registering`() = runTest {
|
||||
fakeFetchAccountCryptoUseCase.givenFetch().returns(AN_ACCOUNT_CRYPTO_SESSION)
|
||||
fakeDeviceService.givenFetchesDevices(USERS_TO_UPDATE, A_SYNC_TOKEN).returns(OWN_DEVICE_KEYS)
|
||||
|
||||
updateKnownOlmSessionUseCase.invoke(USERS_TO_UPDATE, A_SYNC_TOKEN)
|
||||
|
||||
fakeRegisterOlmSessionUseCase.verifyNoInteractions()
|
||||
}
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
package internalfake
|
||||
|
||||
import app.dapk.st.matrix.crypto.Olm
|
||||
import app.dapk.st.matrix.crypto.internal.FetchAccountCryptoUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import test.delegateReturn
|
||||
|
||||
class FakeFetchAccountCryptoUseCase : FetchAccountCryptoUseCase by mockk() {
|
||||
fun givenAccount(account: Olm.AccountCryptoSession) {
|
||||
coEvery { this@FakeFetchAccountCryptoUseCase.invoke() } returns account
|
||||
}
|
||||
fun givenFetch() = coEvery { this@FakeFetchAccountCryptoUseCase.invoke() }.delegateReturn()
|
||||
}
|
|
@ -4,12 +4,21 @@ import app.dapk.st.matrix.crypto.Olm
|
|||
import app.dapk.st.matrix.crypto.internal.RegisterOlmSessionUseCase
|
||||
import app.dapk.st.matrix.device.internal.DeviceKeys
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import test.Returns
|
||||
import test.delegateReturn
|
||||
|
||||
internal class FakeRegisterOlmSessionUseCase : RegisterOlmSessionUseCase by mockk() {
|
||||
fun givenRegistersSessions(devices: List<DeviceKeys>, account: Olm.AccountCryptoSession): Returns<List<Olm.DeviceCryptoSession>> {
|
||||
return coEvery { this@FakeRegisterOlmSessionUseCase.invoke(devices, account) }.delegateReturn()
|
||||
|
||||
fun givenRegistersSessions(devices: List<DeviceKeys>, account: Olm.AccountCryptoSession) = coEvery {
|
||||
this@FakeRegisterOlmSessionUseCase.invoke(devices, account)
|
||||
}.delegateReturn()
|
||||
|
||||
fun verifyRegistersKeys(devices: List<DeviceKeys>, account: Olm.AccountCryptoSession) {
|
||||
coVerify { this@FakeRegisterOlmSessionUseCase.invoke(devices, account) }
|
||||
}
|
||||
|
||||
fun verifyNoInteractions() {
|
||||
coVerify(exactly = 0) { this@FakeRegisterOlmSessionUseCase.invoke(any(), any()) }
|
||||
}
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.matrix.common.CipherText
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.UserCredentials
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.crypto.Olm
|
||||
import app.dapk.st.matrix.device.DeviceService
|
||||
import app.dapk.st.matrix.device.internal.DeviceKeys
|
||||
|
@ -66,4 +63,11 @@ class FakeOlm : Olm by mockk() {
|
|||
}
|
||||
|
||||
fun givenDeviceCrypto(input: Olm.OlmSessionInput, account: Olm.AccountCryptoSession) = coEvery { ensureDeviceCrypto(input, account) }.delegateReturn()
|
||||
|
||||
fun givenDecrypting(payload: EncryptedMessageContent.MegOlmV1) = coEvery { decryptMegOlm(payload.sessionId, payload.cipherText) }
|
||||
|
||||
fun givenDecrypting(payload: EncryptedMessageContent.OlmV1, account: Olm.AccountCryptoSession) = coEvery {
|
||||
val cipherForAccount = payload.cipherText[account.senderKey]!!
|
||||
decryptOlm(account, payload.senderKey, cipherForAccount.type, cipherForAccount.body)
|
||||
}.delegateReturn()
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.matrix.common.SessionId
|
||||
import app.dapk.st.matrix.common.SyncToken
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import app.dapk.st.matrix.device.DeviceService
|
||||
import app.dapk.st.matrix.device.internal.DeviceKeys
|
||||
|
@ -16,8 +17,10 @@ class FakeDeviceService : DeviceService by mockk() {
|
|||
}
|
||||
|
||||
fun verifyDidntUploadOneTimeKeys() {
|
||||
coVerify(exactly = 0) { this@FakeDeviceService.uploadOneTimeKeys(DeviceService.OneTimeKeys(any())) }
|
||||
coVerify(exactly = 0) { uploadOneTimeKeys(DeviceService.OneTimeKeys(any())) }
|
||||
}
|
||||
|
||||
fun givenClaimsKeys(claims: List<DeviceService.KeyClaim>) = coEvery { claimKeys(claims) }.delegateReturn()
|
||||
|
||||
fun givenFetchesDevices(userIds: List<UserId>, syncToken: SyncToken?) = coEvery { fetchDevices(userIds, syncToken) }.delegateReturn()
|
||||
}
|
|
@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldHaveSize
|
||||
import org.amshove.kluent.shouldNotBeEqualTo
|
||||
import org.junit.jupiter.api.MethodOrderer
|
||||
import org.junit.jupiter.api.Order
|
||||
|
@ -108,7 +107,7 @@ class SmokeTest {
|
|||
stream.importRoomKeys(password = "aaaaaa")
|
||||
}
|
||||
|
||||
result shouldBeEqualTo listOf(RoomId(value="!qOSENTtFUuCEKJSVzl:matrix.org"))
|
||||
result shouldBeEqualTo listOf(RoomId(value = "!qOSENTtFUuCEKJSVzl:matrix.org"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue