Benoit Marty
4 months ago
7 changed files with 411 additions and 41 deletions
@ -0,0 +1,35 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.libraries.push.impl.push |
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
interface IncrementPushDataStore { |
||||||
|
suspend fun incrementPushCounter() |
||||||
|
} |
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class) |
||||||
|
class DefaultIncrementPushDataStore @Inject constructor( |
||||||
|
private val defaultPushDataStore: DefaultPushDataStore |
||||||
|
) : IncrementPushDataStore { |
||||||
|
override suspend fun incrementPushCounter() { |
||||||
|
defaultPushDataStore.incrementPushCounter() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.libraries.push.impl.push |
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding |
||||||
|
import io.element.android.libraries.di.AppScope |
||||||
|
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager |
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
interface OnNotifiableEventReceived { |
||||||
|
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) |
||||||
|
} |
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class) |
||||||
|
class DefaultOnNotifiableEventReceived @Inject constructor( |
||||||
|
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, |
||||||
|
) : OnNotifiableEventReceived { |
||||||
|
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { |
||||||
|
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.libraries.push.impl.notifications |
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId |
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent |
||||||
|
|
||||||
|
class FakeNotifiableEventResolver( |
||||||
|
private val notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> TODO() } |
||||||
|
) : NotifiableEventResolver { |
||||||
|
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { |
||||||
|
return notifiableEventResult(sessionId, roomId, eventId) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,267 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class) |
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.push |
||||||
|
|
||||||
|
import app.cash.turbine.test |
||||||
|
import io.element.android.libraries.core.meta.BuildMeta |
||||||
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService |
||||||
|
import io.element.android.libraries.matrix.api.core.EventId |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId |
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||||
|
import io.element.android.libraries.matrix.test.A_SECRET |
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID |
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService |
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta |
||||||
|
import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver |
||||||
|
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent |
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent |
||||||
|
import io.element.android.libraries.push.impl.test.DefaultTestPush |
||||||
|
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler |
||||||
|
import io.element.android.libraries.pushproviders.api.PushData |
||||||
|
import io.element.android.libraries.pushstore.api.UserPushStore |
||||||
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret |
||||||
|
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore |
||||||
|
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory |
||||||
|
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret |
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder |
||||||
|
import io.element.android.tests.testutils.lambda.value |
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi |
||||||
|
import kotlinx.coroutines.test.runTest |
||||||
|
import org.junit.Test |
||||||
|
|
||||||
|
class DefaultPushHandlerTest { |
||||||
|
@Test |
||||||
|
fun `when classical PushData is received, the notification drawer is informed`() = runTest { |
||||||
|
val aNotifiableMessageEvent = aNotifiableMessageEvent() |
||||||
|
val notifiableEventResult = |
||||||
|
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent } |
||||||
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} |
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {} |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = AN_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived, |
||||||
|
notifiableEventResult = notifiableEventResult, |
||||||
|
pushClientSecret = FakePushClientSecret( |
||||||
|
getUserIdFromSecretResult = { A_USER_ID } |
||||||
|
), |
||||||
|
incrementPushCounterResult = incrementPushCounterResult |
||||||
|
) |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
incrementPushCounterResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
notifiableEventResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) |
||||||
|
onNotifiableEventReceived.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
.with(value(aNotifiableMessageEvent)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when classical PushData is received, but notifications are disabled, nothing happen`() = |
||||||
|
runTest { |
||||||
|
val aNotifiableMessageEvent = aNotifiableMessageEvent() |
||||||
|
val notifiableEventResult = |
||||||
|
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent } |
||||||
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} |
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {} |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = AN_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived, |
||||||
|
notifiableEventResult = notifiableEventResult, |
||||||
|
pushClientSecret = FakePushClientSecret( |
||||||
|
getUserIdFromSecretResult = { A_USER_ID } |
||||||
|
), |
||||||
|
userPushStore = FakeUserPushStore().apply { |
||||||
|
setNotificationEnabledForDevice(false) |
||||||
|
}, |
||||||
|
incrementPushCounterResult = incrementPushCounterResult |
||||||
|
) |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
incrementPushCounterResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
notifiableEventResult.assertions() |
||||||
|
.isNeverCalled() |
||||||
|
onNotifiableEventReceived.assertions() |
||||||
|
.isNeverCalled() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when PushData is received, but client secret is not known, fallback the latest session`() = |
||||||
|
runTest { |
||||||
|
val aNotifiableMessageEvent = aNotifiableMessageEvent() |
||||||
|
val notifiableEventResult = |
||||||
|
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent } |
||||||
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} |
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {} |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = AN_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived, |
||||||
|
notifiableEventResult = notifiableEventResult, |
||||||
|
pushClientSecret = FakePushClientSecret( |
||||||
|
getUserIdFromSecretResult = { null } |
||||||
|
), |
||||||
|
matrixAuthenticationService = FakeAuthenticationService().apply { |
||||||
|
getLatestSessionIdLambda = { A_USER_ID } |
||||||
|
}, |
||||||
|
incrementPushCounterResult = incrementPushCounterResult |
||||||
|
) |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
incrementPushCounterResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
notifiableEventResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) |
||||||
|
onNotifiableEventReceived.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
.with(value(aNotifiableMessageEvent)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when PushData is received, but client secret is not known, and there is no latest session, nothing happen`() = |
||||||
|
runTest { |
||||||
|
val aNotifiableMessageEvent = aNotifiableMessageEvent() |
||||||
|
val notifiableEventResult = |
||||||
|
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent } |
||||||
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} |
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {} |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = AN_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived, |
||||||
|
notifiableEventResult = notifiableEventResult, |
||||||
|
pushClientSecret = FakePushClientSecret( |
||||||
|
getUserIdFromSecretResult = { null } |
||||||
|
), |
||||||
|
matrixAuthenticationService = FakeAuthenticationService().apply { |
||||||
|
getLatestSessionIdLambda = { null } |
||||||
|
}, |
||||||
|
incrementPushCounterResult = incrementPushCounterResult |
||||||
|
) |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
incrementPushCounterResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
notifiableEventResult.assertions() |
||||||
|
.isNeverCalled() |
||||||
|
onNotifiableEventReceived.assertions() |
||||||
|
.isNeverCalled() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when classical PushData is received, but not able to resolve the event, nothing happen`() = |
||||||
|
runTest { |
||||||
|
val notifiableEventResult = |
||||||
|
lambdaRecorder<SessionId, RoomId, EventId, NotifiableEvent?> { _, _, _ -> null } |
||||||
|
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {} |
||||||
|
val incrementPushCounterResult = lambdaRecorder<Unit> {} |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = AN_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived = onNotifiableEventReceived, |
||||||
|
notifiableEventResult = notifiableEventResult, |
||||||
|
buildMeta = aBuildMeta( |
||||||
|
// Also test `lowPrivacyLoggingEnabled = false` here |
||||||
|
lowPrivacyLoggingEnabled = false |
||||||
|
), |
||||||
|
pushClientSecret = FakePushClientSecret( |
||||||
|
getUserIdFromSecretResult = { A_USER_ID } |
||||||
|
), |
||||||
|
incrementPushCounterResult = incrementPushCounterResult |
||||||
|
) |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
incrementPushCounterResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
notifiableEventResult.assertions() |
||||||
|
.isCalledOnce() |
||||||
|
.with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) |
||||||
|
onNotifiableEventReceived.assertions() |
||||||
|
.isNeverCalled() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `when diagnostic PushData is received, the diagnostic push handler is informed `() = |
||||||
|
runTest { |
||||||
|
val aPushData = PushData( |
||||||
|
eventId = DefaultTestPush.TEST_EVENT_ID, |
||||||
|
roomId = A_ROOM_ID, |
||||||
|
unread = 0, |
||||||
|
clientSecret = A_SECRET, |
||||||
|
) |
||||||
|
val diagnosticPushHandler = DiagnosticPushHandler() |
||||||
|
val defaultPushHandler = createDefaultPushHandler( |
||||||
|
diagnosticPushHandler = diagnosticPushHandler, |
||||||
|
incrementPushCounterResult = { } |
||||||
|
) |
||||||
|
diagnosticPushHandler.state.test { |
||||||
|
defaultPushHandler.handle(aPushData) |
||||||
|
awaitItem() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun createDefaultPushHandler( |
||||||
|
onNotifiableEventReceived: (NotifiableEvent) -> Unit = { TODO() }, |
||||||
|
notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> TODO() }, |
||||||
|
incrementPushCounterResult: () -> Unit = { TODO() }, |
||||||
|
userPushStore: UserPushStore = FakeUserPushStore(), |
||||||
|
pushClientSecret: PushClientSecret = FakePushClientSecret(), |
||||||
|
buildMeta: BuildMeta = aBuildMeta(), |
||||||
|
matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(), |
||||||
|
diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(), |
||||||
|
): DefaultPushHandler { |
||||||
|
return DefaultPushHandler( |
||||||
|
onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived), |
||||||
|
notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult), |
||||||
|
incrementPushDataStore = object : IncrementPushDataStore { |
||||||
|
override suspend fun incrementPushCounter() { |
||||||
|
incrementPushCounterResult() |
||||||
|
} |
||||||
|
}, |
||||||
|
userPushStoreFactory = FakeUserPushStoreFactory(userPushStore), |
||||||
|
pushClientSecret = pushClientSecret, |
||||||
|
buildMeta = buildMeta, |
||||||
|
matrixAuthenticationService = matrixAuthenticationService, |
||||||
|
diagnosticPushHandler = diagnosticPushHandler, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 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.libraries.push.impl.push |
||||||
|
|
||||||
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent |
||||||
|
|
||||||
|
class FakeOnNotifiableEventReceived( |
||||||
|
private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit, |
||||||
|
) : OnNotifiableEventReceived { |
||||||
|
override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { |
||||||
|
onNotifiableEventReceivedResult(notifiableEvent) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue