mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-1362 Receive and expose push notification events from PushManager (#581)
This commit is contained in:
parent
739004cc57
commit
5a2b1e61c2
16 changed files with 1255 additions and 111 deletions
|
@ -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
|
||||
|
|
|
@ -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<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to log a user out.
|
||||
*/
|
||||
val logoutFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a passwordless request.
|
||||
*/
|
||||
val passwordlessRequestFlow: Flow<PasswordlessRequestData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync cipher delete.
|
||||
*/
|
||||
val syncCipherDeleteFlow: Flow<SyncCipherDeleteData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync cipher upsert.
|
||||
*/
|
||||
val syncCipherUpsertFlow: Flow<SyncCipherUpsertData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync cipher delete.
|
||||
*/
|
||||
val syncFolderDeleteFlow: Flow<SyncFolderDeleteData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync folder upsert.
|
||||
*/
|
||||
val syncFolderUpsertFlow: Flow<SyncFolderUpsertData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger syncing organization keys.
|
||||
*/
|
||||
val syncOrgKeysFlow: Flow<Unit>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync send delete.
|
||||
*/
|
||||
val syncSendDeleteFlow: Flow<SyncSendDeleteData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync send upsert.
|
||||
*/
|
||||
val syncSendUpsertFlow: Flow<SyncSendUpsertData>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
@ -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<Unit>()
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
private val mutableSyncCipherDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncCipherDeleteData>()
|
||||
private val mutableSyncCipherUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncCipherUpsertData>()
|
||||
private val mutableSyncFolderDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncFolderDeleteData>()
|
||||
private val mutableSyncFolderUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncFolderUpsertData>()
|
||||
private val mutableSyncOrgKeysSharedFlow = bufferedMutableSharedFlow<Unit>()
|
||||
private val mutableSyncSendDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendDeleteData>()
|
||||
private val mutableSyncSendUpsertSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
|
||||
override val fullSyncFlow: SharedFlow<Unit>
|
||||
get() = mutableFullSyncSharedFlow.asSharedFlow()
|
||||
|
||||
override val logoutFlow: SharedFlow<Unit>
|
||||
get() = mutableLogoutSharedFlow.asSharedFlow()
|
||||
|
||||
override val passwordlessRequestFlow: SharedFlow<PasswordlessRequestData>
|
||||
get() = mutablePasswordlessRequestSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncCipherDeleteFlow: SharedFlow<SyncCipherDeleteData>
|
||||
get() = mutableSyncCipherDeleteSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncCipherUpsertFlow: SharedFlow<SyncCipherUpsertData>
|
||||
get() = mutableSyncCipherUpsertSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncFolderDeleteFlow: SharedFlow<SyncFolderDeleteData>
|
||||
get() = mutableSyncFolderDeleteSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncFolderUpsertFlow: SharedFlow<SyncFolderUpsertData>
|
||||
get() = mutableSyncFolderUpsertSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncOrgKeysFlow: SharedFlow<Unit>
|
||||
get() = mutableSyncOrgKeysSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncSendDeleteFlow: SharedFlow<SyncSendDeleteData>
|
||||
get() = mutableSyncSendDeleteSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncSendUpsertFlow: SharedFlow<SyncSendUpsertData>
|
||||
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<BitwardenNotification>(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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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<String>,
|
||||
@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()
|
||||
}
|
|
@ -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>(NotificationType.entries.toTypedArray())
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
|
Loading…
Reference in a new issue