From 698d8c745bcbf28df5ca56e2e8475cff8eaf5ebd Mon Sep 17 00:00:00 2001 From: joshua-livefront <139182194+joshua-livefront@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:08:37 -0500 Subject: [PATCH] BIT-654: Generator SDK interface and repository implementation (#233) --- .../datasource/sdk/GeneratorSdkSource.kt | 14 ++++ .../datasource/sdk/GeneratorSdkSourceImpl.kt | 20 +++++ .../datasource/sdk/di/GeneratorSdkModule.kt | 24 ++++++ .../repository/GeneratorRepository.kt | 17 +++++ .../repository/GeneratorRepositoryImpl.kt | 25 ++++++ .../di/GeneratorRepositoryModule.kt | 24 ++++++ .../model/GeneratedPasswordResult.kt | 17 +++++ .../datasource/sdk/GeneratorSdkSourceTest.kt | 45 +++++++++++ .../repository/GeneratorRepositoryTest.kt | 76 +++++++++++++++++++ 9 files changed, 262 insertions(+) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSource.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/di/GeneratorSdkModule.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepository.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/repository/di/GeneratorRepositoryModule.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/generator/repository/model/GeneratedPasswordResult.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSource.kt new file mode 100644 index 0000000000..d19bd7bd5f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSource.kt @@ -0,0 +1,14 @@ +package com.x8bit.bitwarden.data.generator.datasource.sdk + +import com.bitwarden.core.PasswordGeneratorRequest + +/** + * Source of password generation functionality from the Bitwarden SDK. + */ +interface GeneratorSdkSource { + + /** + * Generates a password returning a [String] wrapped in a [Result]. + */ + suspend fun generatePassword(request: PasswordGeneratorRequest): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceImpl.kt new file mode 100644 index 0000000000..a4091f65d1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceImpl.kt @@ -0,0 +1,20 @@ +package com.x8bit.bitwarden.data.generator.datasource.sdk + +import com.bitwarden.core.PasswordGeneratorRequest +import com.bitwarden.sdk.ClientGenerators + +/** + * Implementation of [GeneratorSdkSource] that delegates password generation. + * + * @property clientGenerator An instance of [ClientGenerators] provided by the Bitwarden SDK. + */ +class GeneratorSdkSourceImpl( + private val clientGenerator: ClientGenerators, +) : GeneratorSdkSource { + + override suspend fun generatePassword( + request: PasswordGeneratorRequest, + ): Result = runCatching { + clientGenerator.password(request) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/di/GeneratorSdkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/di/GeneratorSdkModule.kt new file mode 100644 index 0000000000..12b2281d71 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/datasource/sdk/di/GeneratorSdkModule.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.data.generator.datasource.sdk.di + +import com.bitwarden.sdk.Client +import com.x8bit.bitwarden.data.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.generator.datasource.sdk.GeneratorSdkSourceImpl +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 password generation package. + */ +@Module +@InstallIn(SingletonComponent::class) +object GeneratorSdkModule { + + @Provides + @Singleton + fun provideGeneratorSdkSource( + client: Client, + ): GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerator = client.generators()) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepository.kt new file mode 100644 index 0000000000..725c2b01e1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepository.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.generator.repository + +import com.bitwarden.core.PasswordGeneratorRequest +import com.x8bit.bitwarden.data.generator.repository.model.GeneratedPasswordResult + +/** + * Responsible for managing generator data. + */ +interface GeneratorRepository { + + /** + * Attempt to generate a password. + */ + suspend fun generatePassword( + passwordGeneratorRequest: PasswordGeneratorRequest, + ): GeneratedPasswordResult +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryImpl.kt new file mode 100644 index 0000000000..7db2de46e2 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryImpl.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.generator.repository + +import com.bitwarden.core.PasswordGeneratorRequest +import com.x8bit.bitwarden.data.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.generator.repository.model.GeneratedPasswordResult +import javax.inject.Singleton + +/** + * Default implementation of [GeneratorRepository]. + */ +@Singleton +class GeneratorRepositoryImpl constructor( + private val generatorSdkSource: GeneratorSdkSource, +) : GeneratorRepository { + + override suspend fun generatePassword( + passwordGeneratorRequest: PasswordGeneratorRequest, + ): GeneratedPasswordResult = + generatorSdkSource + .generatePassword(passwordGeneratorRequest) + .fold( + onSuccess = { GeneratedPasswordResult.Success(it) }, + onFailure = { GeneratedPasswordResult.InvalidRequest }, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/di/GeneratorRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/di/GeneratorRepositoryModule.kt new file mode 100644 index 0000000000..7ab1306c53 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/di/GeneratorRepositoryModule.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.data.generator.repository.di + +import com.x8bit.bitwarden.data.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.generator.repository.GeneratorRepository +import com.x8bit.bitwarden.data.generator.repository.GeneratorRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Provides repositories in the generator package. + */ +@Module +@InstallIn(SingletonComponent::class) +object GeneratorRepositoryModule { + + @Provides + @Singleton + fun provideGeneratorRepository( + generatorSdkSource: GeneratorSdkSource, + ): GeneratorRepository = GeneratorRepositoryImpl(generatorSdkSource) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/model/GeneratedPasswordResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/model/GeneratedPasswordResult.kt new file mode 100644 index 0000000000..d73568e6f8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/generator/repository/model/GeneratedPasswordResult.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.generator.repository.model + +/** + * Represents the outcome of a generator operation. + */ +sealed class GeneratedPasswordResult { + + /** + * Operation succeeded with a value. + */ + data class Success(val generatedString: String) : GeneratedPasswordResult() + + /** + * There was an error during the operation. + */ + data object InvalidRequest : GeneratedPasswordResult() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceTest.kt new file mode 100644 index 0000000000..e982c6107b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/generator/datasource/sdk/GeneratorSdkSourceTest.kt @@ -0,0 +1,45 @@ +package com.x8bit.bitwarden.data.generator.datasource.sdk + +import com.bitwarden.core.PasswordGeneratorRequest +import com.bitwarden.sdk.ClientGenerators +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test + +class GeneratorSdkSourceTest { + private val clientGenerators = mockk() + private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerators) + + @Suppress("MaxLineLength") + @Test + fun `generatePassword should call SDK and return a Result with the generated password`() = runBlocking { + val request = PasswordGeneratorRequest( + lowercase = true, + uppercase = true, + numbers = true, + special = true, + length = 12.toUByte(), + avoidAmbiguous = false, + minLowercase = true, + minUppercase = true, + minNumber = true, + minSpecial = true, + ) + val expectedResult = "GeneratedPassword123!" + + coEvery { + clientGenerators.password(request) + } returns expectedResult + + val result = generatorSdkSource.generatePassword(request) + + assertEquals(Result.success(expectedResult), result) + + coVerify { + clientGenerators.password(request) + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryTest.kt new file mode 100644 index 0000000000..e593a5c6eb --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/generator/repository/GeneratorRepositoryTest.kt @@ -0,0 +1,76 @@ +package com.x8bit.bitwarden.data.generator.repository + +import com.bitwarden.core.PasswordGeneratorRequest +import com.x8bit.bitwarden.data.generator.datasource.sdk.GeneratorSdkSource +import com.x8bit.bitwarden.data.generator.repository.model.GeneratedPasswordResult +import io.mockk.clearMocks +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.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GeneratorRepositoryTest { + + private val generatorSdkSource: GeneratorSdkSource = mockk() + + private val repository = GeneratorRepositoryImpl( + generatorSdkSource = generatorSdkSource, + ) + + @BeforeEach + fun setUp() { + clearMocks(generatorSdkSource) + } + + @Test + fun `generatePassword should emit Success result with the generated password`() = runTest { + val request = PasswordGeneratorRequest( + lowercase = true, + uppercase = true, + numbers = true, + special = true, + length = 12.toUByte(), + avoidAmbiguous = false, + minLowercase = null, + minUppercase = null, + minNumber = null, + minSpecial = null, + ) + val expectedResult = "GeneratedPassword123!" + coEvery { + generatorSdkSource.generatePassword(request) + } returns Result.success(expectedResult) + + val result = repository.generatePassword(request) + + assertEquals(expectedResult, (result as GeneratedPasswordResult.Success).generatedString) + coVerify { generatorSdkSource.generatePassword(request) } + } + + @Test + fun `generatePassword should emit InvalidRequest result when SDK throws exception`() = runTest { + val request = PasswordGeneratorRequest( + lowercase = true, + uppercase = true, + numbers = true, + special = true, + length = 12.toUByte(), + avoidAmbiguous = false, + minLowercase = null, + minUppercase = null, + minNumber = null, + minSpecial = null, + ) + val exception = RuntimeException("An error occurred") + coEvery { generatorSdkSource.generatePassword(request) } returns Result.failure(exception) + + val result = repository.generatePassword(request) + + assertTrue(result is GeneratedPasswordResult.InvalidRequest) + coVerify { generatorSdkSource.generatePassword(request) } + } +}