Add tests, mocks and lint

This commit is contained in:
David Langley
2023-09-13 12:44:22 +01:00
parent 7d95feadc1
commit ccd684ea40
24 changed files with 523 additions and 96 deletions

View File

@@ -41,6 +41,7 @@ dependencies {
implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.network)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.pushstore.test)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api)

View File

@@ -41,7 +41,6 @@ import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)

View File

@@ -16,16 +16,12 @@
package io.element.android.features.preferences.impl.notifications
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
sealed interface NotificationSettingsEvents {
data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents
data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents
data class SetDefaultGroupNotificationMode(val mode: RoomNotificationMode) : NotificationSettingsEvents
data class SetDefaultOneToOneNotificationMode(val mode: RoomNotificationMode) : NotificationSettingsEvents
data object FixConfigurationMismatch : NotificationSettingsEvents
data object ClearConfigurationMismatchError : NotificationSettingsEvents
}

View File

@@ -26,7 +26,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.UserId
@ContributesNode(SessionScope::class)
class NotificationSettingsNode @AssistedInject constructor(

View File

@@ -56,8 +56,8 @@ class NotificationSettingsPresenter @Inject constructor(
.getNotificationEnabledForDevice()
.collectAsState(initial = false)
val matrixSettings: MutableState<NotificationSettingsState.MatrixNotificationSettings> = remember {
mutableStateOf(NotificationSettingsState.MatrixNotificationSettings.Uninitialized)
val matrixSettings: MutableState<NotificationSettingsState.MatrixSettings> = remember {
mutableStateOf(NotificationSettingsState.MatrixSettings.Uninitialized)
}
LaunchedEffect(Unit) {
@@ -69,18 +69,20 @@ class NotificationSettingsPresenter @Inject constructor(
when (event) {
is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled)
is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled)
is NotificationSettingsEvents.SetDefaultGroupNotificationMode -> localCoroutineScope.setDefaultGroupNotificationMode(event.mode)
is NotificationSettingsEvents.SetDefaultOneToOneNotificationMode -> localCoroutineScope.setDefaultOneToOneNotificationMode(event.mode)
is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled)
NotificationSettingsEvents.ClearConfigurationMismatchError -> matrixSettings.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = false)
NotificationSettingsEvents.ClearConfigurationMismatchError -> {
matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
}
NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings)
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> {
systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled()
}
}
}
return NotificationSettingsState(
matrixNotificationSettings = matrixSettings.value,
appNotificationSettings = NotificationSettingsState.AppNotificationSettings(
matrixSettings = matrixSettings.value,
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = systemNotificationsEnabled.value,
appNotificationsEnabled = appNotificationsEnabled.value
),
@@ -89,7 +91,7 @@ class NotificationSettingsPresenter @Inject constructor(
}
@OptIn(FlowPreview::class)
private fun CoroutineScope.observeNotificationSettings(target: MutableState<NotificationSettingsState.MatrixNotificationSettings>) {
private fun CoroutineScope.observeNotificationSettings(target: MutableState<NotificationSettingsState.MatrixSettings>) {
notificationSettingsService.notificationSettingsChangeFlow
.debounce(0.5.seconds)
.onEach {
@@ -98,7 +100,7 @@ class NotificationSettingsPresenter @Inject constructor(
.launchIn(this)
}
private fun CoroutineScope.fetchSettings(target: MutableState<NotificationSettingsState.MatrixNotificationSettings>) = launch {
private fun CoroutineScope.fetchSettings(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
@@ -106,14 +108,14 @@ class NotificationSettingsPresenter @Inject constructor(
val encryptedOneToOneDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = true).getOrThrow()
if(groupDefaultMode != encryptedGroupDefaultMode || oneToOneDefaultMode != encryptedOneToOneDefaultMode) {
target.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = false)
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false)
return@launch
}
val callNotificationsEnabled = notificationSettingsService.isCallEnabled().getOrThrow()
val atRoomNotificationsEnabled = notificationSettingsService.isRoomMentionEnabled().getOrThrow()
target.value = NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState(
target.value = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = atRoomNotificationsEnabled,
callNotificationsEnabled = callNotificationsEnabled,
defaultGroupNotificationMode = encryptedGroupDefaultMode,
@@ -121,7 +123,7 @@ class NotificationSettingsPresenter @Inject constructor(
)
}
private fun CoroutineScope.fixConfigurationMismatch(target: MutableState<NotificationSettingsState.MatrixNotificationSettings>) = launch {
private fun CoroutineScope.fixConfigurationMismatch(target: MutableState<NotificationSettingsState.MatrixSettings>) = launch {
runCatching {
val groupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false).getOrThrow()
val encryptedGroupDefaultMode = notificationSettingsService.getDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false).getOrThrow()
@@ -147,7 +149,7 @@ class NotificationSettingsPresenter @Inject constructor(
}.fold(
onSuccess = {},
onFailure = {
target.value = NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState(fixFailed = true)
target.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = true)
}
)
}
@@ -160,27 +162,7 @@ class NotificationSettingsPresenter @Inject constructor(
notificationSettingsService.setCallEnabled(enabled)
}
private fun CoroutineScope.setDefaultGroupNotificationMode(mode: RoomNotificationMode) = launch {
notificationSettingsService.setDefaultRoomNotificationMode(false, mode, false)
notificationSettingsService.setDefaultRoomNotificationMode(true, mode, false)
}
private fun CoroutineScope.setDefaultOneToOneNotificationMode(mode: RoomNotificationMode) = launch {
notificationSettingsService.setDefaultRoomNotificationMode(false, mode, true)
notificationSettingsService.setDefaultRoomNotificationMode(true, mode, true)
}
private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch {
userPushStore.setNotificationEnabledForDevice(enabled)
}
private fun CoroutineScope.getDefaultRoomNotificationMode(isOneToOne: Boolean, defaultRoomNotificationMode: MutableState<RoomNotificationMode?>) = launch {
val encryptedMode = notificationSettingsService.getDefaultRoomNotificationMode(true, isOneToOne).getOrThrow()
val unencryptedMode = notificationSettingsService.getDefaultRoomNotificationMode(false, isOneToOne).getOrThrow()
if (encryptedMode == unencryptedMode) {
defaultRoomNotificationMode.value
} else {
defaultRoomNotificationMode.value = null
}
}
}

View File

@@ -21,25 +21,25 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@Immutable
data class NotificationSettingsState(
val matrixNotificationSettings: MatrixNotificationSettings,
val appNotificationSettings: AppNotificationSettings,
val matrixSettings: MatrixSettings,
val appSettings: AppSettings,
val eventSink: (NotificationSettingsEvents) -> Unit,
) {
sealed interface MatrixNotificationSettings {
data object Uninitialized : MatrixNotificationSettings
data class ValidNotificationSettingsState(
sealed interface MatrixSettings {
data object Uninitialized : MatrixSettings
data class Valid(
val atRoomNotificationsEnabled: Boolean,
val callNotificationsEnabled: Boolean,
val defaultGroupNotificationMode: RoomNotificationMode?,
val defaultOneToOneNotificationMode: RoomNotificationMode?,
) : MatrixNotificationSettings
) : MatrixSettings
data class InvalidNotificationSettingsState(
data class Invalid(
val fixFailed: Boolean
) : MatrixNotificationSettings
) : MatrixSettings
}
data class AppNotificationSettings(
data class AppSettings(
val systemNotificationsEnabled: Boolean,
val appNotificationsEnabled: Boolean,
)

View File

@@ -27,13 +27,13 @@ open class NotificationSettingsStateProvider : PreviewParameterProvider<Notifica
}
fun aNotificationSettingsState() = NotificationSettingsState(
matrixNotificationSettings = NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState(
matrixSettings = NotificationSettingsState.MatrixSettings.Valid(
atRoomNotificationsEnabled = true,
callNotificationsEnabled = true,
defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES,
),
appNotificationSettings = NotificationSettingsState.AppNotificationSettings(
appSettings = NotificationSettingsState.AppSettings(
systemNotificationsEnabled = false,
appNotificationsEnabled = true,
),

View File

@@ -74,23 +74,21 @@ fun NotificationSettingsView(
title = stringResource(id = CommonStrings.screen_notification_settings_title)
) {
when (state.matrixNotificationSettings) {
is NotificationSettingsState.MatrixNotificationSettings.InvalidNotificationSettingsState -> InvalidNotificationSettingsView(
showError = state.matrixNotificationSettings.fixFailed,
when (state.matrixSettings) {
is NotificationSettingsState.MatrixSettings.Invalid -> InvalidNotificationSettingsView(
showError = state.matrixSettings.fixFailed,
onContinueClicked = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) },
onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) },
modifier = modifier,
)
NotificationSettingsState.MatrixNotificationSettings.Uninitialized -> return@PreferenceView
is NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState -> NotificationSettingsContentView(
matrixSettings = state.matrixNotificationSettings,
systemSettings = state.appNotificationSettings,
NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferenceView
is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView(
matrixSettings = state.matrixSettings,
systemSettings = state.appSettings,
onNotificationsEnabledChanged = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it))},
onGroupChatsClicked = { onOpenEditDefault(false) },
onDirectChatsClicked = { onOpenEditDefault(true) },
onMentionNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) },
onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) },
modifier = modifier,
)
}
}
@@ -98,8 +96,8 @@ fun NotificationSettingsView(
@Composable
private fun NotificationSettingsContentView(
matrixSettings: NotificationSettingsState.MatrixNotificationSettings.ValidNotificationSettingsState,
systemSettings: NotificationSettingsState.AppNotificationSettings,
matrixSettings: NotificationSettingsState.MatrixSettings.Valid,
systemSettings: NotificationSettingsState.AppSettings,
onNotificationsEnabledChanged: (Boolean) -> Unit,
onGroupChatsClicked: () -> Unit,
onDirectChatsClicked: () -> Unit,
@@ -252,16 +250,16 @@ private fun ContentToPreview(state: NotificationSettingsState) {
@Preview
@Composable
internal fun InvalidNotificationSettingsViewightPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) =
ElementPreviewLight { InvalidNotificationSettingsContentToPreview(state) }
internal fun InvalidNotificationSettingsViewightPreview() =
ElementPreviewLight { InvalidNotificationSettingsContentToPreview() }
@Preview
@Composable
internal fun InvalidNotificationSettingsViewDarkPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) =
ElementPreviewDark { InvalidNotificationSettingsContentToPreview(state) }
internal fun InvalidNotificationSettingsViewDarkPreview() =
ElementPreviewDark { InvalidNotificationSettingsContentToPreview() }
@Composable
private fun InvalidNotificationSettingsContentToPreview(state: NotificationSettingsState) {
private fun InvalidNotificationSettingsContentToPreview() {
InvalidNotificationSettingsView(
showError = false,
onContinueClicked = {},

View File

@@ -26,7 +26,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.coroutines.CoroutineScope

View File

@@ -55,7 +55,7 @@ fun EditDefaultNotificationSettingView(
PreferenceCategory(title = stringResource(id = categoryTitle)) {
if (state.mode != null) {
Column(modifier = modifier.selectableGroup()) {
Column(modifier = Modifier.selectableGroup()) {
validModes.forEach { item ->
DefaultNotificationSettingOption(
mode = item,

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Test
class EditDefaultNotificationSettingsPresenterTests {
@Test
fun `present - ensures initial state is correct`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.mode).isNull()
Truth.assertThat(initialState.isOneToOne).isFalse()
val loadedState = consumeItemsUntilPredicate {
it.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
}.last()
Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
}
}
@Test
fun `present - edit default notification setting`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES))
val loadedState = consumeItemsUntilPredicate {
it.mode == RoomNotificationMode.ALL_MESSAGES
}.last()
Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
class FakeSystemNotificationsEnabledProvider: SystemNotificationsEnabledProvider {
override fun notificationsEnabled(): Boolean {
return true
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.impl.notifications
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import com.google.common.truth.Truth
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.time.Duration.Companion.milliseconds
class NotificationSettingsPresenterTests {
@Test
fun `present - ensures initial state is correct`() = runTest {
val presenter = aNotificationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.appSettings.appNotificationsEnabled).isFalse()
Truth.assertThat(initialState.appSettings.systemNotificationsEnabled).isTrue()
Truth.assertThat(initialState.matrixSettings).isEqualTo(NotificationSettingsState.MatrixSettings.Uninitialized)
val loadedState = consumeItemsUntilPredicate {
it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid
}.last()
Truth.assertThat(loadedState.appSettings.appNotificationsEnabled).isTrue()
Truth.assertThat(loadedState.appSettings.systemNotificationsEnabled).isTrue()
val valid = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(valid?.atRoomNotificationsEnabled).isFalse()
Truth.assertThat(valid?.callNotificationsEnabled).isFalse()
Truth.assertThat(valid?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
Truth.assertThat(valid?.defaultOneToOneNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - default group notification mode changed`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = aNotificationPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES)
notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, isOneToOne = false, mode = RoomNotificationMode.ALL_MESSAGES)
val updatedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)
?.defaultGroupNotificationMode == RoomNotificationMode.ALL_MESSAGES
}.last()
val valid = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(valid?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
}
}
@Test
fun `present - notification settings mismatched`() = runTest {
val notificationSettingsService = FakeNotificationSettingsService()
val presenter = aNotificationPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = true,
isOneToOne = false,
mode = RoomNotificationMode.ALL_MESSAGES
)
notificationSettingsService.setDefaultRoomNotificationMode(
isEncrypted = false,
isOneToOne = false,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
val updatedState = consumeItemsUntilPredicate {
it.matrixSettings is NotificationSettingsState.MatrixSettings.Invalid
}.last()
Truth.assertThat(updatedState.matrixSettings).isEqualTo(NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false))
}
}
@Test
fun `present - fix notification settings mismatched`() = runTest {
// Start with a mismatched configuration
val notificationSettingsService = FakeNotificationSettingsService(
initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES,
initialGroupDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialEncryptedOneToOneDefaultMode = RoomNotificationMode.ALL_MESSAGES,
initialOneToOneDefaultMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
)
val presenter = aNotificationPresenter(notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationSettingsEvents.FixConfigurationMismatch)
val fixedState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) {
it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid
}.last()
val fixedMatrixState = fixedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(fixedMatrixState?.defaultGroupNotificationMode).isEqualTo(RoomNotificationMode.ALL_MESSAGES)
}
}
@Test
fun `present - set notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val loadedState = consumeItemsUntilPredicate {
it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid
}.last()
Truth.assertThat(loadedState.appSettings.appNotificationsEnabled).isTrue()
loadedState.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(false))
val updatedState = consumeItemsUntilPredicate {
!it.appSettings.appNotificationsEnabled
}.last()
Truth.assertThat(updatedState.appSettings.appNotificationsEnabled).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - set call notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == false
}.last()
val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(validMatrixState?.callNotificationsEnabled).isFalse()
loadedState.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(true))
val updatedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == true
}.last()
val updatedMatrixState = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(updatedMatrixState?.callNotificationsEnabled).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - set atRoom notifications enabled`() = runTest {
val presenter = aNotificationPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val loadedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == false
}.last()
val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(validMatrixState?.atRoomNotificationsEnabled).isFalse()
loadedState.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(true))
val updatedState = consumeItemsUntilPredicate {
(it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == true
}.last()
val updatedMatrixState = updatedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid
Truth.assertThat(updatedMatrixState?.atRoomNotificationsEnabled).isTrue()
cancelAndIgnoreRemainingEvents()
}
}
private fun aNotificationPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService()
) : NotificationSettingsPresenter {
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
return NotificationSettingsPresenter(
notificationSettingsService = notificationSettingsService,
userPushStoreFactory = FakeUserPushStoreFactory(),
matrixClient = matrixClient,
systemNotificationsEnabledProvider = FakeSystemNotificationsEnabledProvider(),
)
}
}

View File

@@ -24,6 +24,7 @@ import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_USER_NAME
@@ -53,6 +54,7 @@ class PreferencesRootPresenterTest {
BuildType.DEBUG,
FakeVersionFormatter(),
SnackbarDispatcher(),
FakeFeatureFlagService()
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()

View File

@@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

View File

@@ -385,7 +385,7 @@ class RoomDetailsPresenterTests {
@Test
fun `present - mute room notifications`() = runTest {
val leaveRoomPresenter = LeaveRoomPresenterFake()
val notificationSettingsService = FakeNotificationSettingsService(initialMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService)
moleculeFlow(RecompositionMode.Immediate) {
@@ -404,8 +404,8 @@ class RoomDetailsPresenterTests {
fun `present - unmute room notifications`() = runTest {
val leaveRoomPresenter = LeaveRoomPresenterFake()
val notificationSettingsService = FakeNotificationSettingsService(
initialMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialDefaultMode = RoomNotificationMode.ALL_MESSAGES
initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES
)
val room = aMatrixRoom(notificationSettingsService = notificationSettingsService)
val presenter = aRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService)

View File

@@ -24,7 +24,6 @@ import io.element.android.features.roomdetails.aMatrixRoom
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsEvents
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -67,9 +66,9 @@ class RoomNotificationSettingsPresenterTests {
initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY))
initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true))
val defaultState = consumeItemsUntilPredicate {
it.roomNotificationSettings?.mode == A_ROOM_NOTIFICATION_MODE
it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY
}.last()
Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(A_ROOM_NOTIFICATION_MODE)
Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)
}
}

View File

@@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettin
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.withContext
interface NotificationSettingsService {
/**

View File

@@ -57,7 +57,11 @@ class RustNotificationSettingsService(
notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode)
}
override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result<Unit> = withContext(dispatchers.io) {
override suspend fun setDefaultRoomNotificationMode(
isEncrypted: Boolean,
mode: RoomNotificationMode,
isOneToOne: Boolean
): Result<Unit> = withContext(dispatchers.io) {
runCatching {
notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode))
}

View File

@@ -25,32 +25,75 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
class FakeNotificationSettingsService(
initialMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE,
initialDefaultMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE
initialRoomMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE,
initialGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
initialEncryptedOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
) : NotificationSettingsService {
private var _roomNotificationSettingsStateFlow = MutableStateFlow(Unit)
private var defaultRoomNotificationMode: RoomNotificationMode = initialDefaultMode
private var roomNotificationMode: RoomNotificationMode = initialMode
private var _notificationSettingsStateFlow = MutableStateFlow(Unit)
private var defaultGroupRoomNotificationMode: RoomNotificationMode = initialGroupDefaultMode
private var defaultEncryptedGroupRoomNotificationMode: RoomNotificationMode = initialEncryptedGroupDefaultMode
private var defaultOneToOneRoomNotificationMode: RoomNotificationMode = initialOneToOneDefaultMode
private var defaultEncryptedOneToOneRoomNotificationMode: RoomNotificationMode = initialEncryptedOneToOneDefaultMode
private var roomNotificationMode: RoomNotificationMode = initialRoomMode
private var callNotificationsEnabled = false
private var atRoomNotificationsEnabled = false
override val notificationSettingsChangeFlow: SharedFlow<Unit>
get() = _roomNotificationSettingsStateFlow
get() = _notificationSettingsStateFlow
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationSettings> {
return Result.success(RoomNotificationSettings(mode = roomNotificationMode, isDefault = roomNotificationMode == defaultRoomNotificationMode))
override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationSettings> {
return Result.success(
RoomNotificationSettings(
mode = roomNotificationMode,
isDefault = roomNotificationMode == defaultEncryptedGroupRoomNotificationMode
)
)
}
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, membersCount: Long): Result<RoomNotificationMode> {
return Result.success(defaultRoomNotificationMode)
override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result<RoomNotificationMode> {
return if (isOneToOne) {
if (isEncrypted) {
Result.success(defaultEncryptedOneToOneRoomNotificationMode)
} else {
Result.success(defaultOneToOneRoomNotificationMode)
}
} else {
if (isEncrypted) {
Result.success(defaultEncryptedGroupRoomNotificationMode)
} else {
Result.success(defaultGroupRoomNotificationMode)
}
}
}
override suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isOneToOne: Boolean): Result<Unit> {
if (isOneToOne) {
if (isEncrypted) {
defaultEncryptedOneToOneRoomNotificationMode = mode
} else {
defaultOneToOneRoomNotificationMode = mode
}
} else {
if (isEncrypted) {
defaultEncryptedGroupRoomNotificationMode = mode
} else {
defaultGroupRoomNotificationMode = mode
}
}
_notificationSettingsStateFlow.emit(Unit)
return Result.success(Unit)
}
override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result<Unit> {
roomNotificationMode = mode
_roomNotificationSettingsStateFlow.emit(Unit)
_notificationSettingsStateFlow.emit(Unit)
return Result.success(Unit)
}
override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result<Unit> {
roomNotificationMode = defaultRoomNotificationMode
_roomNotificationSettingsStateFlow.emit(Unit)
roomNotificationMode = defaultEncryptedGroupRoomNotificationMode
_notificationSettingsStateFlow.emit(Unit)
return Result.success(Unit)
}
@@ -58,23 +101,25 @@ class FakeNotificationSettingsService(
return setRoomNotificationMode(roomId, RoomNotificationMode.MUTE)
}
override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, membersCount: Long): Result<Unit> {
override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result<Unit> {
return restoreDefaultRoomNotificationMode(roomId)
}
override suspend fun isRoomMentionEnabled(): Result<Boolean> {
return Result.success(false)
return Result.success(atRoomNotificationsEnabled)
}
override suspend fun setRoomMentionEnabled(enabled: Boolean): Result<Unit> {
atRoomNotificationsEnabled = enabled
return Result.success(Unit)
}
override suspend fun isCallEnabled(): Result<Boolean> {
return Result.success(false)
return Result.success(callNotificationsEnabled)
}
override suspend fun setCallEnabled(enabled: Boolean): Result<Unit> {
callNotificationsEnabled = enabled
return Result.success(Unit)
}
}

View File

@@ -72,7 +72,6 @@ class FakeMatrixRoom(
private var userAvatarUrlResult = Result.success<String?>(null)
private var updateMembersResult: Result<Unit> = Result.success(Unit)
private var joinRoomResult = Result.success(Unit)
private var updateRoomNotificationSettingsResult: Result<Unit> = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
private var canRedactResult = Result.success(canRedact)
@@ -149,7 +148,7 @@ class FakeMatrixRoom(
}
override suspend fun updateRoomNotificationSettings(): Result<Unit> = simulateLongTask {
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, activeMemberCount).getOrThrow()
val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow()
roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings)
return Result.success(Unit)
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.pushstore.test"
}
dependencies {
api(projects.libraries.matrix.api)
api(libs.coroutines.core)
implementation(libs.coroutines.test)
implementation(projects.tests.testutils)
implementation(projects.libraries.pushstore.api)
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.element.android.libraries.pushstore.test.userpushstore
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeUserPushStore: UserPushStore {
private var pushProviderName: String? = null
private var currentRegisteredPushKey: String? = null
private val notificationEnabledForDevice = MutableStateFlow(true)
override suspend fun getPushProviderName(): String? {
return pushProviderName
}
override suspend fun setPushProviderName(value: String) {
pushProviderName = value
}
override suspend fun getCurrentRegisteredPushKey(): String? {
return currentRegisteredPushKey
}
override suspend fun setCurrentRegisteredPushKey(value: String) {
currentRegisteredPushKey = value
}
override fun getNotificationEnabledForDevice(): Flow<Boolean> {
return notificationEnabledForDevice
}
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
notificationEnabledForDevice.value = enabled
}
override fun useCompleteNotificationFormat(): Boolean {
return true
}
override suspend fun reset() {
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.element.android.libraries.pushstore.test.userpushstore
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
class FakeUserPushStoreFactory: UserPushStoreFactory {
override fun create(userId: SessionId): UserPushStore {
return FakeUserPushStore()
}
}