mirror of
https://github.com/bitwarden/android.git
synced 2026-06-02 02:36:58 -05:00
BIT-765: Add access token storage (#138)
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -8,8 +20,15 @@ import org.junit.jupiter.api.Test
|
||||
class AuthDiskSourceTest {
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
private val authDiskSource = AuthDiskSourceImpl(
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -31,4 +50,145 @@ class AuthDiskSourceTest {
|
||||
fakeSharedPreferences.edit().putString(rememberedEmailKey, null).apply()
|
||||
assertNull(authDiskSource.rememberedEmailAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userState should pull from and update SharedPreferences`() {
|
||||
val userStateKey = "bwPreferencesStorage:state"
|
||||
|
||||
// Shared preferences and the repository start with the same value.
|
||||
assertNull(authDiskSource.userState)
|
||||
assertNull(fakeSharedPreferences.getString(userStateKey, null))
|
||||
|
||||
// Updating the repository updates shared preferences
|
||||
authDiskSource.userState = USER_STATE
|
||||
assertEquals(
|
||||
json.parseToJsonElement(
|
||||
USER_STATE_JSON,
|
||||
),
|
||||
json.parseToJsonElement(
|
||||
fakeSharedPreferences.getString(userStateKey, null)!!,
|
||||
),
|
||||
)
|
||||
|
||||
// Update SharedPreferences updates the repository
|
||||
fakeSharedPreferences.edit().putString(userStateKey, null).apply()
|
||||
assertNull(authDiskSource.userState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `userStateFlow should react to changes in userState`() = runTest {
|
||||
authDiskSource.userStateFlow.test {
|
||||
// The initial values of the Flow and the property are in sync
|
||||
assertNull(authDiskSource.userState)
|
||||
assertNull(awaitItem())
|
||||
|
||||
// Updating the repository updates shared preferences
|
||||
authDiskSource.userState = USER_STATE
|
||||
assertEquals(USER_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_STATE_JSON = """
|
||||
{
|
||||
"activeUserId": "activeUserId",
|
||||
"accounts": {
|
||||
"activeUserId": {
|
||||
"profile": {
|
||||
"userId": "activeUserId",
|
||||
"email": "email",
|
||||
"emailVerified": true,
|
||||
"name": "name",
|
||||
"stamp": "stamp",
|
||||
"orgIdentifier": "organizationId",
|
||||
"avatarColor": "avatarColorHex",
|
||||
"hasPremiumPersonally": true,
|
||||
"forcePasswordResetReason": "adminForcePasswordReset",
|
||||
"kdfType": 1,
|
||||
"kdfIterations": 600000,
|
||||
"kdfMemory": 16,
|
||||
"kdfParallelism": 4,
|
||||
"accountDecryptionOptions": {
|
||||
"HasMasterPassword": true,
|
||||
"TrustedDeviceOption": {
|
||||
"EncryptedPrivateKey": "encryptedPrivateKey",
|
||||
"EncryptedUserKey": "encryptedUserKey",
|
||||
"HasAdminApproval": true,
|
||||
"HasLoginApprovingDevice": true,
|
||||
"HasManageResetPasswordPermission": true
|
||||
},
|
||||
"KeyConnectorOption": {
|
||||
"KeyConnectorUrl": "keyConnectorUrl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tokens": {
|
||||
"accessToken": "accessToken",
|
||||
"refreshToken": "refreshToken"
|
||||
},
|
||||
"settings": {
|
||||
"environmentUrls": {
|
||||
"base": "base",
|
||||
"api": "api",
|
||||
"identity": "identity",
|
||||
"icon": "icon",
|
||||
"notifications": "notifications",
|
||||
"webVault": "webVault",
|
||||
"events": "events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
private val USER_STATE = UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = "stamp",
|
||||
organizationId = "organizationId",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
hasPremium = true,
|
||||
forcePasswordResetReason = ForcePasswordResetReason.ADMIN_FORCE_PASSWORD_RESET,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
trustedDeviceUserDecryptionOptions = TrustedDeviceUserDecryptionOptionsJson(
|
||||
encryptedPrivateKey = "encryptedPrivateKey",
|
||||
encryptedUserKey = "encryptedUserKey",
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasManageResetPasswordPermission = true,
|
||||
),
|
||||
keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson(
|
||||
keyConnectorUrl = "keyConnectorUrl",
|
||||
),
|
||||
),
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "base",
|
||||
api = "api",
|
||||
identity = "identity",
|
||||
icon = "icon",
|
||||
notifications = "notifications",
|
||||
webVault = "webVault",
|
||||
events = "events",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,10 @@ import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RsaKeyPair
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
@@ -17,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import io.mockk.clearMocks
|
||||
@@ -24,8 +28,15 @@ import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -35,7 +46,7 @@ class AuthRepositoryTest {
|
||||
|
||||
private val accountsService: AccountsService = mockk()
|
||||
private val identityService: IdentityService = mockk()
|
||||
private val authInterceptor = mockk<AuthTokenInterceptor>()
|
||||
private val authInterceptor = AuthTokenInterceptor()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val authSdkSource = mockk<AuthSdkSource> {
|
||||
coEvery {
|
||||
@@ -63,17 +74,25 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val repository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
identityService = identityService,
|
||||
authSdkSource = authSdkSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
authTokenInterceptor = authInterceptor,
|
||||
dispatcher = UnconfinedTestDispatcher(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
clearMocks(identityService, accountsService, authInterceptor)
|
||||
clearMocks(identityService, accountsService)
|
||||
mockkStatic(GET_TOKEN_RESPONSE_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(GET_TOKEN_RESPONSE_EXTENSIONS_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -162,9 +181,7 @@ class AuthRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun `login get token succeeds should return Success and update AuthState`() = runTest {
|
||||
val successResponse = mockk<GetTokenResponseJson.Success> {
|
||||
every { accessToken } returns ACCESS_TOKEN
|
||||
}
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
@@ -176,11 +193,13 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
.returns(Result.success(successResponse))
|
||||
every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Success, result)
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
verify { authInterceptor.authToken = ACCESS_TOKEN }
|
||||
assertEquals(ACCESS_TOKEN, authInterceptor.authToken)
|
||||
coVerify { accountsService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
@@ -365,11 +384,9 @@ class AuthRepositoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout should change AuthState to be Unauthenticated`() = runTest {
|
||||
fun `logout for single account should clear the access token`() = runTest {
|
||||
// First login:
|
||||
val successResponse = mockk<GetTokenResponseJson.Success> {
|
||||
every { accessToken } returns ACCESS_TOKEN
|
||||
}
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
@@ -379,36 +396,189 @@ class AuthRepositoryTest {
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(Result.success(successResponse))
|
||||
} returns Result.success(successResponse)
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
|
||||
every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
assertEquals(ACCESS_TOKEN, authInterceptor.authToken)
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
|
||||
// Then call logout:
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
|
||||
|
||||
repository.logout()
|
||||
|
||||
assertEquals(AuthState.Unauthenticated, awaitItem())
|
||||
assertNull(authInterceptor.authToken)
|
||||
assertNull(fakeAuthDiskSource.userState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logout for multiple accounts should update current access token`() = runTest {
|
||||
// First populate multiple user accounts
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
// Then login:
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
accountsService.preLogin(email = EMAIL)
|
||||
} returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns Result.success(successResponse)
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = SINGLE_USER_STATE_2)
|
||||
} returns MULTI_USER_STATE
|
||||
|
||||
repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
assertEquals(ACCESS_TOKEN, authInterceptor.authToken)
|
||||
assertEquals(MULTI_USER_STATE, fakeAuthDiskSource.userState)
|
||||
|
||||
// Then call logout:
|
||||
repository.authStateFlow.test {
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), awaitItem())
|
||||
|
||||
repository.logout()
|
||||
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN_2), awaitItem())
|
||||
assertEquals(ACCESS_TOKEN_2, authInterceptor.authToken)
|
||||
assertEquals(SINGLE_USER_STATE_2, fakeAuthDiskSource.userState)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val GET_TOKEN_RESPONSE_EXTENSIONS_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.repository.util.GetTokenResponseExtensionsKt"
|
||||
private const val EMAIL = "test@bitwarden.com"
|
||||
private const val PASSWORD = "password"
|
||||
private const val PASSWORD_HASH = "passwordHash"
|
||||
private const val ACCESS_TOKEN = "accessToken"
|
||||
private const val ACCESS_TOKEN_2 = "accessToken2"
|
||||
private const val CAPTCHA_KEY = "captcha"
|
||||
private const val DEFAULT_KDF_ITERATIONS = 600000
|
||||
private const val ENCRYPTED_USER_KEY = "encryptedUserKey"
|
||||
private const val PUBLIC_KEY = "PublicKey"
|
||||
private const val PRIVATE_KEY = "privateKey"
|
||||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
||||
kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u),
|
||||
)
|
||||
private val GET_TOKEN_RESPONSE_SUCCESS = GetTokenResponseJson.Success(
|
||||
accessToken = ACCESS_TOKEN,
|
||||
refreshToken = "refreshToken",
|
||||
tokenType = "Bearer",
|
||||
expiresInSeconds = 3600,
|
||||
key = "key",
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
privateKey = "privateKey",
|
||||
shouldForcePasswordReset = true,
|
||||
shouldResetMasterPassword = true,
|
||||
masterPasswordPolicyOptions = null,
|
||||
userDecryptionOptions = null,
|
||||
)
|
||||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = "test@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN,
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val ACCOUNT_2 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_2,
|
||||
email = "test2@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester 2",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 400000,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN_2,
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_1 = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
private val MULTI_USER_STATE = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeAuthDiskSource : AuthDiskSource {
|
||||
override var rememberedEmailAddress: String? = null
|
||||
|
||||
override var userState: UserStateJson? = null
|
||||
set(value) {
|
||||
field = value
|
||||
mutableUserStateFlow.tryEmit(value)
|
||||
}
|
||||
|
||||
override val userStateFlow: Flow<UserStateJson?>
|
||||
get() = mutableUserStateFlow.onSubscription { emit(userState) }
|
||||
|
||||
private val mutableUserStateFlow =
|
||||
MutableSharedFlow<UserStateJson?>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GetTokenResponseExtensionsTest {
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
mockkStatic(JWT_TOKEN_UTILS_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(JWT_TOKEN_UTILS_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserState with a null previous state creates a new single user state`() {
|
||||
every { parseJwtTokenDataOrNull(ACCESS_TOKEN_1) } returns JWT_TOKEN_DATA
|
||||
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1,
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserState with a non-null previous state updates the previous state`() {
|
||||
every { parseJwtTokenDataOrNull(ACCESS_TOKEN_1) } returns JWT_TOKEN_DATA
|
||||
|
||||
assertEquals(
|
||||
MULTI_USER_STATE,
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = SINGLE_USER_STATE_2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val ACCESS_TOKEN_1 = "accessToken1"
|
||||
private const val ACCESS_TOKEN_2 = "accessToken2"
|
||||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
|
||||
private const val JWT_TOKEN_UTILS_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.repository.util.JwtTokenUtilsKt"
|
||||
|
||||
private val JWT_TOKEN_DATA = JwtTokenDataJson(
|
||||
userId = "2a135b23-e1fb-42c9-bec3-573857bc8181",
|
||||
email = "test@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
expirationAsEpochTime = 1697495714,
|
||||
hasPremium = false,
|
||||
authenticationMethodsReference = listOf("Application"),
|
||||
)
|
||||
|
||||
private val GET_TOKEN_RESPONSE_SUCCESS = GetTokenResponseJson.Success(
|
||||
accessToken = ACCESS_TOKEN_1,
|
||||
refreshToken = "refreshToken",
|
||||
tokenType = "Bearer",
|
||||
expiresInSeconds = 3600,
|
||||
key = "key",
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
privateKey = "privateKey",
|
||||
shouldForcePasswordReset = true,
|
||||
shouldResetMasterPassword = true,
|
||||
masterPasswordPolicyOptions = null,
|
||||
userDecryptionOptions = null,
|
||||
)
|
||||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = "test@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN_1,
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val ACCOUNT_2 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_2,
|
||||
email = "test2@bitwarden.com",
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester 2",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 400000,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
),
|
||||
tokens = AccountJson.Tokens(
|
||||
accessToken = ACCESS_TOKEN_2,
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_1 = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
),
|
||||
)
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
private val MULTI_USER_STATE = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
@@ -7,6 +7,7 @@ import android.content.SharedPreferences
|
||||
*/
|
||||
class FakeSharedPreferences : SharedPreferences {
|
||||
private val sharedPreferences: MutableMap<String, Any?> = mutableMapOf()
|
||||
private val listeners = mutableSetOf<SharedPreferences.OnSharedPreferenceChangeListener>()
|
||||
|
||||
override fun contains(key: String): Boolean =
|
||||
sharedPreferences.containsKey(key)
|
||||
@@ -36,17 +37,13 @@ class FakeSharedPreferences : SharedPreferences {
|
||||
override fun registerOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
) {
|
||||
throw NotImplementedError(
|
||||
"registerOnSharedPreferenceChangeListener is not currently implemented.",
|
||||
)
|
||||
listeners += listener
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
) {
|
||||
throw NotImplementedError(
|
||||
"unregisterOnSharedPreferenceChangeListener is not currently implemented.",
|
||||
)
|
||||
listeners -= listener
|
||||
}
|
||||
|
||||
private inline fun <reified T> getValue(
|
||||
@@ -61,6 +58,13 @@ class FakeSharedPreferences : SharedPreferences {
|
||||
sharedPreferences.apply {
|
||||
clear()
|
||||
putAll(pendingSharedPreferences)
|
||||
|
||||
// Notify listeners
|
||||
listeners.forEach { listener ->
|
||||
pendingSharedPreferences.keys.forEach { key ->
|
||||
listener.onSharedPreferenceChanged(this@FakeSharedPreferences, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user