From 69a4eef68f01df9496fc056c23c4f55c6a8d5656 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Mon, 16 Oct 2023 13:52:18 -0500 Subject: [PATCH] Add AuthSdkSource (#118) --- app/build.gradle.kts | 3 +- .../data/auth/datasource/sdk/AuthSdkSource.kt | 50 ++++++ .../auth/datasource/sdk/AuthSdkSourceImpl.kt | 69 ++++++++ .../data/auth/datasource/sdk/di/SdkModule.kt | 24 +++ .../datasource/sdk/model/PasswordStrength.kt | 33 ++++ .../sdk/util/PasswordStrengthExtensions.kt | 59 +++++++ .../auth/repository/AuthRepositoryImpl.kt | 18 +- .../data/platform/util/ResultExtensions.kt | 12 ++ .../auth/datasource/sdk/AuthSdkSourceTest.kt | 159 ++++++++++++++++++ .../auth/repository/AuthRepositoryTest.kt | 12 +- .../util/PasswordStrengthExtensionsTest.kt | 151 +++++++++++++++++ .../data/platform/util/ResultTest.kt | 17 ++ 12 files changed, 591 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/di/SdkModule.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/model/PasswordStrength.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/util/PasswordStrengthExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/sdk/util/PasswordStrengthExtensionsTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6811aed45..bf81fb59d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,7 +149,8 @@ koverReport { "*.*NavigationKt*", // Composable singletons "*.*ComposableSingletons*", - + // Generated classes related to interfaces with default values + "*.*DefaultImpls*", // OS-level components "com.x8bit.bitwarden.BitwardenApplication", "com.x8bit.bitwarden.MainActivity*", diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt new file mode 100644 index 000000000..e4b5f02f3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSource.kt @@ -0,0 +1,50 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk + +import com.bitwarden.core.Kdf +import com.bitwarden.core.MasterPasswordPolicyOptions +import com.bitwarden.core.RegisterKeyResponse +import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength + +/** + * Source of authentication information and functionality from the Bitwarden SDK. + */ +interface AuthSdkSource { + /** + * Creates a hashed password provided the given [email], [password], and [kdf]. + * [kdf]. + */ + suspend fun hashPassword( + email: String, + password: String, + kdf: Kdf, + ): Result + + /** + * Creates a set of encryption key information for registraation pers + */ + suspend fun makeRegisterKeys( + email: String, + password: String, + kdf: Kdf, + ): Result + + /** + * Checks the password strength for the given [email] and [password] combination, along with + * some [additionalInputs]. + */ + suspend fun passwordStrength( + email: String, + password: String, + additionalInputs: List = emptyList(), + ): Result + + /** + * Checks that the given [password] with the given [passwordStrength] satisfies the given + * [policy]. Returns `true` if so and `false` otherwise. + */ + suspend fun satisfiesPolicy( + password: String, + passwordStrength: PasswordStrength, + policy: MasterPasswordPolicyOptions, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt new file mode 100644 index 000000000..14ac9fa05 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceImpl.kt @@ -0,0 +1,69 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk + +import com.bitwarden.core.Kdf +import com.bitwarden.core.MasterPasswordPolicyOptions +import com.bitwarden.core.RegisterKeyResponse +import com.bitwarden.sdk.ClientAuth +import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte + +/** + * Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a + * [ClientAuth]. + */ +class AuthSdkSourceImpl( + private val clientAuth: ClientAuth, +) : AuthSdkSource { + + override suspend fun hashPassword( + email: String, + password: String, + kdf: Kdf, + ): Result = runCatching { + clientAuth.hashPassword( + email = email, + password = password, + kdfParams = kdf, + ) + } + + override suspend fun makeRegisterKeys( + email: String, + password: String, + kdf: Kdf, + ): Result = runCatching { + clientAuth.makeRegisterKeys( + email = email, + password = password, + kdf = kdf, + ) + } + + override suspend fun passwordStrength( + email: String, + password: String, + additionalInputs: List, + ): Result = runCatching { + @Suppress("UnsafeCallOnNullableType") + clientAuth + .passwordStrength( + password = password, + email = email, + additionalInputs = additionalInputs, + ) + .toPasswordStrengthOrNull()!! + } + + override suspend fun satisfiesPolicy( + password: String, + passwordStrength: PasswordStrength, + policy: MasterPasswordPolicyOptions, + ): Result = runCatching { + clientAuth.satisfiesPolicy( + password = password, + strength = passwordStrength.toUByte(), + policy = policy, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/di/SdkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/di/SdkModule.kt new file mode 100644 index 000000000..3f585eb4d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/di/SdkModule.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk.di + +import com.bitwarden.sdk.Client +import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource +import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSourceImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Provides SDK-related dependencies for the auth package. + */ +@Module +@InstallIn(SingletonComponent::class) +object SdkModule { + + @Provides + @Singleton + fun provideAuthSdkSource( + client: Client, + ): AuthSdkSource = AuthSdkSourceImpl(clientAuth = client.auth()) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/model/PasswordStrength.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/model/PasswordStrength.kt new file mode 100644 index 000000000..a0180a8dc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/model/PasswordStrength.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk.model + +/** + * An estimate of password strength. + * + * Adapted from [zxcvbn](https://github.com/dropbox/zxcvbn#usage). + */ +enum class PasswordStrength { + /** + * Too guessable; very risky. + */ + LEVEL_0, + + /** + * Very guessable; limited protection. + */ + LEVEL_1, + + /** + * Somewhat guessable; some protection. + */ + LEVEL_2, + + /** + * Safely unguessable; moderate protection. + */ + LEVEL_3, + + /** + * Very unguessable; strong protection. + */ + LEVEL_4, +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/util/PasswordStrengthExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/util/PasswordStrengthExtensions.kt new file mode 100644 index 000000000..67b0f81d3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/sdk/util/PasswordStrengthExtensions.kt @@ -0,0 +1,59 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk.util + +import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength + +/** + * Converts the given [Int] to a [PasswordStrength]. A `null` value is returned if this value is + * not in the [0, 4] range. + */ +@Suppress("MagicNumber") +fun Int.toPasswordStrengthOrNull(): PasswordStrength? = + when (this) { + 0 -> PasswordStrength.LEVEL_0 + 1 -> PasswordStrength.LEVEL_1 + 2 -> PasswordStrength.LEVEL_2 + 3 -> PasswordStrength.LEVEL_3 + 4 -> PasswordStrength.LEVEL_4 + else -> null + } + +/** + * Converts the given [UByte] to a [PasswordStrength]. A `null` value is returned if this value is + * not in the [0, 4] range. + */ +fun UByte.toPasswordStrengthOrNull(): PasswordStrength? = + this.toInt().toPasswordStrengthOrNull() + +/** + * Converts the given [UInt] to a [PasswordStrength]. A `null` value is returned if this value is + * not in the [0, 4] range. + */ +fun UInt.toPasswordStrengthOrNull(): PasswordStrength? = + this.toInt().toPasswordStrengthOrNull() + +/** + * Converts the given [PasswordStrength] to an [Int]. + */ +@Suppress("MagicNumber") +fun PasswordStrength.toInt(): Int = + when (this) { + PasswordStrength.LEVEL_0 -> 0 + PasswordStrength.LEVEL_1 -> 1 + PasswordStrength.LEVEL_2 -> 2 + PasswordStrength.LEVEL_3 -> 3 + PasswordStrength.LEVEL_4 -> 4 + } + +/** + * Converts the given [PasswordStrength] to a [UByte]. + */ +@Suppress("MagicNumber") +fun PasswordStrength.toUByte(): UByte = + this.toInt().toUByte() + +/** + * Converts the given [PasswordStrength] to a [UInt]. + */ +@Suppress("MagicNumber") +fun PasswordStrength.toUInt(): UInt = + this.toInt().toUInt() diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index b7fee8c75..1c0ecf934 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.data.auth.repository -import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson @@ -10,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult +import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import com.x8bit.bitwarden.data.platform.util.flatMap @@ -29,7 +29,7 @@ import javax.inject.Singleton class AuthRepositoryImpl @Inject constructor( private val accountsService: AccountsService, private val identityService: IdentityService, - private val bitwardenSdkClient: Client, + private val authSdkSource: AuthSdkSource, private val authDiskSource: AuthDiskSource, private val authTokenInterceptor: AuthTokenInterceptor, ) : AuthRepository { @@ -55,13 +55,13 @@ class AuthRepositoryImpl @Inject constructor( ): LoginResult = accountsService .preLogin(email = email) .flatMap { - val passwordHash = bitwardenSdkClient - .auth() - .hashPassword( - email = email, - password = password, - kdfParams = it.kdfParams.toSdkParams(), - ) + authSdkSource.hashPassword( + email = email, + password = password, + kdf = it.kdfParams.toSdkParams(), + ) + } + .flatMap { passwordHash -> identityService.getToken( email = email, passwordHash = passwordHash, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt index 72337c27a..daf1a1578 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/util/ResultExtensions.kt @@ -8,3 +8,15 @@ inline fun Result.flatMap(transform: (T) -> Result): Result = this.exceptionOrNull() ?.let { Result.failure(it) } ?: transform(this.getOrThrow()) + +/** + * Returns the given receiver of type [T] as a "success" [Result]. + */ +fun T.asSuccess(): Result = + Result.success(this) + +/** + * Returns the given [Throwable] as a "failure" [Result]. + */ +fun Throwable.asFailure(): Result = + Result.failure(this) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt new file mode 100644 index 000000000..c034f7463 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/sdk/AuthSdkSourceTest.kt @@ -0,0 +1,159 @@ +package com.x8bit.bitwarden.data.auth.datasource.sdk + +import com.bitwarden.core.Kdf +import com.bitwarden.core.MasterPasswordPolicyOptions +import com.bitwarden.core.RegisterKeyResponse +import com.bitwarden.sdk.ClientAuth +import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength +import com.x8bit.bitwarden.data.platform.util.asSuccess +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class AuthSdkSourceTest { + private val clientAuth = mockk() + + private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl( + clientAuth = clientAuth, + ) + + @Test + fun `hashPassword should call SDK and return a Result with the correct data`() = runBlocking { + val email = "email" + val password = "password" + val kdf = mockk() + val expectedResult = "hashedPassword" + coEvery { + clientAuth.hashPassword( + email = email, + password = password, + kdfParams = kdf, + ) + } returns expectedResult + + val result = authSkdSource.hashPassword( + email = email, + password = password, + kdf = kdf, + ) + assertEquals( + expectedResult.asSuccess(), + result, + ) + coVerify { + clientAuth.hashPassword( + email = email, + password = password, + kdfParams = kdf, + ) + } + } + + @Test + fun `makeRegisterKeys should call SDK and return a Result with the correct data`() = + runBlocking { + val email = "email" + val password = "password" + val kdf = mockk() + val expectedResult = mockk() + coEvery { + clientAuth.makeRegisterKeys( + email = email, + password = password, + kdf = kdf, + ) + } returns expectedResult + + val result = authSkdSource.makeRegisterKeys( + email = email, + password = password, + kdf = kdf, + ) + assertEquals( + expectedResult.asSuccess(), + result, + ) + coVerify { + clientAuth.makeRegisterKeys( + email = email, + password = password, + kdf = kdf, + ) + } + } + + // TODO: This test is disabled due to issue here with mocking UByte (BIT-877). + // See: https://github.com/mockk/mockk/issues/544 + @Disabled + @Test + fun `passwordStrength should call SDK and return a Result with the correct data`() = + runBlocking { + val email = "email" + val password = "password" + val additionalInputs = listOf("test1", "test2") + val sdkResult = 3.toUByte() + val expectedResult = PasswordStrength.LEVEL_3 + coEvery { + clientAuth.passwordStrength( + email = email, + password = password, + additionalInputs = additionalInputs, + ) + } returns sdkResult + + val result = authSkdSource.passwordStrength( + email = email, + password = password, + additionalInputs = additionalInputs, + ) + assertEquals( + expectedResult.asSuccess(), + result, + ) + coVerify { + clientAuth.passwordStrength( + email = email, + password = password, + additionalInputs = additionalInputs, + ) + } + } + + @Test + fun `satisfiesPolicy should call SDK and return a Result with the correct data`() = + runBlocking { + val password = "password" + val passwordStrength = PasswordStrength.LEVEL_3 + val rawStrength = 3.toUByte() + val policy = mockk() + val expectedResult = true + coEvery { + clientAuth.satisfiesPolicy( + password = password, + strength = rawStrength, + policy = policy, + ) + } returns expectedResult + + val result = authSkdSource.satisfiesPolicy( + password = password, + passwordStrength = passwordStrength, + policy = policy, + ) + assertEquals( + expectedResult.asSuccess(), + result, + ) + coVerify { + clientAuth.satisfiesPolicy( + password = password, + strength = rawStrength, + policy = policy, + ) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index d53290a8a..b0e6439c8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -1,7 +1,6 @@ package com.x8bit.bitwarden.data.auth.repository import app.cash.turbine.test -import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson @@ -10,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJs import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult +import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor import io.mockk.clearMocks @@ -30,20 +30,20 @@ class AuthRepositoryTest { private val identityService: IdentityService = mockk() private val authInterceptor = mockk() private val fakeAuthDiskSource = FakeAuthDiskSource() - private val mockBitwardenSdk = mockk { + private val authSdkSource = mockk { coEvery { - auth().hashPassword( + hashPassword( email = EMAIL, password = PASSWORD, - kdfParams = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(), + kdf = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(), ) - } returns PASSWORD_HASH + } returns Result.success(PASSWORD_HASH) } private val repository = AuthRepositoryImpl( accountsService = accountsService, identityService = identityService, - bitwardenSdkClient = mockBitwardenSdk, + authSdkSource = authSdkSource, authDiskSource = fakeAuthDiskSource, authTokenInterceptor = authInterceptor, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/sdk/util/PasswordStrengthExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/sdk/util/PasswordStrengthExtensionsTest.kt new file mode 100644 index 000000000..72095d7d3 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/sdk/util/PasswordStrengthExtensionsTest.kt @@ -0,0 +1,151 @@ +package com.x8bit.bitwarden.data.platform.datasource.sdk.util + +import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte +import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUInt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class PasswordStrengthExtensionsTest { + @Nested + inner class IntegerType { + @Test + fun `toPasswordStrengthOrNull returns the correct values in 0 to 4 range`() { + mapOf( + 0 to PasswordStrength.LEVEL_0, + 1 to PasswordStrength.LEVEL_1, + 2 to PasswordStrength.LEVEL_2, + 3 to PasswordStrength.LEVEL_3, + 4 to PasswordStrength.LEVEL_4, + ) + .forEach { (intValue, level) -> + assertEquals( + level, + intValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toPasswordStrengthOrNull returns null outside the 0 to 4 range`() { + listOf(-2, -1, 5, 6).forEach { intValue -> + assertNull( + intValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toInt returns the correct Int for each level`() { + mapOf( + PasswordStrength.LEVEL_0 to 0, + PasswordStrength.LEVEL_1 to 1, + PasswordStrength.LEVEL_2 to 2, + PasswordStrength.LEVEL_3 to 3, + PasswordStrength.LEVEL_4 to 4, + ) + .forEach { (level, intValue) -> + assertEquals( + intValue, + level.toInt(), + ) + } + } + } + + @Nested + inner class UByteType { + @Test + fun `toPasswordStrengthOrNull returns the correct values in 0 to 4 range`() { + mapOf( + 0.toUByte() to PasswordStrength.LEVEL_0, + 1.toUByte() to PasswordStrength.LEVEL_1, + 2.toUByte() to PasswordStrength.LEVEL_2, + 3.toUByte() to PasswordStrength.LEVEL_3, + 4.toUByte() to PasswordStrength.LEVEL_4, + ) + .forEach { (uByteValue, level) -> + assertEquals( + level, + uByteValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toPasswordStrengthOrNull returns null outside the 0 to 4 range`() { + listOf(5.toUByte(), 6.toUByte()).forEach { uByteValue -> + assertNull( + uByteValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toUByte returns the correct UByte for each level`() { + mapOf( + PasswordStrength.LEVEL_0 to 0.toUByte(), + PasswordStrength.LEVEL_1 to 1.toUByte(), + PasswordStrength.LEVEL_2 to 2.toUByte(), + PasswordStrength.LEVEL_3 to 3.toUByte(), + PasswordStrength.LEVEL_4 to 4.toUByte(), + ) + .forEach { (level, uByteValue) -> + assertEquals( + uByteValue, + level.toUByte(), + ) + } + } + } + + @Nested + inner class UIntType { + @Test + fun `toPasswordStrengthOrNull returns the correct values in 0 to 4 range`() { + mapOf( + 0u to PasswordStrength.LEVEL_0, + 1u to PasswordStrength.LEVEL_1, + 2u to PasswordStrength.LEVEL_2, + 3u to PasswordStrength.LEVEL_3, + 4u to PasswordStrength.LEVEL_4, + ) + .forEach { (uIntValue, level) -> + assertEquals( + level, + uIntValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toPasswordStrengthOrNull returns null outside the 0 to 4 range`() { + listOf(5u, 6u).forEach { uIntValue -> + assertNull( + uIntValue.toPasswordStrengthOrNull(), + ) + } + } + + @Test + fun `toUInt returns the correct UInt for each level`() { + mapOf( + PasswordStrength.LEVEL_0 to 0u, + PasswordStrength.LEVEL_1 to 1u, + PasswordStrength.LEVEL_2 to 2u, + PasswordStrength.LEVEL_3 to 3u, + PasswordStrength.LEVEL_4 to 4u, + ) + .forEach { (level, uIntValue) -> + assertEquals( + uIntValue, + level.toUInt(), + ) + } + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt index 25a4db865..2f911965a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/util/ResultTest.kt @@ -49,4 +49,21 @@ class ResultTest { assertTrue(stringResult.isFailure) assertEquals(expectedException, stringResult.exceptionOrNull()) } + + @Test + fun `asSuccess returns a success Result with the correct content`() { + assertEquals( + Result.success("Test"), + "Test".asSuccess(), + ) + } + + @Test + fun `asFailure returns a failure Result with the correct content`() { + val throwable = IllegalStateException("Test") + assertEquals( + Result.failure(throwable), + throwable.asFailure(), + ) + } }