Improve view events and add unit tests

This commit is contained in:
Florian Renaud 2023-02-10 15:29:56 +01:00
parent 71455706cb
commit 25d49806cc
5 changed files with 328 additions and 19 deletions

View file

@ -19,6 +19,19 @@ package im.vector.app.features.settings.notifications
import im.vector.app.core.platform.VectorViewEvents
sealed interface VectorSettingsPushRuleNotificationViewEvent : VectorViewEvents {
data class PushRuleUpdated(val ruleId: String, val checked: Boolean) : VectorSettingsPushRuleNotificationViewEvent
data class Failure(val throwable: Throwable) : VectorSettingsPushRuleNotificationViewEvent
/**
* A global push rule checked state has changed.
*
* @property ruleId the global rule id which has been updated.
* @property checked whether the global rule is checked.
* @property failure whether there has been a failure when updating the global rule (ie. a sub rule has not been updated).
*/
data class PushRuleUpdated(val ruleId: String, val checked: Boolean, val failure: Throwable? = null) : VectorSettingsPushRuleNotificationViewEvent
/**
* A failure has occurred.
*
* @property throwable the related exception, if any.
*/
data class Failure(val throwable: Throwable?) : VectorSettingsPushRuleNotificationViewEvent
}

View file

@ -83,9 +83,19 @@ class VectorSettingsPushRuleNotificationViewModel @AssistedInject constructor(
}
}
setState { copy(isLoading = false) }
val failure = results.firstNotNullOfOrNull { it.exceptionOrNull() }
if (failure == null) {
_viewEvents.post(PushRuleUpdated(ruleId, checked))
val failure = results.firstNotNullOfOrNull { result ->
// If the failure is a rule not found error, do not consider it
result.exceptionOrNull()?.takeUnless { it is ServerError && it.error.code == MatrixError.M_NOT_FOUND }
}
val newChecked = if (checked) {
// If any rule is checked, the global rule is checked
results.any { it.isSuccess }
} else {
// If any rule has not been unchecked, the global rule remains checked
failure != null
}
if (results.any { it.isSuccess }) {
_viewEvents.post(PushRuleUpdated(ruleId, newChecked, failure))
} else {
_viewEvents.post(Failure(failure))
}
@ -93,18 +103,11 @@ class VectorSettingsPushRuleNotificationViewModel @AssistedInject constructor(
}
private suspend fun updatePushRule(kind: RuleKind, ruleId: String, enable: Boolean, newActions: List<Action>?) {
try {
activeSessionHolder.getSafeActiveSession()?.pushRuleService()?.updatePushRuleActions(
kind = kind,
ruleId = ruleId,
enable = enable,
actions = newActions
)
} catch (failure: ServerError) {
// Ignore the error if the rule id is not known from the server
if (failure.error.code != MatrixError.M_NOT_FOUND) {
throw failure
}
}
activeSessionHolder.getSafeActiveSession()?.pushRuleService()?.updatePushRuleActions(
kind = kind,
ruleId = ruleId,
enable = enable,
actions = newActions
)
}
}

View file

