BIT-1361 Setup GCM and Bitwarden push registration (#547)

This commit is contained in:
Sean Weiser
2024-01-09 14:06:15 -06:00
committed by GitHub
parent a6657e427e
commit 08fd7477cd
19 changed files with 865 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import androidx.core.content.edit
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.util.getBinaryLongFromZoneDateTime
import com.x8bit.bitwarden.data.platform.util.getZoneDateTimeFromBinaryLong
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
class PushDiskSourceTest {
private val fakeSharedPreferences = FakeSharedPreferences()
private val pushDiskSource = PushDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
)
@Test
fun `registeredPushToken should pull from and update SharedPreferences`() {
val registeredPushTokenKey = "bwPreferencesStorage:pushRegisteredToken"
// Shared preferences and the repository start with the same value.
assertNull(pushDiskSource.registeredPushToken)
assertNull(fakeSharedPreferences.getString(registeredPushTokenKey, null))
// Updating the repository updates shared preferences
pushDiskSource.registeredPushToken = "abcd"
assertEquals(
"abcd",
fakeSharedPreferences.getString(registeredPushTokenKey, null),
)
// Update SharedPreferences updates the repository
fakeSharedPreferences.edit().putString(registeredPushTokenKey, null).apply()
assertNull(pushDiskSource.registeredPushToken)
}
@Test
fun `getCurrentPushToken should pull from SharedPreferences`() {
val currentPushTokenBaseKey = "bwPreferencesStorage:pushCurrentToken"
val mockUserId = "mockUserId"
val mockCurrentPushToken = "abcd"
fakeSharedPreferences
.edit()
.putString(
"${currentPushTokenBaseKey}_$mockUserId",
mockCurrentPushToken,
)
.apply()
val actual = pushDiskSource.getCurrentPushToken(userId = mockUserId)
assertEquals(
mockCurrentPushToken,
actual,
)
}
@Test
fun `storeCurrentPushToken should update SharedPreferences`() {
val currentPushTokenBaseKey = "bwPreferencesStorage:pushCurrentToken"
val mockUserId = "mockUserId"
val mockCurrentPushToken = "abcd"
pushDiskSource.storeCurrentPushToken(
userId = mockUserId,
pushToken = mockCurrentPushToken,
)
val actual = fakeSharedPreferences
.getString(
"${currentPushTokenBaseKey}_$mockUserId",
null,
)
assertEquals(
mockCurrentPushToken,
actual,
)
}
@Test
fun `getLastPushTokenRegistrationDate should pull from SharedPreferences`() {
val lastPushTokenBaseKey = "bwPreferencesStorage:pushLastRegistrationDate"
val mockUserId = "mockUserId"
val mockLastPushTokenRegistration = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
fakeSharedPreferences
.edit()
.putLong(
"${lastPushTokenBaseKey}_$mockUserId",
getBinaryLongFromZoneDateTime(mockLastPushTokenRegistration),
)
.apply()
val actual = pushDiskSource.getLastPushTokenRegistrationDate(userId = mockUserId)!!
assertEquals(
mockLastPushTokenRegistration,
actual,
)
}
@Test
fun `storeLastPushTokenRegistrationDate for non-null values should update SharedPreferences`() {
val lastPushTokenBaseKey = "bwPreferencesStorage:pushLastRegistrationDate"
val mockUserId = "mockUserId"
val mockLastPushTokenRegistration = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
pushDiskSource.storeLastPushTokenRegistrationDate(
userId = mockUserId,
registrationDate = mockLastPushTokenRegistration,
)
val actual = fakeSharedPreferences
.getLong(
"${lastPushTokenBaseKey}_$mockUserId",
0,
)
assertEquals(
mockLastPushTokenRegistration,
getZoneDateTimeFromBinaryLong(actual),
)
}
@Test
fun `storeLastPushTokenRegistrationDate for null values should clear SharedPreferences`() {
val lastPushTokenBaseKey = "bwPreferencesStorage:pushLastRegistrationDate"
val mockUserId = "mockUserId"
val mockLastPushTokenRegistration = ZonedDateTime.now()
val lastPushTokenKey = "${lastPushTokenBaseKey}_$mockUserId"
fakeSharedPreferences.edit {
putLong(lastPushTokenKey, mockLastPushTokenRegistration.toEpochSecond())
}
assertTrue(fakeSharedPreferences.contains(lastPushTokenKey))
pushDiskSource.storeLastPushTokenRegistrationDate(
userId = mockUserId,
registrationDate = null,
)
assertFalse(fakeSharedPreferences.contains(lastPushTokenKey))
}
}

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.platform.datasource.network.api.PushApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.PushTokenRequest
import kotlinx.coroutines.test.runTest
import okhttp3.mockwebserver.MockResponse
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import retrofit2.create
import java.util.UUID
class PushServiceTest : BaseServiceTest() {
private val mockAppId = UUID.randomUUID().toString()
private val pushApi: PushApi = retrofit.create()
private val pushService: PushService = PushServiceImpl(
pushApi = pushApi,
appId = mockAppId,
)
@Test
fun `putDeviceToken should return the correct response`() = runTest {
val pushToken = UUID.randomUUID().toString()
server.enqueue(MockResponse())
val result = pushService.putDeviceToken(
body = PushTokenRequest(
pushToken = pushToken,
),
)
assertEquals(
Unit,
result.getOrThrow(),
)
}
}

