diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 2e823d19f..248b3c25b 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -13,4 +13,15 @@ dependencies { implementation(Dependencies.coroutinesDependency) // Paging implementation(Dependencies.pagingCommon) + // Required for Coroutines + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") + + testImplementation ("org.junit.jupiter:junit-jupiter-api:5.8.2") + testImplementation ("org.junit.jupiter:junit-jupiter-engine:5.8.2") + testImplementation ("org.mockito:mockito-junit-jupiter:3.11.2") + testImplementation("org.junit.jupiter:junit-jupiter:5.8.1") } +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/core/domain/src/main/java/org/the_chance/honeymart/domain/repository/AuthRepository.kt b/core/domain/src/main/java/org/the_chance/honeymart/domain/repository/AuthRepository.kt index 160852d18..6d3d99635 100644 --- a/core/domain/src/main/java/org/the_chance/honeymart/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/org/the_chance/honeymart/domain/repository/AuthRepository.kt @@ -4,8 +4,12 @@ import org.the_chance.honeymart.domain.model.AdminLogin import org.the_chance.honeymart.domain.model.Owner import org.the_chance.honeymart.domain.model.OwnerProfile import org.the_chance.honeymart.domain.usecase.Tokens +import org.the_chance.honeymart.domain.util.InvalidPasswordInputException +import org.the_chance.honeymart.domain.util.InvalidUserNameOrPasswordException +import org.the_chance.honeymart.domain.util.UnKnownUserException interface AuthRepository { + @Throws(InvalidUserNameOrPasswordException::class) suspend fun loginUser( email: String, password: String, @@ -13,7 +17,7 @@ interface AuthRepository { ): Tokens suspend fun refreshToken(refreshToken: String): Tokens - + @Throws(InvalidPasswordInputException::class) suspend fun saveTokens(accessToken: String, refreshToken: String) fun getAccessToken(): String? @@ -22,7 +26,7 @@ interface AuthRepository { suspend fun clearToken() suspend fun registerUser(fullName: String, password: String, email: String): Boolean - + @Throws(UnKnownUserException::class) suspend fun getDeviceToken(): String suspend fun createOwnerAccount(fullName: String, email: String, password: String): Boolean diff --git a/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/IValidationUseCase.kt b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/IValidationUseCase.kt new file mode 100644 index 000000000..710ee7ea9 --- /dev/null +++ b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/IValidationUseCase.kt @@ -0,0 +1,10 @@ +package org.the_chance.honeymart.domain.usecase + +import org.the_chance.honeymart.domain.util.ValidationState + +interface IValidationUseCase { + fun validateEmail(email: String): ValidationState + fun validateConfirmPassword(password: String, repeatedPassword: String): Boolean + fun validationFullName(fullName: String): ValidationState + fun validationPassword(password: String): ValidationState +} \ No newline at end of file diff --git a/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCase.kt b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCase.kt index 795d1c469..8f583a01a 100644 --- a/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCase.kt +++ b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCase.kt @@ -1,14 +1,33 @@ package org.the_chance.honeymart.domain.usecase import org.the_chance.honeymart.domain.repository.AuthRepository +import org.the_chance.honeymart.domain.util.InvalidEmailException +import org.the_chance.honeymart.domain.util.InvalidPasswordInputException +import org.the_chance.honeymart.domain.util.ValidationState import javax.inject.Inject class LoginUserUseCase @Inject constructor( private val authRepository: AuthRepository, + private val validationUseCase: IValidationUseCase ) { suspend operator fun invoke(email: String, password: String) { + when (validationUseCase.validateEmail(email)) { + ValidationState.BLANK_EMAIL, ValidationState.INVALID_EMAIL -> throw InvalidEmailException() + else -> {} + } + + when (validationUseCase.validationPassword(password)) { + ValidationState.BLANK_PASSWORD, ValidationState.INVALID_PASSWORD, + ValidationState.INVALID_PASSWORD_LENGTH_SHORT -> throw InvalidPasswordInputException() + else -> {} + } + val deviceToken = authRepository.getDeviceToken() - val tokens = authRepository.loginUser(email, password, deviceToken) - authRepository.saveTokens(tokens.accessToken, tokens.refreshToken) + try { + val tokens = authRepository.loginUser(email, password, deviceToken) + authRepository.saveTokens(tokens.accessToken, tokens.refreshToken) + } catch (e: Exception) { + throw InvalidPasswordInputException() + } } } \ No newline at end of file diff --git a/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/ValidationUseCase.kt b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/ValidationUseCase.kt index cba3aba42..fe8627984 100644 --- a/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/ValidationUseCase.kt +++ b/core/domain/src/main/java/org/the_chance/honeymart/domain/usecase/ValidationUseCase.kt @@ -4,9 +4,9 @@ import org.the_chance.honeymart.domain.util.ValidationState import java.util.regex.Pattern import javax.inject.Inject -class ValidationUseCase @Inject constructor() { +class ValidationUseCase @Inject constructor() : IValidationUseCase { - fun validateEmail(email: String): ValidationState { + override fun validateEmail(email: String): ValidationState { if (email.isBlank()) { return ValidationState.BLANK_EMAIL } @@ -16,10 +16,10 @@ class ValidationUseCase @Inject constructor() { return ValidationState.VALID_EMAIL } - fun validateConfirmPassword(password: String, repeatedPassword: String) = + override fun validateConfirmPassword(password: String, repeatedPassword: String) = password == repeatedPassword - fun validationFullName(fullName: String): ValidationState { + override fun validationFullName(fullName: String): ValidationState { if (fullName.isBlank()) { return ValidationState.BLANK_FULL_NAME } @@ -31,7 +31,7 @@ class ValidationUseCase @Inject constructor() { } - fun validationPassword(password: String): ValidationState { + override fun validationPassword(password: String): ValidationState { return when { password.isBlank() -> { ValidationState.BLANK_PASSWORD diff --git a/core/domain/src/main/java/org/the_chance/honeymart/domain/util/ValidationState.kt b/core/domain/src/main/java/org/the_chance/honeymart/domain/util/ValidationState.kt index 15f7bbbb1..95b2af17f 100644 --- a/core/domain/src/main/java/org/the_chance/honeymart/domain/util/ValidationState.kt +++ b/core/domain/src/main/java/org/the_chance/honeymart/domain/util/ValidationState.kt @@ -29,6 +29,7 @@ enum class ValidationState { PASSWORD_REGEX_ERROR_DIGIT, PASSWORD_REGEX_ERROR_SPECIAL_CHARACTER, VALID_PASSWORD, + SHORT_PASSWORD, CONFIRM_PASSWORD_DOES_NOT_MATCH, CONFIRM_PASSWORD_MATCH, diff --git a/core/domain/src/test/java/org/the_chance/honeymart/ExampleUnitTest.kt b/core/domain/src/test/java/org/the_chance/honeymart/ExampleUnitTest.kt new file mode 100644 index 000000000..cddb12fa2 --- /dev/null +++ b/core/domain/src/test/java/org/the_chance/honeymart/ExampleUnitTest.kt @@ -0,0 +1,20 @@ +package org.the_chance.honeymart + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +//import org.junit.Assert.assertEquals +//import org.junit.Test + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } + +} \ No newline at end of file diff --git a/core/domain/src/test/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCaseTest.kt b/core/domain/src/test/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCaseTest.kt new file mode 100644 index 000000000..53ae9bdd1 --- /dev/null +++ b/core/domain/src/test/java/org/the_chance/honeymart/domain/usecase/LoginUserUseCaseTest.kt @@ -0,0 +1,156 @@ +package org.the_chance.honeymart.domain.usecase + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.* +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.jupiter.MockitoExtension +import org.the_chance.honeymart.domain.repository.AuthRepository +import org.the_chance.honeymart.domain.util.InvalidEmailException +import org.the_chance.honeymart.domain.util.InvalidPasswordInputException +import org.the_chance.honeymart.domain.util.InvalidUserNameOrPasswordException +import org.the_chance.honeymart.domain.util.UnKnownUserException +import org.the_chance.honeymart.domain.util.ValidationState + +@ExtendWith(MockitoExtension::class) +class LoginUserUseCaseTest { + + @Mock + private lateinit var mockAuthRepository: AuthRepository + + @InjectMocks + private lateinit var loginUserUseCase: LoginUserUseCase + + @Mock + private lateinit var mockValidationUseCase: IValidationUseCase + + @BeforeEach + fun setup() { + reset(mockAuthRepository) + lenient().`when`(mockValidationUseCase.validateEmail(TEST_EMAIL)) + .thenReturn(ValidationState.VALID_EMAIL) + lenient().`when`(mockValidationUseCase.validationPassword(TEST_PASSWORD)) + .thenReturn(ValidationState.VALID_PASSWORD) + } + + @Test + fun `given valid email and password, when login is invoked, then tokens are saved`() = + runBlocking { + `when`(mockAuthRepository.getDeviceToken()).thenReturn(TEST_DEVICE_TOKEN) + `when`( + mockAuthRepository.loginUser( + TEST_EMAIL, + TEST_PASSWORD, + TEST_DEVICE_TOKEN + ) + ).thenReturn(TEST_TOKENS) + + loginUserUseCase(TEST_EMAIL, TEST_PASSWORD) + + verify(mockAuthRepository).getDeviceToken() + verify(mockAuthRepository) + .loginUser(TEST_EMAIL, TEST_PASSWORD, TEST_DEVICE_TOKEN) + verify(mockAuthRepository) + .saveTokens(TEST_TOKENS.accessToken, TEST_TOKENS.refreshToken) + } + + @Test + fun `given getDeviceToken fails, when login is invoked, then ensure no further calls are made`() = + runBlocking { + `when`(mockAuthRepository.getDeviceToken()) + .thenThrow(UnKnownUserException::class.java) + + assertThrows { loginUserUseCase(TEST_EMAIL, TEST_PASSWORD) } + + verify(mockAuthRepository).getDeviceToken() + verifyNoMoreInteractions(mockAuthRepository) + } + + @Test + fun `given loginUser fails, when login is invoked, then tokens are not saved`() = runBlocking { + // Arrange + `when`(mockAuthRepository.getDeviceToken()).thenReturn(TEST_DEVICE_TOKEN) + `when`(mockAuthRepository.loginUser(TEST_EMAIL, TEST_PASSWORD, TEST_DEVICE_TOKEN)) + .thenThrow(InvalidUserNameOrPasswordException::class.java) + + assertThrows { + // Act + loginUserUseCase(TEST_EMAIL, TEST_PASSWORD) + } + + // Assert + verify(mockAuthRepository).getDeviceToken() + verify(mockAuthRepository).loginUser(TEST_EMAIL, TEST_PASSWORD, TEST_DEVICE_TOKEN) + verify(mockAuthRepository, never()).saveTokens( + TEST_TOKENS.accessToken, + TEST_TOKENS.refreshToken + ) + } + + @ParameterizedTest + @EnumSource(ValidationState::class, names = ["BLANK_PASSWORD", "SHORT_PASSWORD", "INVALID_PASSWORD","INVALID_PASSWORD_LENGTH_SHORT","INVALID_PASSWORD_LENGTH_LONG", + "INVALID_CONFIRM_PASSWORD", "PASSWORD_REGEX_ERROR_LETTER","PASSWORD_REGEX_ERROR_DIGIT", "PASSWORD_REGEX_ERROR_SPECIAL_CHARACTER"]) + fun `given invalid passwords, ensure repository calls are not made`(invalidState: ValidationState) = runBlocking { + println("Testing with ValidationState: $invalidState") + + `when`(mockValidationUseCase.validationPassword(anyString())).thenReturn(invalidState) + + try { + loginUserUseCase(TEST_EMAIL, "invalidPassword") + Assertions.fail("Expected an InvalidPasswordInputException to be thrown.") + } catch (e: Exception) { + if (e is NullPointerException) { + println("NullPointerException encountered. Printing stack trace...") + e.printStackTrace() + } else { + Assertions.assertTrue( + e is InvalidPasswordInputException, + "Expected InvalidPasswordInputException but received ${e::class.java}" + ) + } + } + } + + + @Test + fun `given token saving fails, when login is , then appropriate error is thrown`() = + runBlocking { + `when`(mockAuthRepository.getDeviceToken()).thenReturn(TEST_DEVICE_TOKEN) + `when`( + mockAuthRepository.loginUser( + TEST_EMAIL, + TEST_PASSWORD, + TEST_DEVICE_TOKEN + ) + ).thenReturn(TEST_TOKENS) + doThrow(InvalidPasswordInputException()).`when`(mockAuthRepository).saveTokens( + TEST_TOKENS.accessToken, + TEST_TOKENS.refreshToken + ) + + assertThrows { loginUserUseCase(TEST_EMAIL, TEST_PASSWORD) } + } + + @ParameterizedTest + @ValueSource(strings = ["", "test@", "test.com"]) + fun `given invalid email formats, ensure repository calls are not made`(email: String) = + runBlocking { + `when`(mockValidationUseCase.validateEmail(email)).thenReturn(ValidationState.INVALID_EMAIL) + + assertThrows { loginUserUseCase(email, TEST_PASSWORD) } + verifyNoInteractions(mockAuthRepository) + } + + + companion object { + private const val TEST_EMAIL = "test@example.com" + private const val TEST_PASSWORD = "password" + private const val TEST_DEVICE_TOKEN = "deviceToken" + private val TEST_TOKENS = Tokens("accessToken", "refreshToken") + } +} \ No newline at end of file diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 82f4f21b2..d728a0a8b 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -97,4 +97,5 @@ dependencies { //Permission implementation("com.google.accompanist:accompanist-permissions:0.28.0") + androidTestImplementation ("com.google.truth:truth:1.1.4") } diff --git a/user/src/androidTest/java/org/the_chance/honeymart/ui/feature/home/search/SearchBarTest.kt b/user/src/androidTest/java/org/the_chance/honeymart/ui/feature/home/search/SearchBarTest.kt new file mode 100644 index 000000000..12ea537d3 --- /dev/null +++ b/user/src/androidTest/java/org/the_chance/honeymart/ui/feature/home/search/SearchBarTest.kt @@ -0,0 +1,85 @@ +package org.the_chance.honeymart.ui.feature.home.search + +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.google.common.truth.Truth +import org.junit.Rule +import org.junit.Test +import org.the_chance.design_system.R +import org.the_chance.honeymart.ui.feature.home.composables.SearchBar + +class SearchBarTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun checkTextIsDisplayed() { + composeTestRule.setContent { + SearchBar( + icon = painterResource(id = R.drawable.ic_search), + text = "search", + onClick = { /*TODO*/ }) + } + + composeTestRule.onNodeWithText("search").assertIsDisplayed() + } + + @Test + fun checkIconIsDisplayed() { + composeTestRule.setContent { + SearchBar( + icon = painterResource(id = R.drawable.ic_search), + onClick = { /*TODO*/ }) + } + + composeTestRule.onNodeWithContentDescription("SearchIcon").assertIsDisplayed() + } + + @Test + fun checkSearchBarClick() { + var isClicked = false + + composeTestRule.setContent { + SearchBar( + icon = painterResource(id = R.drawable.ic_search), + onClick = { isClicked = true }) + } + composeTestRule.onNodeWithContentDescription("SearchWidget").performClick() + Truth.assertThat(isClicked).isTrue() + } + + @Test + fun searchBar_DisplaysText_Search() { + testDisplayText("search") + } + + @Test + fun searchBar_DisplaysText_Find() { + testDisplayText("find") + } + + @Test + fun searchBar_DisplaysText_Lookup() { + testDisplayText("lookup") + } + + private fun testDisplayText(text: String) { + composeTestRule.setContent { + SearchBar( + icon = painterResource(id = R.drawable.ic_search), + text = text, + onClick = { /*TODO*/ } + ) + } + + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText(text).assertIsDisplayed() + } + +} \ No newline at end of file diff --git a/user/src/main/java/org/the_chance/honeymart/di/RepositoryModule.kt b/user/src/main/java/org/the_chance/honeymart/di/RepositoryModule.kt index d3fd232be..5030e3de4 100644 --- a/user/src/main/java/org/the_chance/honeymart/di/RepositoryModule.kt +++ b/user/src/main/java/org/the_chance/honeymart/di/RepositoryModule.kt @@ -2,12 +2,15 @@ package org.the_chance.honeymart.di import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.the_chance.honeymart.data.repository.AuthRepositoryImp import org.the_chance.honeymart.data.repository.HoneyMartRepositoryImp import org.the_chance.honeymart.domain.repository.AuthRepository import org.the_chance.honeymart.domain.repository.HoneyMartRepository +import org.the_chance.honeymart.domain.usecase.IValidationUseCase +import org.the_chance.honeymart.domain.usecase.ValidationUseCase import javax.inject.Singleton @Module @@ -21,5 +24,4 @@ internal abstract class RepositoryModule { @Singleton @Binds abstract fun bindAuthRepository(repository: AuthRepositoryImp): AuthRepository -} - +} \ No newline at end of file diff --git a/user/src/main/java/org/the_chance/honeymart/di/UseCaseModule.kt b/user/src/main/java/org/the_chance/honeymart/di/UseCaseModule.kt new file mode 100644 index 000000000..88ac24b8e --- /dev/null +++ b/user/src/main/java/org/the_chance/honeymart/di/UseCaseModule.kt @@ -0,0 +1,17 @@ +package org.the_chance.honeymart.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.the_chance.honeymart.domain.usecase.IValidationUseCase +import org.the_chance.honeymart.domain.usecase.ValidationUseCase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class UseCaseModule { + @Singleton + @Binds + abstract fun providesValidationUseCase(useCase: ValidationUseCase): IValidationUseCase +} \ No newline at end of file diff --git a/user/src/main/java/org/the_chance/honeymart/ui/feature/home/composables/SearchBar.kt b/user/src/main/java/org/the_chance/honeymart/ui/feature/home/composables/SearchBar.kt index 61067058b..351977d7a 100644 --- a/user/src/main/java/org/the_chance/honeymart/ui/feature/home/composables/SearchBar.kt +++ b/user/src/main/java/org/the_chance/honeymart/ui/feature/home/composables/SearchBar.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import org.the_chance.design_system.R import org.the_chance.honymart.ui.theme.HoneyMartTheme @@ -28,12 +30,16 @@ import org.the_chance.honymart.ui.theme.dimens fun SearchBar( icon: Painter, onClick: () -> Unit, + text : String = stringResource(R.string.search), modifier: Modifier = Modifier, ) { Surface( modifier = modifier .clip(shape = MaterialTheme.shapes.medium) - .clickable { onClick() }, + .clickable { onClick() } + .semantics { + contentDescription = "SearchWidget" + }, color = MaterialTheme.colorScheme.tertiaryContainer, shape = MaterialTheme.shapes.medium, border = BorderStroke( @@ -55,12 +61,18 @@ fun SearchBar( painter = icon, contentDescription = null, modifier = Modifier - .size(MaterialTheme.dimens.icon24), + .size(MaterialTheme.dimens.icon24) + .semantics { + contentDescription = "SearchIcon" + }, tint = MaterialTheme.colorScheme.onBackground ) Text( - text = stringResource(R.string.search), + modifier = Modifier.semantics { + contentDescription = "TextFiled" + }, + text = text, style = Typography.displaySmall.copy(MaterialTheme.colorScheme.onSecondaryContainer), ) } diff --git a/user/src/main/java/org/the_chance/honeymart/util/ValidationHelper.kt b/user/src/main/java/org/the_chance/honeymart/util/ValidationHelper.kt index a4edbc422..d18bd2cf5 100644 --- a/user/src/main/java/org/the_chance/honeymart/util/ValidationHelper.kt +++ b/user/src/main/java/org/the_chance/honeymart/util/ValidationHelper.kt @@ -44,6 +44,7 @@ internal fun handleValidation(validationStat: ValidationState): Int { ValidationState.INVALID_COUPON_DISCOUNT_PERCENTAGE -> TODO() ValidationState.INVALID_COUPON_COUNT -> TODO() ValidationState.INVALID_PASSWORD_LENGTH_LONG -> TODO() + else -> {TODO()} } }