diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9565d9d5..436ecc460 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -187,8 +187,9 @@ koverReport { "*.*DefaultImpls*", // OS-level components "com.x8bit.bitwarden.BitwardenApplication", - "com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*", "com.x8bit.bitwarden.MainActivity*", + "com.x8bit.bitwarden.data.autofill.BitwardenAutofillService*", + "com.x8bit.bitwarden.data.push.BitwardenFirebaseMessagingService*", // Empty Composables "com.x8bit.bitwarden.ui.platform.feature.splash.SplashScreenKt", // Databases diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManager.kt index 8740706b3..e5c0321ca 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManager.kt @@ -1,9 +1,73 @@ package com.x8bit.bitwarden.data.platform.manager +import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData +import kotlinx.coroutines.flow.Flow + /** * Manager to handle push notification registration. */ interface PushManager { + /** + * Flow that represents requests intended for full syncs. + */ + val fullSyncFlow: Flow + + /** + * Flow that represents requests intended to log a user out. + */ + val logoutFlow: Flow + + /** + * Flow that represents requests intended to trigger a passwordless request. + */ + val passwordlessRequestFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync cipher delete. + */ + val syncCipherDeleteFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync cipher upsert. + */ + val syncCipherUpsertFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync cipher delete. + */ + val syncFolderDeleteFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync folder upsert. + */ + val syncFolderUpsertFlow: Flow + + /** + * Flow that represents requests intended to trigger syncing organization keys. + */ + val syncOrgKeysFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync send delete. + */ + val syncSendDeleteFlow: Flow + + /** + * Flow that represents requests intended to trigger a sync send upsert. + */ + val syncSendUpsertFlow: Flow + + /** + * Handles the necessary steps to take when a push notification with payload [data] is received. + */ + fun onMessageReceived(data: String) + /** * Registers a [token] for the current user with Bitwarden's server if needed. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt index 2e9364f20..ec09fe1bf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/PushManagerImpl.kt @@ -5,12 +5,27 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import com.x8bit.bitwarden.data.platform.manager.model.BitwardenNotification +import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload +import com.x8bit.bitwarden.data.platform.manager.model.NotificationType +import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData +import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromJsonElement import java.time.Clock import java.time.ZoneOffset import java.time.ZonedDateTime @@ -25,11 +40,60 @@ class PushManagerImpl @Inject constructor( private val pushDiskSource: PushDiskSource, private val pushService: PushService, private val clock: Clock, + private val json: Json, dispatcherManager: DispatcherManager, ) : PushManager { private val ioScope = CoroutineScope(dispatcherManager.io) private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + private val mutableFullSyncSharedFlow = bufferedMutableSharedFlow() + private val mutableLogoutSharedFlow = bufferedMutableSharedFlow() + private val mutablePasswordlessRequestSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncCipherDeleteSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncCipherUpsertSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncFolderDeleteSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncFolderUpsertSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow() + private val mutableSyncSendDeleteSharedFlow = + bufferedMutableSharedFlow() + private val mutableSyncSendUpsertSharedFlow = + bufferedMutableSharedFlow() + + override val fullSyncFlow: SharedFlow + get() = mutableFullSyncSharedFlow.asSharedFlow() + + override val logoutFlow: SharedFlow + get() = mutableLogoutSharedFlow.asSharedFlow() + + override val passwordlessRequestFlow: SharedFlow + get() = mutablePasswordlessRequestSharedFlow.asSharedFlow() + + override val syncCipherDeleteFlow: SharedFlow + get() = mutableSyncCipherDeleteSharedFlow.asSharedFlow() + + override val syncCipherUpsertFlow: SharedFlow + get() = mutableSyncCipherUpsertSharedFlow.asSharedFlow() + + override val syncFolderDeleteFlow: SharedFlow + get() = mutableSyncFolderDeleteSharedFlow.asSharedFlow() + + override val syncFolderUpsertFlow: SharedFlow + get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow() + + override val syncOrgKeysFlow: SharedFlow + get() = mutableSyncOrgKeysSharedFlow.asSharedFlow() + + override val syncSendDeleteFlow: SharedFlow + get() = mutableSyncSendDeleteSharedFlow.asSharedFlow() + + override val syncSendUpsertFlow: SharedFlow + get() = mutableSyncSendUpsertSharedFlow.asSharedFlow() + init { authDiskSource .userStateFlow @@ -39,6 +103,127 @@ class PushManagerImpl @Inject constructor( .launchIn(unconfinedScope) } + @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount") + override fun onMessageReceived(data: String) { + val notification = try { + json.decodeFromString(data) + } catch (exception: IllegalArgumentException) { + return + } + + if (authDiskSource.uniqueAppId == notification.contextId) return + + val userId = authDiskSource.userState?.activeUserId + + when (val type = notification.notificationType) { + NotificationType.AUTH_REQUEST, + NotificationType.AUTH_REQUEST_RESPONSE, + -> { + val payload: NotificationPayload.PasswordlessRequestNotification = + json.decodeFromJsonElement(notification.payload) + mutablePasswordlessRequestSharedFlow.tryEmit( + PasswordlessRequestData( + loginRequestId = payload.id, + userId = payload.userId, + ), + ) + } + + NotificationType.LOG_OUT -> { + if (userId == null) return + mutableLogoutSharedFlow.tryEmit(Unit) + } + + NotificationType.SYNC_CIPHER_CREATE, + NotificationType.SYNC_CIPHER_UPDATE, + -> { + val payload: NotificationPayload.SyncCipherNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + mutableSyncCipherUpsertSharedFlow.tryEmit( + SyncCipherUpsertData( + cipherId = payload.id, + revisionDate = payload.revisionDate, + isUpdate = type == NotificationType.SYNC_CIPHER_UPDATE, + ), + ) + } + + NotificationType.SYNC_CIPHER_DELETE, + NotificationType.SYNC_LOGIN_DELETE, + -> { + val payload: NotificationPayload.SyncCipherNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + mutableSyncCipherDeleteSharedFlow.tryEmit( + SyncCipherDeleteData(payload.id), + ) + } + + NotificationType.SYNC_CIPHERS, + NotificationType.SYNC_SETTINGS, + NotificationType.SYNC_VAULT, + -> { + if (userId == null) return + mutableFullSyncSharedFlow.tryEmit(Unit) + } + + NotificationType.SYNC_FOLDER_CREATE, + NotificationType.SYNC_FOLDER_UPDATE, + -> { + val payload: NotificationPayload.SyncFolderNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + mutableSyncFolderUpsertSharedFlow.tryEmit( + SyncFolderUpsertData( + folderId = payload.id, + revisionDate = payload.revisionDate, + isUpdate = type == NotificationType.SYNC_FOLDER_UPDATE, + ), + ) + } + + NotificationType.SYNC_FOLDER_DELETE -> { + val payload: NotificationPayload.SyncFolderNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + + mutableSyncFolderDeleteSharedFlow.tryEmit( + SyncFolderDeleteData(payload.id), + ) + } + + NotificationType.SYNC_ORG_KEYS -> { + if (userId == null) return + mutableSyncOrgKeysSharedFlow.tryEmit(Unit) + } + + NotificationType.SYNC_SEND_CREATE, + NotificationType.SYNC_SEND_UPDATE, + -> { + val payload: NotificationPayload.SyncSendNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + mutableSyncSendUpsertSharedFlow.tryEmit( + SyncSendUpsertData( + sendId = payload.id, + revisionDate = payload.revisionDate, + isUpdate = type == NotificationType.SYNC_SEND_UPDATE, + ), + ) + } + + NotificationType.SYNC_SEND_DELETE -> { + val payload: NotificationPayload.SyncSendNotification = + json.decodeFromJsonElement(notification.payload) + if (!payload.userMatchesNotification(userId)) return + mutableSyncSendDeleteSharedFlow.tryEmit( + SyncSendDeleteData(payload.id), + ) + } + } + } + override fun registerPushTokenIfNecessary(token: String) { pushDiskSource.registeredPushToken = token val userId = authDiskSource.userState?.activeUserId ?: return @@ -98,7 +283,11 @@ class PushManagerImpl @Inject constructor( onFailure = { // Silently fail. This call will be attempted again the next time the token // registration is done. - }, + }, ) } } + +private fun NotificationPayload.userMatchesNotification(userId: String?): Boolean { + return this.userId != null && this.userId == userId +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PushManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PushManagerModule.kt index fd69732d1..1a952437f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PushManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PushManagerModule.kt @@ -10,6 +10,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json import java.time.Clock import javax.inject.Singleton @@ -28,11 +29,13 @@ object PushManagerModule { pushService: PushService, dispatcherManager: DispatcherManager, clock: Clock, + json: Json, ): PushManager = PushManagerImpl( authDiskSource = authDiskSource, pushDiskSource = pushDiskSource, pushService = pushService, dispatcherManager = dispatcherManager, clock = clock, + json = json, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/BitwardenNotification.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/BitwardenNotification.kt new file mode 100644 index 000000000..8181d7feb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/BitwardenNotification.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +/** + * Represents a Bitwarden push notification. + * + * @property contextId The context ID. This is mainly used to check if the push notification + * originated from this app. + * @property notificationType The type of notication. + * @property payload Data associated with the push notification. + */ +@Serializable +data class BitwardenNotification( + @SerialName("contextId") val contextId: String, + @SerialName("type") val notificationType: NotificationType, + @SerialName("payload") val payload: JsonElement, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt new file mode 100644 index 000000000..1b3388116 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationPayload.kt @@ -0,0 +1,71 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.ZonedDateTime + +/** + * The payload of a push notification. + */ +@Serializable +sealed class NotificationPayload { + /** + * The user ID associated with the push notification. + */ + abstract val userId: String? + + /** + * A notification payload for sync cipher operations. + */ + @Serializable + data class SyncCipherNotification( + @SerialName("id") val id: String, + @SerialName("userId") override val userId: String?, + @SerialName("organizationId") val organizationId: String?, + @SerialName("collectionIds") val collectionIds: List, + @Contextual + @SerialName("revisionDate") val revisionDate: ZonedDateTime, + ) : NotificationPayload() + + /** + * A notification payload for sync folder operations. + */ + @Serializable + data class SyncFolderNotification( + @SerialName("id") val id: String, + @SerialName("userId") override val userId: String, + @Contextual + @SerialName("revisionDate") val revisionDate: ZonedDateTime, + ) : NotificationPayload() + + /** + * A notification payload for user-based operations. + */ + @Serializable + data class UserNotification( + @SerialName("userId") override val userId: String, + @Contextual + @SerialName("date") val date: ZonedDateTime, + ) : NotificationPayload() + + /** + * A notification payload for sync send operations. + */ + @Serializable + data class SyncSendNotification( + @SerialName("id") val id: String, + @SerialName("userId") override val userId: String, + @Contextual + @SerialName("revisionDate") val revisionDate: ZonedDateTime, + ) : NotificationPayload() + + /** + * A notification payload for passwordless requests. + */ + @Serializable + data class PasswordlessRequestNotification( + @SerialName("userId") override val userId: String, + @SerialName("id") val id: String, + ) : NotificationPayload() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationType.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationType.kt new file mode 100644 index 000000000..d03a88998 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/NotificationType.kt @@ -0,0 +1,67 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import androidx.annotation.Keep +import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Possible notification types. + */ +@Serializable(NotificationTypeSerializer::class) +enum class NotificationType { + @SerialName("0") + SYNC_CIPHER_UPDATE, + + @SerialName("1") + SYNC_CIPHER_CREATE, + + @SerialName("2") + SYNC_LOGIN_DELETE, + + @SerialName("3") + SYNC_FOLDER_DELETE, + + @SerialName("4") + SYNC_CIPHERS, + + @SerialName("5") + SYNC_VAULT, + + @SerialName("6") + SYNC_ORG_KEYS, + + @SerialName("7") + SYNC_FOLDER_CREATE, + + @SerialName("8") + SYNC_FOLDER_UPDATE, + + @SerialName("9") + SYNC_CIPHER_DELETE, + + @SerialName("10") + SYNC_SETTINGS, + + @SerialName("11") + LOG_OUT, + + @SerialName("12") + SYNC_SEND_CREATE, + + @SerialName("13") + SYNC_SEND_UPDATE, + + @SerialName("14") + SYNC_SEND_DELETE, + + @SerialName("15") + AUTH_REQUEST, + + @SerialName("16") + AUTH_REQUEST_RESPONSE, +} + +@Keep +private class NotificationTypeSerializer : + BaseEnumeratedIntSerializer(NotificationType.entries.toTypedArray()) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/PasswordlessRequestData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/PasswordlessRequestData.kt new file mode 100644 index 000000000..116c99ac2 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/PasswordlessRequestData.kt @@ -0,0 +1,12 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +/** + * Required data for passwordless requests. + * + * @property loginRequestId The login request ID. + * @property userId The user ID. + */ +data class PasswordlessRequestData( + val loginRequestId: String, + val userId: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherDeleteData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherDeleteData.kt new file mode 100644 index 000000000..66531a622 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherDeleteData.kt @@ -0,0 +1,10 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +/** + * Required data for sync cipher delete operations. + * + * @property cipherId The cipher ID. + */ +data class SyncCipherDeleteData( + val cipherId: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherUpsertData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherUpsertData.kt new file mode 100644 index 000000000..8e20270f9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncCipherUpsertData.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import java.time.ZonedDateTime + +/** + * Required data for sync cipher upsert operations. + * + * @property cipherId The cipher ID. + * @property revisionDate The cipher's revision date. This is used to determine if the local copy of + * the cipher is out-of-date. + * @property isUpdate Whether or not this is an update of an existing cipher. + */ +data class SyncCipherUpsertData( + val cipherId: String, + val revisionDate: ZonedDateTime, + val isUpdate: Boolean, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderDeleteData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderDeleteData.kt new file mode 100644 index 000000000..8f3d7fba3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderDeleteData.kt @@ -0,0 +1,10 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +/** + * Required data for sync folder delete operations. + * + * @property folderId The folder ID. + */ +data class SyncFolderDeleteData( + val folderId: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderUpsertData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderUpsertData.kt new file mode 100644 index 000000000..8d45786e9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncFolderUpsertData.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import java.time.ZonedDateTime + +/** + * Required data for sync folder upsert operations. + * + * @property folderId The folder ID. + * @property revisionDate The folder's revision date. This is used to determine if the local copy of + * the folder is out-of-date. + * @property isUpdate Whether or not this is an update of an existing folder. + */ +data class SyncFolderUpsertData( + val folderId: String, + val revisionDate: ZonedDateTime, + val isUpdate: Boolean, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendDeleteData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendDeleteData.kt new file mode 100644 index 000000000..6f85274bd --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendDeleteData.kt @@ -0,0 +1,10 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +/** + * Required data for sync send delete operations. + * + * @property sendId The send ID. + */ +data class SyncSendDeleteData( + val sendId: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendUpsertData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendUpsertData.kt new file mode 100644 index 000000000..37039c21b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SyncSendUpsertData.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import java.time.ZonedDateTime + +/** + * Required data for sync send upsert operations. + * + * @property sendId The send ID. + * @property revisionDate The send's revision date. This is used to determine if the local copy of + * the send is out-of-date. + * @property isUpdate Whether or not this is an update of an existing send. + */ +data class SyncSendUpsertData( + val sendId: String, + val revisionDate: ZonedDateTime, + val isUpdate: Boolean, +) diff --git a/app/src/standard/java/com/x8bit/bitwarden/data/push/BitwardenFirebaseMessagingService.kt b/app/src/standard/java/com/x8bit/bitwarden/data/push/BitwardenFirebaseMessagingService.kt index dc79962c5..142508367 100644 --- a/app/src/standard/java/com/x8bit/bitwarden/data/push/BitwardenFirebaseMessagingService.kt +++ b/app/src/standard/java/com/x8bit/bitwarden/data/push/BitwardenFirebaseMessagingService.kt @@ -15,7 +15,8 @@ class BitwardenFirebaseMessagingService : FirebaseMessagingService() { lateinit var pushManager: PushManager override fun onMessageReceived(message: RemoteMessage) { - // TODO handle new messages. See BIT-1362. + val data = message.data["data"] ?: return + pushManager.onMessageReceived(data) } override fun onNewToken(token: String) { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt index 305425a15..801019354 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/PushManagerTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.platform.manager +import app.cash.turbine.test import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource @@ -7,14 +8,23 @@ import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSourceImpl +import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData +import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach @@ -51,171 +61,796 @@ class PushManagerTest { pushService = pushService, dispatcherManager = dispatcherManager, clock = clock, + json = PlatformNetworkModule.providesJson(), ) } @Nested - inner class NullUserState { - @BeforeEach - fun setUp() { - authDiskSource.userState = null + inner class PushNotificationHandling { + @Test + fun `onMessageReceived invalid JSON does not crash`() { + pushManager.onMessageReceived(INVALID_NOTIFICATION_JSON) } @Test - fun `registerPushTokenIfNecessary should update registeredPushToken`() { - assertEquals(null, pushDiskSource.registeredPushToken) - - val token = "token" - pushManager.registerPushTokenIfNecessary(token) - - assertEquals(token, pushDiskSource.registeredPushToken) - } + fun `onMessageReceived auth request emits to passwordlessRequestFlow`() = + runTest { + pushManager.passwordlessRequestFlow.test { + pushManager.onMessageReceived(AUTH_REQUEST_NOTIFICATION_JSON) + assertEquals( + PasswordlessRequestData( + loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + userId = "078966a2-93c2-4618-ae2a-0a2394c88d37", + ), + awaitItem(), + ) + } + } @Test - fun `registerStoredPushTokenIfNecessary should do nothing`() { - pushManager.registerStoredPushTokenIfNecessary() + fun `onMessageReceived auth request response emits to passwordlessRequestFlow`() = + runTest { + pushManager.passwordlessRequestFlow.test { + pushManager.onMessageReceived(AUTH_REQUEST_RESPONSE_NOTIFICATION_JSON) + assertEquals( + PasswordlessRequestData( + loginRequestId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + userId = "078966a2-93c2-4618-ae2a-0a2394c88d37", + ), + awaitItem(), + ) + } + } - assertNull(pushDiskSource.registeredPushToken) + @Nested + inner class MatchingUser { + @BeforeEach + fun setUp() { + val userId = "078966a2-93c2-4618-ae2a-0a2394c88d37" + authDiskSource.userState = UserStateJson(userId, mapOf(userId to mockk())) + } + + @Test + fun `onMessageReceived sync cipher create emits to syncCipherUpsertFlow`() = + runTest { + pushManager.syncCipherUpsertFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_CREATE_NOTIFICATION_JSON) + assertEquals( + SyncCipherUpsertData( + cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = false, + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync cipher delete emits to syncCipherDeleteFlow`() = + runTest { + pushManager.syncCipherDeleteFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_DELETE_NOTIFICATION_JSON) + assertEquals( + SyncCipherDeleteData( + cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync cipher update emits to syncCipherUpsertFlow`() = + runTest { + pushManager.syncCipherUpsertFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_UPDATE_NOTIFICATION_JSON) + assertEquals( + SyncCipherUpsertData( + cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = true, + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync folder create emits to syncFolderUpsertFlow`() = + runTest { + pushManager.syncFolderUpsertFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_CREATE_NOTIFICATION_JSON) + assertEquals( + SyncFolderUpsertData( + folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = false, + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync folder delete emits to syncFolderDeleteFlow`() = + runTest { + pushManager.syncFolderDeleteFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_JSON) + assertEquals( + SyncFolderDeleteData( + folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync folder update emits to syncFolderUpsertFlow`() = + runTest { + pushManager.syncFolderUpsertFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_UPDATE_NOTIFICATION_JSON) + assertEquals( + SyncFolderUpsertData( + folderId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = true, + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync login delete emits to syncCipherDeleteFlow`() = + runTest { + pushManager.syncCipherDeleteFlow.test { + pushManager.onMessageReceived(SYNC_LOGIN_DELETE_NOTIFICATION_JSON) + assertEquals( + SyncCipherDeleteData( + cipherId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync send create emits to syncSendUpsertFlow`() = + runTest { + pushManager.syncSendUpsertFlow.test { + pushManager.onMessageReceived(SYNC_SEND_CREATE_NOTIFICATION_JSON) + assertEquals( + SyncSendUpsertData( + sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = false, + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync send delete emits to syncSendDeleteFlow`() = + runTest { + pushManager.syncSendDeleteFlow.test { + pushManager.onMessageReceived(SYNC_SEND_DELETE_NOTIFICATION_JSON) + assertEquals( + SyncSendDeleteData( + sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + ), + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync send update emits to syncSendUpsertFlow`() = + runTest { + pushManager.syncSendUpsertFlow.test { + pushManager.onMessageReceived(SYNC_SEND_UPDATE_NOTIFICATION_JSON) + assertEquals( + SyncSendUpsertData( + sendId = "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + revisionDate = ZonedDateTime.parse("2023-10-27T12:00:00.000Z"), + isUpdate = true, + ), + awaitItem(), + ) + } + } + } + + @Nested + inner class NonMatchingUser { + @BeforeEach + fun setUp() { + val userId = "bad user ID" + authDiskSource.userState = UserStateJson(userId, mapOf(userId to mockk())) + } + + @Test + fun `onMessageReceived sync cipher create does nothing`() = runTest { + pushManager.syncCipherUpsertFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_CREATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync cipher delete does nothing`() = runTest { + pushManager.syncCipherDeleteFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_DELETE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync cipher update does nothing`() = runTest { + pushManager.syncCipherUpsertFlow.test { + pushManager.onMessageReceived(SYNC_CIPHER_UPDATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync folder create does nothing`() = runTest { + pushManager.syncFolderUpsertFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_CREATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync folder delete does nothing`() = runTest { + pushManager.syncFolderDeleteFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_DELETE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync folder update does nothing`() = runTest { + pushManager.syncFolderDeleteFlow.test { + pushManager.onMessageReceived(SYNC_FOLDER_UPDATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync login delete does nothing`() = runTest { + pushManager.syncCipherDeleteFlow.test { + pushManager.onMessageReceived(SYNC_LOGIN_DELETE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync send create does nothing`() = runTest { + pushManager.syncSendUpsertFlow.test { + pushManager.onMessageReceived(SYNC_SEND_CREATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync send delete does nothing`() = runTest { + pushManager.syncSendDeleteFlow.test { + pushManager.onMessageReceived(SYNC_SEND_DELETE_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync send update does nothing`() = runTest { + pushManager.syncSendUpsertFlow.test { + pushManager.onMessageReceived(SYNC_SEND_UPDATE_NOTIFICATION_JSON) + expectNoEvents() + } + } + } + + @Nested + inner class NullUserState { + @BeforeEach + fun setUp() { + authDiskSource.userState = null + } + + @Test + fun `onMessageReceived logout does nothing`() = runTest { + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync ciphers does nothing`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync org keys does nothing`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_ORG_KEYS_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync settings does nothing`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_JSON) + expectNoEvents() + } + } + + @Test + fun `onMessageReceived sync vault does nothing`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_JSON) + expectNoEvents() + } + } + } + + @Nested + inner class NonNullUserState { + @BeforeEach + fun setUp() { + val userId = "any user ID" + authDiskSource.userState = UserStateJson(userId, mapOf(userId to mockk())) + } + + @Test + fun `onMessageReceived logout emits to logoutFlow`() = runTest { + pushManager.logoutFlow.test { + pushManager.onMessageReceived(LOGOUT_NOTIFICATION_JSON) + assertEquals( + Unit, + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync ciphers emits to fullSyncFlow`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_CIPHERS_NOTIFICATION_JSON) + assertEquals( + Unit, + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync org keys emits to syncOrgKeysFlow`() = runTest { + pushManager.syncOrgKeysFlow.test { + pushManager.onMessageReceived(SYNC_ORG_KEYS_NOTIFICATION_JSON) + assertEquals( + Unit, + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync settings emits to fullSyncFlow`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_SETTINGS_NOTIFICATION_JSON) + assertEquals( + Unit, + awaitItem(), + ) + } + } + + @Test + fun `onMessageReceived sync vault emits to fullSyncFlow`() = runTest { + pushManager.fullSyncFlow.test { + pushManager.onMessageReceived(SYNC_VAULT_NOTIFICATION_JSON) + assertEquals( + Unit, + awaitItem(), + ) + } + } } } @Nested - inner class NonNullUserState { - private val existingToken = "existingToken" - private val userId = "userId" - - @BeforeEach - fun setUp() { - pushDiskSource.storeCurrentPushToken(userId, existingToken) - authDiskSource.userState = UserStateJson(userId, mapOf(userId to mockk())) - } - - @Suppress("MaxLineLength") - @Test - fun `registerStoredPushTokenIfNecessary should do nothing if registered less than a day before`() { - val lastRegistration = ZonedDateTime.ofInstant( - clock.instant().minus(23, ChronoUnit.HOURS), - ZoneOffset.UTC, - ) - pushDiskSource.registeredPushToken = existingToken - pushDiskSource.storeLastPushTokenRegistrationDate( - userId, - lastRegistration, - ) - pushManager.registerStoredPushTokenIfNecessary() - - // Assert the last registration value has not changed - assertEquals( - lastRegistration.toEpochSecond(), - pushDiskSource.getLastPushTokenRegistrationDate(userId)!!.toEpochSecond(), - ) - } - + inner class PushNotificationRegistration { @Nested - inner class MatchingToken { - private val newToken = "existingToken" - - @Suppress("MaxLineLength") - @Test - fun `registerPushTokenIfNecessary should update registeredPushToken and lastPushTokenRegistrationDate`() { - pushManager.registerPushTokenIfNecessary(newToken) - - coVerify(exactly = 0) { pushService.putDeviceToken(any()) } - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals( - clock.instant().epochSecond, - pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toEpochSecond(), - ) + inner class NullUserState { + @BeforeEach + fun setUp() { + authDiskSource.userState = null } - @Suppress("MaxLineLength") @Test - fun `registerStoredPushTokenIfNecessary should update registeredPushToken and lastPushTokenRegistrationDate`() { - pushDiskSource.registeredPushToken = newToken + fun `registerPushTokenIfNecessary should update registeredPushToken`() { + assertEquals(null, pushDiskSource.registeredPushToken) + + val token = "token" + pushManager.registerPushTokenIfNecessary(token) + + assertEquals(token, pushDiskSource.registeredPushToken) + } + + @Test + fun `registerStoredPushTokenIfNecessary should do nothing`() { pushManager.registerStoredPushTokenIfNecessary() - coVerify(exactly = 0) { pushService.putDeviceToken(any()) } - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals( - clock.instant().epochSecond, - pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toEpochSecond(), - ) + assertNull(pushDiskSource.registeredPushToken) } } @Nested - inner class DifferentToken { - private val newToken = "newToken" + inner class NonNullUserState { + private val existingToken = "existingToken" + private val userId = "userId" + + @BeforeEach + fun setUp() { + pushDiskSource.storeCurrentPushToken(userId, existingToken) + authDiskSource.userState = UserStateJson(userId, mapOf(userId to mockk())) + } + + @Suppress("MaxLineLength") + @Test + fun `registerStoredPushTokenIfNecessary should do nothing if registered less than a day before`() { + val lastRegistration = ZonedDateTime.ofInstant( + clock.instant().minus(23, ChronoUnit.HOURS), + ZoneOffset.UTC, + ) + pushDiskSource.registeredPushToken = existingToken + pushDiskSource.storeLastPushTokenRegistrationDate( + userId, + lastRegistration, + ) + pushManager.registerStoredPushTokenIfNecessary() + + // Assert the last registration value has not changed + assertEquals( + lastRegistration.toEpochSecond(), + pushDiskSource.getLastPushTokenRegistrationDate(userId)!!.toEpochSecond(), + ) + } @Nested - inner class SuccessfulRequest { - @BeforeEach - fun setUp() { - coEvery { - pushService.putDeviceToken(any()) - } returns Unit.asSuccess() - } + inner class MatchingToken { + private val newToken = "existingToken" @Suppress("MaxLineLength") @Test - fun `registerPushTokenIfNecessary should update registeredPushToken, lastPushTokenRegistrationDate and currentPushToken`() { + fun `registerPushTokenIfNecessary should update registeredPushToken and lastPushTokenRegistrationDate`() { pushManager.registerPushTokenIfNecessary(newToken) - coVerify(exactly = 1) { pushService.putDeviceToken(PushTokenRequest(newToken)) } + coVerify(exactly = 0) { pushService.putDeviceToken(any()) } + assertEquals(newToken, pushDiskSource.registeredPushToken) assertEquals( clock.instant().epochSecond, pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toEpochSecond(), ) - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals(newToken, pushDiskSource.getCurrentPushToken(userId)) } @Suppress("MaxLineLength") @Test - fun `registerStoredPushTokenIfNecessary should update registeredPushToken, lastPushTokenRegistrationDate and currentPushToken`() { + fun `registerStoredPushTokenIfNecessary should update registeredPushToken and lastPushTokenRegistrationDate`() { pushDiskSource.registeredPushToken = newToken pushManager.registerStoredPushTokenIfNecessary() - coVerify(exactly = 1) { pushService.putDeviceToken(PushTokenRequest(newToken)) } + coVerify(exactly = 0) { pushService.putDeviceToken(any()) } + assertEquals(newToken, pushDiskSource.registeredPushToken) assertEquals( clock.instant().epochSecond, pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toEpochSecond(), ) - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals(newToken, pushDiskSource.getCurrentPushToken(userId)) } } @Nested - inner class FailedRequest { - @BeforeEach - fun setUp() { - coEvery { - pushService.putDeviceToken(any()) - } returns Throwable().asFailure() + inner class DifferentToken { + private val newToken = "newToken" + + @Nested + inner class SuccessfulRequest { + @BeforeEach + fun setUp() { + coEvery { + pushService.putDeviceToken(any()) + } returns Unit.asSuccess() + } + + @Suppress("MaxLineLength") + @Test + fun `registerPushTokenIfNecessary should update registeredPushToken, lastPushTokenRegistrationDate and currentPushToken`() { + pushManager.registerPushTokenIfNecessary(newToken) + + coVerify(exactly = 1) { + pushService.putDeviceToken(PushTokenRequest(newToken)) + } + assertEquals( + clock.instant().epochSecond, + pushDiskSource + .getLastPushTokenRegistrationDate(userId) + ?.toEpochSecond(), + ) + assertEquals(newToken, pushDiskSource.registeredPushToken) + assertEquals(newToken, pushDiskSource.getCurrentPushToken(userId)) + } + + @Suppress("MaxLineLength") + @Test + fun `registerStoredPushTokenIfNecessary should update registeredPushToken, lastPushTokenRegistrationDate and currentPushToken`() { + pushDiskSource.registeredPushToken = newToken + pushManager.registerStoredPushTokenIfNecessary() + + coVerify(exactly = 1) { + pushService.putDeviceToken(PushTokenRequest(newToken)) + } + assertEquals( + clock.instant().epochSecond, + pushDiskSource + .getLastPushTokenRegistrationDate(userId) + ?.toEpochSecond(), + ) + assertEquals(newToken, pushDiskSource.registeredPushToken) + assertEquals(newToken, pushDiskSource.getCurrentPushToken(userId)) + } } - @Test - fun `registerPushTokenIfNecessary should update registeredPushToken`() { - pushManager.registerPushTokenIfNecessary(newToken) + @Nested + inner class FailedRequest { + @BeforeEach + fun setUp() { + coEvery { + pushService.putDeviceToken(any()) + } returns Throwable().asFailure() + } - coVerify(exactly = 1) { pushService.putDeviceToken(PushTokenRequest(newToken)) } - assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId)) - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals(existingToken, pushDiskSource.getCurrentPushToken(userId)) - } + @Test + fun `registerPushTokenIfNecessary should update registeredPushToken`() { + pushManager.registerPushTokenIfNecessary(newToken) - @Test - fun `registerStoredPushTokenIfNecessary should update registeredPushToken`() { - pushDiskSource.registeredPushToken = newToken - pushManager.registerStoredPushTokenIfNecessary() + coVerify(exactly = 1) { + pushService.putDeviceToken(PushTokenRequest(newToken)) + } + assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId)) + assertEquals(newToken, pushDiskSource.registeredPushToken) + assertEquals(existingToken, pushDiskSource.getCurrentPushToken(userId)) + } - coVerify(exactly = 1) { pushService.putDeviceToken(PushTokenRequest(newToken)) } - assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId)) - assertEquals(newToken, pushDiskSource.registeredPushToken) - assertEquals(existingToken, pushDiskSource.getCurrentPushToken(userId)) + @Test + fun `registerStoredPushTokenIfNecessary should update registeredPushToken`() { + pushDiskSource.registeredPushToken = newToken + pushManager.registerStoredPushTokenIfNecessary() + + coVerify(exactly = 1) { + pushService.putDeviceToken(PushTokenRequest(newToken)) + } + assertNull(pushDiskSource.getLastPushTokenRegistrationDate(userId)) + assertEquals(newToken, pushDiskSource.registeredPushToken) + assertEquals(existingToken, pushDiskSource.getCurrentPushToken(userId)) + } } } } } } + +private const val AUTH_REQUEST_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 15, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37" + } + } +""" + +private const val AUTH_REQUEST_RESPONSE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 16, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37" + } + } +""" + +private const val INVALID_NOTIFICATION_JSON = """ + {} +""" + +private const val LOGOUT_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 11, + "payload": { + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "date": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_CIPHER_CREATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 1, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "organizationId": "6a41d965-ed95-4eae-98c3-5f1ec609c2c1", + "collectionIds": [], + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_CIPHER_DELETE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 9, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "organizationId": "6a41d965-ed95-4eae-98c3-5f1ec609c2c1", + "collectionIds": [], + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_CIPHER_UPDATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 0, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "organizationId": "6a41d965-ed95-4eae-98c3-5f1ec609c2c1", + "collectionIds": [], + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_CIPHERS_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 4, + "payload": { + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "date": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_FOLDER_CREATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 7, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_FOLDER_DELETE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 3, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_FOLDER_UPDATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 8, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_LOGIN_DELETE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 2, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "organizationId": "6a41d965-ed95-4eae-98c3-5f1ec609c2c1", + "collectionIds": [], + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_ORG_KEYS_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 6, + "payload": { + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "date": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_SEND_CREATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 12, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_SEND_DELETE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 14, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_SEND_UPDATE_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 13, + "payload": { + "id": "aab5cdcc-f4a7-4e65-bf6d-5e0eab052321", + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "revisionDate": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_SETTINGS_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 10, + "payload": { + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "date": "2023-10-27T12:00:00.000Z" + } + } +""" + +private const val SYNC_VAULT_NOTIFICATION_JSON = """ + { + "contextId": "801f459d-8e51-47d0-b072-3f18c9f66f64", + "type": 5, + "payload": { + "userId": "078966a2-93c2-4618-ae2a-0a2394c88d37", + "date": "2023-10-27T12:00:00.000Z" + } + } +"""