View File

@@ -0,0 +1,221 @@
package com.x8bit.bitwarden.data.platform.manager
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
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.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.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.TimeZone
class PushManagerTest {
private val clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
TimeZone.getTimeZone("UTC").toZoneId(),
)
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val authDiskSource: AuthDiskSource = FakeAuthDiskSource()
private val pushDiskSource: PushDiskSource = PushDiskSourceImpl(FakeSharedPreferences())
private val pushService: PushService = mockk()
private lateinit var pushManager: PushManager
@BeforeEach
fun setUp() {
pushManager = PushManagerImpl(
authDiskSource = authDiskSource,
pushDiskSource = pushDiskSource,
pushService = pushService,
dispatcherManager = dispatcherManager,
clock = clock,
)
}
@Nested
inner class NullUserState {
@BeforeEach
fun setUp() {
authDiskSource.userState = null
}
@Test
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()
assertNull(pushDiskSource.registeredPushToken)
}
}
@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(),
)
}
@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(),
)
}
@Suppress("MaxLineLength")
@Test
fun `registerStoredPushTokenIfNecessary should update registeredPushToken and lastPushTokenRegistrationDate`() {
pushDiskSource.registeredPushToken = newToken
pushManager.registerStoredPushTokenIfNecessary()
coVerify(exactly = 0) { pushService.putDeviceToken(any()) }
assertEquals(newToken, pushDiskSource.registeredPushToken)
assertEquals(
clock.instant().epochSecond,
pushDiskSource.getLastPushTokenRegistrationDate(userId)?.toEpochSecond(),
)
}
}
@Nested
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))
}
}
@Nested
inner class FailedRequest {
@BeforeEach
fun setUp() {
coEvery {
pushService.putDeviceToken(any())
} returns Throwable().asFailure()
}
@Test
fun `registerPushTokenIfNecessary should update registeredPushToken`() {
pushManager.registerPushTokenIfNecessary(newToken)
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))
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.ui.platform.util
import com.x8bit.bitwarden.data.platform.util.getBinaryLongFromZoneDateTime
import com.x8bit.bitwarden.data.platform.util.getZoneDateTimeFromBinaryLong
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.ZonedDateTime
class ZonedDateTimeUtilsTest {
@Test
fun `getZoneDateTimeFromBinaryLong should correctly convert a Long to a ZonedDateTime`() {
val binaryLong = 5250087787086431044L
val expectedDateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
assertEquals(expectedDateTime, getZoneDateTimeFromBinaryLong(binaryLong))
val a = getZoneDateTimeFromBinaryLong(binaryLong)
val b = getBinaryLongFromZoneDateTime(a)
assertEquals(binaryLong, b)
}
@Test
fun `getBinaryLongFromZoneDateTime should correctly convert a ZonedDateTime to a Long`() {
val dateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
val expectedBinaryLong = 5250087787086431044L
assertEquals(expectedBinaryLong, getBinaryLongFromZoneDateTime(dateTime))
}
}