@ -0,0 +1,258 @@
/*
* 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 im.vector.app.features.settings.notifications
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.test
import im.vector.app.test.testDispatcher
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.session.pushrules.RuleIds
import org.matrix.android.sdk.api.session.pushrules.rest.PushRuleAndKind
internal class VectorSettingsPushRuleNotificationViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher)
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakePushRuleService = fakeActiveSessionHolder.fakeSession.fakePushRuleService
private val initialState = VectorSettingsPushRuleNotificationViewState()
private fun createViewModel() = VectorSettingsPushRuleNotificationViewModel(
initialState = initialState,
activeSessionHolder = fakeActiveSessionHolder.instance,
)
@Before
fun setup() {
mockkStatic("im.vector.app.features.settings.notifications.NotificationIndexKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a ruleId, when the rule is checked or unchecked, then the related rules are also updated and a view event is posted`() = runTest {
// Given
val viewModel = createViewModel()
val firstRuleId = RuleIds.RULE_ID_ONE_TO_ONE_ROOM
val secondRuleId = RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS
fakePushRuleService.givenUpdatePushRuleActionsSucceed()
// When
val viewModelTest = viewModel.test()
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(firstRuleId), true))
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(secondRuleId), false))
// Then
coVerifyOrder {
// first rule id
fakePushRuleService.updatePushRuleActions(any(), firstRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE_UNSTABLE, any(), any())
// second rule id
fakePushRuleService.updatePushRuleActions(any(), secondRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_UNSTABLE, any(), any())
}
viewModelTest
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) },
{ copy(isLoading = true) },
{ copy(isLoading = false) },
)
.assertEvents(
VectorSettingsPushRuleNotificationViewEvent.PushRuleUpdated(RuleIds.RULE_ID_ONE_TO_ONE_ROOM, true),
VectorSettingsPushRuleNotificationViewEvent.PushRuleUpdated(RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS, false),
)
.finish()
}
@Test
fun `given a ruleId, when the rule is checked and an error is thrown, then all the related rules are updated and an event is posted with the failure`() = runTest {
// Given
val viewModel = createViewModel()
val failure = mockk<Throwable>()
val firstRuleId = RuleIds.RULE_ID_ONE_TO_ONE_ROOM
fakePushRuleService.givenUpdatePushRuleActionsSucceed()
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, failure)
val secondRuleId = RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS
fakePushRuleService.givenUpdatePushRuleActionsFail(secondRuleId, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START_UNSTABLE, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_END, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_END_UNSTABLE, failure)
// When
val viewModelTest = viewModel.test()
// One rule failed to update
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(firstRuleId), true))
// All the rules failed to update
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(secondRuleId), true))
// Then
coVerifyOrder {
// first rule id
fakePushRuleService.updatePushRuleActions(any(), firstRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE_UNSTABLE, any(), any())
// second rule id
fakePushRuleService.updatePushRuleActions(any(), secondRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_UNSTABLE, any(), any())
}
viewModelTest
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) },
{ copy(isLoading = true) },
{ copy(isLoading = false) },
)
.assertEvents(
VectorSettingsPushRuleNotificationViewEvent.PushRuleUpdated(RuleIds.RULE_ID_ONE_TO_ONE_ROOM, true, failure),
VectorSettingsPushRuleNotificationViewEvent.Failure(failure),
)
.finish()
}
@Test
fun `given a ruleId, when the rule is unchecked and an error is thrown, then all the related rules are updated and an event is posted with the failure`() = runTest {
// Given
val viewModel = createViewModel()
val failure = mockk<Throwable>()
val firstRuleId = RuleIds.RULE_ID_ONE_TO_ONE_ROOM
fakePushRuleService.givenUpdatePushRuleActionsSucceed()
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, failure)
val secondRuleId = RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS
fakePushRuleService.givenUpdatePushRuleActionsFail(secondRuleId, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_START_UNSTABLE, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_END, failure)
fakePushRuleService.givenUpdatePushRuleActionsFail(RuleIds.RULE_ID_POLL_END_UNSTABLE, failure)
// When
val viewModelTest = viewModel.test()
// One rule failed to update
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(firstRuleId), false))
// All the rules failed to update
viewModel.handle(VectorSettingsPushRuleNotificationViewAction.UpdatePushRule(givenARuleId(secondRuleId), false))
// Then
coVerifyOrder {
// first rule id
fakePushRuleService.updatePushRuleActions(any(), firstRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_ONE_TO_ONE_UNSTABLE, any(), any())
// second rule id
fakePushRuleService.updatePushRuleActions(any(), secondRuleId, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_START_UNSTABLE, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END, any(), any())
fakePushRuleService.updatePushRuleActions(any(), RuleIds.RULE_ID_POLL_END_UNSTABLE, any(), any())
}
viewModelTest
.assertStatesChanges(
initialState,
{ copy(isLoading = true) },
{ copy(isLoading = false) },
{ copy(isLoading = true) },
{ copy(isLoading = false) },
)
.assertEvents(
// The global rule remains checked if all the rules are not unchecked
VectorSettingsPushRuleNotificationViewEvent.PushRuleUpdated(RuleIds.RULE_ID_ONE_TO_ONE_ROOM, true, failure),
VectorSettingsPushRuleNotificationViewEvent.Failure(failure),
)
.finish()
}
@Test
fun `given a rule id, when requesting the check state, returns the expected value according to the related rules`() {
// Given
val viewModel = createViewModel()
val firstRuleId = RuleIds.RULE_ID_ONE_TO_ONE_ROOM
givenARuleId(firstRuleId, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_START_ONE_TO_ONE, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_START_ONE_TO_ONE_UNSTABLE, NotificationIndex.SILENT)
givenARuleId(RuleIds.RULE_ID_POLL_END_ONE_TO_ONE, NotificationIndex.NOISY)
givenARuleId(RuleIds.RULE_ID_POLL_END_ONE_TO_ONE_UNSTABLE, NotificationIndex.OFF)
val secondRuleId = RuleIds.RULE_ID_ALL_OTHER_MESSAGES_ROOMS
givenARuleId(secondRuleId, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_START, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_START_UNSTABLE, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_END, NotificationIndex.OFF)
givenARuleId(RuleIds.RULE_ID_POLL_END_UNSTABLE, NotificationIndex.OFF)
// When
val firstResult = viewModel.isPushRuleChecked(firstRuleId)
val secondResult = viewModel.isPushRuleChecked(secondRuleId)
// Then
firstResult shouldBe true
secondResult shouldBe false
}
private fun givenARuleId(ruleId: String, notificationIndex: NotificationIndex = NotificationIndex.NOISY): PushRuleAndKind {
val ruleAndKind = mockk<PushRuleAndKind> {
every { pushRule.ruleId } returns ruleId
every { pushRule.notificationIndex } returns notificationIndex
every { kind } returns mockk()
}
every { fakePushRuleService.getPushRules().findDefaultRule(ruleId) } returns ruleAndKind
return ruleAndKind
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import io.mockk.coEvery
import io.mockk.coJustRun
import io.mockk.mockk
import org.matrix.android.sdk.api.session.pushrules.PushRuleService
class FakePushRuleService : PushRuleService by mockk(relaxed = true) {
fun givenUpdatePushRuleActionsSucceed(ruleId: String? = null) {
coJustRun { updatePushRuleActions(any(), ruleId ?: any(), any(), any()) }
}
fun givenUpdatePushRuleActionsFail(ruleId: String? = null, failure: Throwable = mockk()) {
coEvery { updatePushRuleActions(any(), ruleId ?: any(), any(), any()) }.throws(failure)
}
}

View file

@ -41,10 +41,11 @@ class FakeSession(
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
val fakeRoomService: FakeRoomService = FakeRoomService(),
val fakePushRuleService: FakePushRuleService = FakePushRuleService(),
val fakePushersService: FakePushersService = FakePushersService(),
val fakeUserService: FakeUserService = FakeUserService(),
private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService()
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService(),
) : Session by mockk(relaxed = true) {
init {
@ -61,6 +62,7 @@ class FakeSession(
override fun sharedSecretStorageService() = fakeSharedSecretStorageService
override fun roomService() = fakeRoomService
override fun eventService() = fakeEventService
override fun pushRuleService() = fakePushRuleService
override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService
override fun userService() = fakeUserService