more tests

This commit is contained in:
Adam Brown 2022-03-03 20:57:13 +00:00
parent 634fc15829
commit ecb24ece11
30 changed files with 357 additions and 218 deletions

View File

@ -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,

View File

@ -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'
}

View File

@ -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)
}

View File

@ -12,4 +12,4 @@ fun <T, B> MockKStubScope<T, B>.delegateReturn(): Returns<T> = Returns { value -
fun interface Returns<T> {
fun returns(value: T)
}
}

View File

@ -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}"

View File

@ -49,6 +49,7 @@ data class SpiderPage<T>(
val label: String,
val parent: Route<*>?,
val state: T,
val hasToolbar: Boolean = true,
)
@JvmInline

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}
}
}

View File

@ -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(),

View File

@ -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(
//
//)

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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",
)
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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]

View File

@ -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")
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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()) }
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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"))
}
}