diff --git a/build.gradle.kts b/build.gradle.kts index e230fa1..a94367b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,5 +58,5 @@ tasks.test { } kotlin { - jvmToolchain(23) + jvmToolchain(22) } \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index fba2b4a..b542887 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -4,7 +4,6 @@ import com.berlin.di.uiModule import com.berlin.di.useCaseModule import com.berlin.di.* import com.berlin.presentation.MainMenuUI -import com.berlin.presentation.authService.AuthenticateUserUi import org.koin.core.context.startKoin import org.koin.mp.KoinPlatform.getKoin diff --git a/src/main/kotlin/data/DummyData.kt b/src/main/kotlin/data/DummyData.kt index 3535e7f..8b27e55 100644 --- a/src/main/kotlin/data/DummyData.kt +++ b/src/main/kotlin/data/DummyData.kt @@ -19,6 +19,7 @@ object DummyData : BaseDataSource { ) val states = mutableListOf( + State("Q1","Menna","P5"), State("S1", "TODO", "P1"), State("S2", "IN_PROGRESS", "P1"), State("S3", "REVIEW", "P1"), diff --git a/src/main/kotlin/data/state/StateRepositoryImpl.kt b/src/main/kotlin/data/state/StateRepositoryImpl.kt index c7c1885..ca47270 100644 --- a/src/main/kotlin/data/state/StateRepositoryImpl.kt +++ b/src/main/kotlin/data/state/StateRepositoryImpl.kt @@ -2,54 +2,60 @@ package com.berlin.data.state import com.berlin.data.BaseDataSource import com.berlin.domain.exception.InvalidStateException +import com.berlin.domain.exception.StateNotFoundException import com.berlin.domain.model.State import com.berlin.domain.model.Task import com.berlin.domain.repository.StateRepository +import kotlin.Result.Companion.failure +import kotlin.Result.Companion.success class StateRepositoryImpl( private val stateDataSource: BaseDataSource, - private val taskDataSource: BaseDataSource -):StateRepository { - override fun addState(state: State): Result { - return if (stateDataSource.write(state)) - Result.success(state.id) + private val taskDataSource: BaseDataSource, +) : StateRepository { + override fun addState(state: State): Result = + if (stateDataSource.write(state)) + success(state.id) else - Result.failure(InvalidStateException("can not add state")) - } + failure(InvalidStateException("can not add state")) - override fun getStatesByProjectId(projectId: String): List? { - return stateDataSource.getAll() - .filter { it.projectId == projectId } - .takeIf { it.isNotEmpty() } - } - override fun getTasksByStateId(stateId: String): List? { - return taskDataSource.getAll() + override fun getStatesByProjectId(projectId: String): Result> = + success(stateDataSource.getAll().filter { it.projectId == projectId }) + + + override fun getTasksByStateId(stateId: String): List? = + taskDataSource.getAll() .filter { it.stateId == stateId } .takeIf { it.isNotEmpty() } - } - override fun deleteState(stateId: String): Result { - return if (stateDataSource.delete(stateId)) - Result.success(stateId) + + override fun deleteState(stateId: String): Result = + if (stateDataSource.delete(stateId)) + success(stateId) else - Result.failure(InvalidStateException("can not delete state")) - } + failure(InvalidStateException("can not delete state")) - override fun updateState(state: State): Result { - return if (stateDataSource.update(state.id,state)) - Result.success(state.id) + + override fun updateState(state: State): Result = + if (stateDataSource.update(state.id, state)) + success(state.id) else - Result.failure(InvalidStateException("can not update state")) - } + failure(InvalidStateException("can not update state")) + override fun getStateByTaskId(taskId: String): State? { - return taskDataSource - .getById(taskId) - ?.let { stateDataSource.getById(it.stateId) } + return taskDataSource + .getById(taskId) + ?.let { stateDataSource.getById(it.stateId) } } - override fun getStateById(stateId: String): State? { - return stateDataSource.getById(stateId) + override fun getStateById(stateId: String): Result = + stateDataSource.getById(stateId) + ?.let { success(it) } + ?: failure(StateNotFoundException(stateId)) + + override fun getAllStates(): List { + return stateDataSource.getAll() } } \ No newline at end of file diff --git a/src/main/kotlin/di/uiModule.kt b/src/main/kotlin/di/uiModule.kt index 0d14083..9a5ce53 100644 --- a/src/main/kotlin/di/uiModule.kt +++ b/src/main/kotlin/di/uiModule.kt @@ -5,6 +5,7 @@ import com.berlin.domain.model.User import com.berlin.domain.usecase.authService.GetUserByIDUseCase import com.berlin.presentation.MainMenuUI import com.berlin.presentation.authService.* +import com.berlin.presentation.state.* import com.berlin.presentation.project.* import com.berlin.presentation.task.* import org.koin.core.qualifier.named @@ -16,13 +17,14 @@ val uiModule = module { single { CreateTaskUI(get(), get(named("currentUser")), get(), get()) } single { AssignTaskUI(get(), get(), get(), get()) } - single { DeleteTaskUI(get(), get(), get(),get()) } + single { DeleteTaskUI(get(), get(), get(), get()) } single { GetTasksByProjectIdUI(get(), get(), get()) } single { UpdateTaskUI(get(), get(), get(), get()) } single { ChangeTaskStateUI(get(), get(), get(), get()) } single { GetTaskByIdUI(get(), get(), get()) } single { GetUserByIDUseCase(get()) } single { GettingUsersLoggedInUI(get(), get()) } + single { CreationOfMateUi(get(),get(),get()) } single { AuthenticateUserUi(get(),get(),get()) } single { FetchAllUsersUI(get(),get()) } @@ -33,7 +35,11 @@ val uiModule = module { single { GetProjectByIdUi(get(),get(),get()) } single { UpdateProjectUi(get(),get(),get(),get(),get()) } - + single { CreateStateUi(get(), get(), get()) } + single { DeleteStateUi(get(), get(), get(), get()) } + single { GetStateByIdUi(get(), get(), get()) } + single { UpdateStateUi(get(), get(), get(), get()) } + single { GetAllStatesByProjectIdUi(get(),get(),get()) } /* aggregated main menu */ single { MainMenuUI( @@ -52,6 +58,12 @@ val uiModule = module { get(), get(), + get(), + get(), + get(), + get(), + get(), + get(), get(), get(), @@ -62,4 +74,4 @@ val uiModule = module { reader = get() ) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/di/useCaseModule.kt b/src/main/kotlin/di/useCaseModule.kt index 97b9c26..819191e 100644 --- a/src/main/kotlin/di/useCaseModule.kt +++ b/src/main/kotlin/di/useCaseModule.kt @@ -9,6 +9,14 @@ import com.berlin.domain.usecase.authService.FetchAllUsersUseCase import com.berlin.domain.usecase.authService.GetUserByIDUseCase import com.berlin.domain.usecase.authService.GettingUsersLoggedInUseCase import com.berlin.domain.usecase.project.* +import com.berlin.domain.usecase.state.CreateStateUseCase +import com.berlin.domain.usecase.state.DeleteStateUseCase +import com.berlin.domain.usecase.state.GetAllStatesByProjectIdUseCase +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.domain.usecase.state.GetStateByIdUseCase +import com.berlin.domain.usecase.state.GetStateByTaskIdUseCase +import com.berlin.domain.usecase.state.GetTasksByStateIdUseCase +import com.berlin.domain.usecase.state.UpdateStateUseCase import com.berlin.domain.usecase.task.* import domain.usecase.authService.AuthenticateUserUseCase import org.koin.dsl.module @@ -39,4 +47,13 @@ val useCaseModule = module { single { FetchAllUsersUseCase(get()) } single { AuthenticateUserUseCase(get(), get()) } single { CreationOfMateUseCase(get(), get(), get()) } -} + + single { CreateStateUseCase(get(),get())} + single { DeleteStateUseCase(get()) } + single { GetAllStatesByProjectIdUseCase(get(),get()) } + single { GetStateByIdUseCase(get()) } + single { GetStateByTaskIdUseCase(get(),get()) } + single { GetTasksByStateIdUseCase(get()) } + single { UpdateStateUseCase(get()) } + single { GetAllStatesUseCase(get()) } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/exception/Exceptions.kt b/src/main/kotlin/domain/exception/Exceptions.kt index 25560be..ab78c84 100644 --- a/src/main/kotlin/domain/exception/Exceptions.kt +++ b/src/main/kotlin/domain/exception/Exceptions.kt @@ -20,4 +20,4 @@ class InvalidProjectException(message: String): Exception(message) class InvalidStateException(message: String): Exception(message) class InvalidAuditLogException(message: String): Exception(message) class InvalidStateIdException(message: String): Exception(message) -class InvalidStateNameException(message: String): Exception(message) \ No newline at end of file +class InvalidStateNameException(message: String): Exception(message) diff --git a/src/main/kotlin/domain/repository/StateRepository.kt b/src/main/kotlin/domain/repository/StateRepository.kt index 85968f8..5c82938 100644 --- a/src/main/kotlin/domain/repository/StateRepository.kt +++ b/src/main/kotlin/domain/repository/StateRepository.kt @@ -5,10 +5,11 @@ import com.berlin.domain.model.Task interface StateRepository { fun addState(state: State): Result - fun getStatesByProjectId(projectId: String): List? + fun getStatesByProjectId(projectId: String): Result> fun getTasksByStateId(stateId: String):List? fun deleteState(stateId: String): Result fun updateState(state: State): Result fun getStateByTaskId(taskId: String): State? - fun getStateById(stateId: String): State? + fun getStateById(stateId: String): Result + fun getAllStates(): List } \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/state/DeleteStateUseCase.kt b/src/main/kotlin/domain/usecase/state/DeleteStateUseCase.kt index 378aeb2..4bba9c3 100644 --- a/src/main/kotlin/domain/usecase/state/DeleteStateUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/DeleteStateUseCase.kt @@ -26,6 +26,6 @@ class DeleteStateUseCase( private fun validateStateId(stateId: String): Boolean = stateId.isNotBlank() || !(stateId.all { it.isDigit() }) - fun checkStateExists(stateId: String): Boolean = - stateRepository.getStateById(stateId) != null + private fun checkStateExists(stateId: String): Boolean = + stateRepository.getStateById(stateId).isSuccess } diff --git a/src/main/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCase.kt b/src/main/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCase.kt index a084bb9..4acc5ce 100644 --- a/src/main/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCase.kt @@ -2,8 +2,6 @@ package com.berlin.domain.usecase.state import com.berlin.domain.exception.InvalidProjectIdException -import com.berlin.domain.exception.ProjectNotFoundException -import com.berlin.domain.exception.StateNotFoundException import com.berlin.domain.model.State import com.berlin.domain.repository.ProjectRepository import com.berlin.domain.repository.StateRepository @@ -13,16 +11,15 @@ class GetAllStatesByProjectIdUseCase( private val projectRepository: ProjectRepository ) { - fun getAllStatesByProjectId(projectId: String): List { + fun getAllStatesByProjectId(projectId: String): Result> { if (!validateProjectId(projectId)) throw InvalidProjectIdException("Project ID must not be empty or blank") if (checkProjectExists(projectId)) { return stateRepository.getStatesByProjectId(projectId) - ?: throw StateNotFoundException("No states found for project ID $projectId") } else { - throw ProjectNotFoundException("Project with ID $projectId does not exist") + return Result.failure(Exception("Project with ID $projectId does not exist")) } } diff --git a/src/main/kotlin/domain/usecase/state/GetAllStatesUseCase.kt b/src/main/kotlin/domain/usecase/state/GetAllStatesUseCase.kt new file mode 100644 index 0000000..ef43820 --- /dev/null +++ b/src/main/kotlin/domain/usecase/state/GetAllStatesUseCase.kt @@ -0,0 +1,12 @@ +package com.berlin.domain.usecase.state + +import com.berlin.domain.model.State +import com.berlin.domain.repository.StateRepository + +class GetAllStatesUseCase( + private val stateRepository: StateRepository, +) { + operator fun invoke(): List { + return stateRepository.getAllStates() + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/state/GetStateByIdUseCase.kt b/src/main/kotlin/domain/usecase/state/GetStateByIdUseCase.kt index 56aa29b..0416526 100644 --- a/src/main/kotlin/domain/usecase/state/GetStateByIdUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/GetStateByIdUseCase.kt @@ -8,12 +8,11 @@ class GetStateByIdUseCase( private val stateRepository: StateRepository ) { - fun getStateById(stateId: String): State { + fun getStateById(stateId: String): Result { if(!validateStateId(stateId)) - throw InvalidStateIdException("State ID must not be empty or blank") + throw InvalidStateIdException("State id must not be empty, blank, or purely numeric") return stateRepository.getStateById(stateId) - ?: throw InvalidStateIdException("State with ID $stateId does not exist") } private fun validateStateId(stateId: String): Boolean = diff --git a/src/main/kotlin/domain/usecase/state/GetStateByTaskIdUseCase.kt b/src/main/kotlin/domain/usecase/state/GetStateByTaskIdUseCase.kt index 25300d9..99dedd3 100644 --- a/src/main/kotlin/domain/usecase/state/GetStateByTaskIdUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/GetStateByTaskIdUseCase.kt @@ -22,7 +22,7 @@ class GetStateByTaskIdUseCase( } - private fun checkTaskExists(taskId: String): Boolean = taskRepository.findById(taskId) != null + private fun checkTaskExists(taskId: String): Boolean = taskRepository.findById(taskId).isSuccess private fun validateTaskId(taskId: String): Boolean = taskId.isNotBlank() && !(taskId.all { it.isDigit() }) diff --git a/src/main/kotlin/domain/usecase/state/GetTasksByStateIdUseCase.kt b/src/main/kotlin/domain/usecase/state/GetTasksByStateIdUseCase.kt index bff0fd0..3460488 100644 --- a/src/main/kotlin/domain/usecase/state/GetTasksByStateIdUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/GetTasksByStateIdUseCase.kt @@ -1,7 +1,6 @@ package com.berlin.domain.usecase.state import com.berlin.domain.exception.InvalidStateIdException -import com.berlin.domain.exception.InvalidTaskStateException import com.berlin.domain.exception.TaskNotFoundException import com.berlin.domain.model.Task import com.berlin.domain.repository.StateRepository @@ -10,7 +9,7 @@ class GetTasksByStateIdUseCase ( private val stateRepository: StateRepository ) { - fun getAllTasksByStateId(stateId: String): List? { + fun getAllTasksByStateId(stateId: String): List { if (!validateStateId(stateId)) throw InvalidStateIdException("State ID must not be empty or blank") if (checkStateExists(stateId)) { @@ -22,7 +21,7 @@ class GetTasksByStateIdUseCase ( } - private fun checkStateExists(stateId: String): Boolean = stateRepository.getStateById(stateId) != null + private fun checkStateExists(stateId: String): Boolean = stateRepository.getStateById(stateId).isSuccess private fun validateStateId(stateId: String): Boolean = stateId.isNotBlank() && !(stateId.all { it.isDigit() }) } \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/state/UpdateStateUseCase.kt b/src/main/kotlin/domain/usecase/state/UpdateStateUseCase.kt index e4c8936..66197f2 100644 --- a/src/main/kotlin/domain/usecase/state/UpdateStateUseCase.kt +++ b/src/main/kotlin/domain/usecase/state/UpdateStateUseCase.kt @@ -5,13 +5,17 @@ import com.berlin.domain.model.State import com.berlin.domain.repository.StateRepository class UpdateStateUseCase( - private val stateRepository: StateRepository + private val stateRepository: StateRepository, ) { - fun updateState(state: State): Result { - if(!validateStateName(state.name)) + fun updateState(stateId: String, newStateName: String, projectId: String): Result { + if (!validateStateName(newStateName)) throw InvalidStateNameException("State Name must not be empty or blank") - - return stateRepository.updateState(state) + val updatedState = State( + id = stateId, + name = newStateName, + projectId = projectId + ) + return stateRepository.updateState(updatedState) .map { "Updated Successfully" } .recover { "Update Failed" } } diff --git a/src/main/kotlin/presentation/state/CreatestateUi.kt b/src/main/kotlin/presentation/state/CreatestateUi.kt new file mode 100644 index 0000000..136cecc --- /dev/null +++ b/src/main/kotlin/presentation/state/CreatestateUi.kt @@ -0,0 +1,65 @@ +package com.berlin.presentation.state + +import com.berlin.data.DummyData +import com.berlin.domain.exception.InputCancelledException +import com.berlin.domain.model.Project +import com.berlin.domain.usecase.state.CreateStateUseCase +import com.berlin.presentation.UiRunner +import com.berlin.presentation.helper.choose +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer + +class CreateStateUi( + private val createStateUseCase: CreateStateUseCase, + private val viewer: Viewer, + private val reader: Reader +) : UiRunner { + + override val id: Int = 1000 + override val label: String = "Create New State" + + override fun run() { + + try { + val project = selectProject() + viewer.show("-- Enter a state in project ${project.name} --") + addStateName(project) + } catch (_: InputCancelledException) { + viewer.show("Cancelled!") + } + + + } + + private fun addStateName(project: Project) { + + viewer.show("Enter a state name (or type 'exit' to finish):") + viewer.show("State Name: ") + val stateName: String? = reader.read()?.trim() + when { + + (stateName?.lowercase().equals("exit")) -> return + + (stateName.isNullOrEmpty()) -> { + viewer.show("State Name can not be empty") + } + + else -> { + try { + createStateUseCase.createNewState(stateName, project.id) + .onSuccess { viewer.show(it) } + .onFailure { viewer.show(it.message ?: "Creation failed") } + + } catch (_: Exception) { + viewer.show("Invalid State Name, Try Again") + } + } + } + addStateName(project) + } + + private fun selectProject() = choose( + title = "Projects", elements = DummyData.projects, labelOf = { it.name }, viewer = viewer, reader = reader + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/state/DeleteStateUi.kt b/src/main/kotlin/presentation/state/DeleteStateUi.kt new file mode 100644 index 0000000..aaa0888 --- /dev/null +++ b/src/main/kotlin/presentation/state/DeleteStateUi.kt @@ -0,0 +1,53 @@ +package com.berlin.presentation.state + +import com.berlin.data.DummyData +import com.berlin.domain.exception.InputCancelledException +import com.berlin.domain.exception.InvalidSelectionException +import com.berlin.domain.exception.InvalidStateIdException +import com.berlin.domain.usecase.state.DeleteStateUseCase +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.presentation.UiRunner +import com.berlin.presentation.helper.choose +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer + +class DeleteStateUi( + private val deleteStateUseCase: DeleteStateUseCase, + private val getAllStates: GetAllStatesUseCase, + private val viewer: Viewer, + private val reader: Reader +) : UiRunner { + override val id: Int = 2000 + override val label: String = "Delete State" + + override fun run() { + + try { + + val state = choose( + title = "States", + elements = getAllStates(), + labelOf = { "${it.id} – ${it.name}" }, + viewer = viewer, + reader = reader + ) + viewer.show("Type Y to confirm deletion:") + if (!reader.read().equals("y", true)) throw InputCancelledException("Cancelled.") + + deleteStateUseCase.deleteState(state.id) + .onSuccess { + DummyData.states.remove(state) + viewer.show("Deleted.") + }.onFailure { + viewer.show(it.message ?: "Deletion failed") + } + } catch (ex: InputCancelledException) { + viewer.show("Cancelled.") + } catch (ex: InvalidSelectionException) { + viewer.show("Invalid selection") + } catch (ex: InvalidStateIdException) { + viewer.show("invalid state id") + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/state/GetAllstatesByProjectIdUi.kt b/src/main/kotlin/presentation/state/GetAllstatesByProjectIdUi.kt new file mode 100644 index 0000000..952ea86 --- /dev/null +++ b/src/main/kotlin/presentation/state/GetAllstatesByProjectIdUi.kt @@ -0,0 +1,69 @@ +package com.berlin.presentation.state + +import com.berlin.data.DummyData +import com.berlin.domain.exception.InputCancelledException +import com.berlin.domain.exception.InvalidProjectIdException +import com.berlin.domain.exception.InvalidSelectionException +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.GetAllStatesByProjectIdUseCase +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.presentation.UiRunner +import com.berlin.presentation.helper.choose +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer + +class GetAllStatesByProjectIdUi( + private val getAllStatesByProjectIdUseCase: GetAllStatesByProjectIdUseCase, + // private val getAllStates: GetAllStatesUseCase, + private val viewer: Viewer, + private val reader: Reader, +) : UiRunner { + + override val id: Int = 3000 + override val label: String = "Get current states for a specific project" + + override fun run() { + try { + val project = choose( + title = "Projects", + elements = DummyData.projects, + labelOf = { it.name }, + viewer = viewer, + reader = reader + ) + + getAllStatesByProjectIdUseCase.getAllStatesByProjectId(project.id) + .onSuccess { state -> showSwimLaneFor(project.id, state) } + .onFailure { viewer.show(it.message ?: "Failed to load states") } + + + } catch (ex: InputCancelledException) { + viewer.show("Cancelled.") + } catch (ex: InvalidSelectionException) { + viewer.show("Invalid selection") + } catch (ex: InvalidProjectIdException) { + viewer.show("invalid project id") + } + } + + private fun showSwimLaneFor(projectId: String, states: List) { + val projects = DummyData.projects.filter { it.id == projectId } + if (projects.isEmpty()) { + viewer.show("No projects found") + return + } + viewer.show("\n=== States for project $projectId ===") + projects.forEach { project -> + viewer.show("\n[${project.name}]") + stateInProject(states, project.id).forEach { line -> + viewer.show(line) + } + } + } + + private fun stateInProject(all: List, projectId: String): List { + val here = all.filter { it.projectId == projectId } + if (here.isEmpty()) return listOf(" (no states)") + return here.map { "- ${it.id}: ${it.name}" } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/state/GetStateByIdUi.kt b/src/main/kotlin/presentation/state/GetStateByIdUi.kt new file mode 100644 index 0000000..d5cdbce --- /dev/null +++ b/src/main/kotlin/presentation/state/GetStateByIdUi.kt @@ -0,0 +1,39 @@ +package com.berlin.presentation.state + +import com.berlin.domain.exception.StateNotFoundException +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.GetStateByIdUseCase +import com.berlin.presentation.UiRunner +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer + +class GetStateByIdUi( + private val getStateById: GetStateByIdUseCase, + private val viewer: Viewer, + private val reader: Reader, +) : UiRunner { + override val id: Int = 200000 + override val label: String = "Get state by ID" + override fun run() { + viewer.show("Enter state ID: ") + val stateId = reader.read()?.trim().orEmpty() + getStateById.getStateById(stateId) + .onSuccess { showState(it) } + .onFailure { ex -> + when (ex) { + is StateNotFoundException -> + viewer.show("No state found with ID “$stateId”") + + else -> + viewer.show(ex.message ?: "Lookup failed") + } + } + + } + + private fun showState(state: State) { + viewer.show("ID: ${state.id}") + viewer.show("Title: ${state.name}") + viewer.show("Project ID: ${state.projectId}") + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/state/updateStateUi.kt b/src/main/kotlin/presentation/state/updateStateUi.kt new file mode 100644 index 0000000..8be0482 --- /dev/null +++ b/src/main/kotlin/presentation/state/updateStateUi.kt @@ -0,0 +1,48 @@ +package com.berlin.presentation.state + +import com.berlin.domain.exception.InputCancelledException +import com.berlin.domain.exception.InvalidStateNameException +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.domain.usecase.state.UpdateStateUseCase +import com.berlin.presentation.UiRunner +import com.berlin.presentation.helper.choose +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer + +class UpdateStateUi( + private val updateState: UpdateStateUseCase, + private val getAllStates: GetAllStatesUseCase, + private val viewer: Viewer, + private val reader: Reader, +) : UiRunner { + override val id: Int = 50 + override val label: String = "Update State" + override fun run() { + try { + + val state = choose( + title = "States to update", + elements = getAllStates(), + labelOf = { "${it.id} – ${it.name}" }, + viewer = viewer, + reader = reader + ) + + viewer.show("Enter new state name ((or X to keep ${state.name}): ") + val newName = reader.read()?.trim().orEmpty() + updateState.updateState( + state.id, + newName, + state.projectId + ) + .onSuccess { viewer.show("Updated Successfully") } + .onFailure { viewer.show("Update Failed") } + + } catch (ex: InvalidStateNameException) { + viewer.show("State Name must not be empty or blank") + } catch (_: InputCancelledException) { + viewer.show("Cancelled!") + } + + } +} \ No newline at end of file diff --git a/src/test/kotlin/data/state/StateRepositoryImplTest.kt b/src/test/kotlin/data/state/StateRepositoryImplTest.kt index 2f57b80..8d0913d 100644 --- a/src/test/kotlin/data/state/StateRepositoryImplTest.kt +++ b/src/test/kotlin/data/state/StateRepositoryImplTest.kt @@ -1,7 +1,6 @@ package com.berlin.data.state import com.berlin.data.BaseDataSource -import com.berlin.data.csv_data_source.CsvDataSource import com.berlin.domain.exception.InvalidStateException import com.berlin.domain.model.State import com.berlin.domain.model.Task @@ -73,7 +72,7 @@ class StateRepositoryImplTest { // When val result = repository.getStateById(validState.id) // Then - assertThat(result).isEqualTo(validState) + assertThat(result.getOrNull()).isEqualTo(validState) } @Test @@ -83,7 +82,7 @@ class StateRepositoryImplTest { // When val result = repository.getStateById(validState.id) // Then - assertThat(result).isNull() + assertThat(result.getOrNull()).isNull() } // endregion @@ -95,7 +94,8 @@ class StateRepositoryImplTest { // When val result = repository.getStatesByProjectId(validState.projectId) // Then - assertThat(result).isEqualTo(states) + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrNull()).isEqualTo(states) } @Test @@ -105,8 +105,7 @@ class StateRepositoryImplTest { // When val result = repository.getStatesByProjectId(validState.projectId) // Then - assertThat(result).isNull() - } + assertThat(result.getOrNull()).isEmpty() } // endregion // region getTasksByStateId diff --git a/src/test/kotlin/logic/usecase/state/CreateStateUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/CreateStateUseCaseTest.kt similarity index 96% rename from src/test/kotlin/logic/usecase/state/CreateStateUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/CreateStateUseCaseTest.kt index f244117..5736f3b 100644 --- a/src/test/kotlin/logic/usecase/state/CreateStateUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/CreateStateUseCaseTest.kt @@ -1,7 +1,6 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state import com.berlin.domain.helper.IdGeneratorImplementation -import com.berlin.domain.usecase.state.CreateStateUseCase import com.berlin.domain.model.State import com.berlin.domain.repository.StateRepository import com.google.common.truth.Truth.assertThat diff --git a/src/test/kotlin/logic/usecase/state/DeleteStateUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/DeleteStateUseCaseTest.kt similarity index 86% rename from src/test/kotlin/logic/usecase/state/DeleteStateUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/DeleteStateUseCaseTest.kt index 17a248f..a67fade 100644 --- a/src/test/kotlin/logic/usecase/state/DeleteStateUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/DeleteStateUseCaseTest.kt @@ -1,7 +1,7 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state +import com.berlin.domain.exception.StateNotFoundException import com.berlin.domain.repository.StateRepository -import com.berlin.domain.usecase.state.DeleteStateUseCase import com.google.common.truth.Truth.assertThat import io.mockk.every import io.mockk.mockk @@ -51,14 +51,15 @@ class DeleteStateUseCaseTest { @Test fun `should throw exception when state does not exist`() { // Given - every { stateRepository.getStateById(any()) } returns null + val stateId="q2" + every { stateRepository.getStateById(any()) } returns Result.failure(StateNotFoundException(stateId)) // When - val result = deleteStateUseCase.deleteState("S2") + val result = deleteStateUseCase.deleteState("q2") // Then result.onFailure { exception -> - assertThat(exception.message).isEqualTo("State with ID S2 does not exist") + assertThat(exception.message).isEqualTo("State with ID q2 does not exist") } } diff --git a/src/test/kotlin/logic/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt similarity index 76% rename from src/test/kotlin/logic/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt index 012ea69..51049a2 100644 --- a/src/test/kotlin/logic/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/GetAllStatesByProjectIdUseCaseTest.kt @@ -1,6 +1,5 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state -import com.berlin.domain.usecase.state.GetAllStatesByProjectIdUseCase import com.berlin.domain.model.State import com.berlin.domain.repository.ProjectRepository import com.berlin.domain.repository.StateRepository @@ -35,24 +34,22 @@ class GetAllStatesByProjectIdUseCaseTest { State(id = "S2", name = "Inactive", projectId = "P1") ) every { projectRepository.getProjectById("P1") } returns mockk() - every { stateRepository.getStatesByProjectId("P1") } returns expectedStates + every { stateRepository.getStatesByProjectId("P1") } returns Result.success(expectedStates) // When val result = getAllStatesByProjectIdUseCase.getAllStatesByProjectId("P1") // Then - assertThat(result).isEqualTo(expectedStates) + assertThat(result).isEqualTo(Result.success(expectedStates)) } @Test fun `should throw exception when no states are found for the project`() { // Given - every { projectRepository.getProjectById("P1") } returns mockk() - every { stateRepository.getStatesByProjectId("P3") } returns null + every { stateRepository.getStatesByProjectId("P3") } returns Result.failure(Exception("Project with ID P3 does not exist")) // When & Then - val exception = assertThrows { getAllStatesByProjectIdUseCase.getAllStatesByProjectId("P3") } - assertThat(exception.message).isEqualTo("No states found for project ID P3") + assertThat(getAllStatesByProjectIdUseCase.getAllStatesByProjectId("P3").isFailure).isTrue() } @Test @@ -61,8 +58,7 @@ class GetAllStatesByProjectIdUseCaseTest { every { projectRepository.getProjectById("P2") } returns null // When & Then - val exception = assertThrows { getAllStatesByProjectIdUseCase.getAllStatesByProjectId("P2") } - assertThat(exception.message).isEqualTo("Project with ID P2 does not exist") + assertThat(getAllStatesByProjectIdUseCase.getAllStatesByProjectId("P2").isFailure).isTrue() } @ParameterizedTest diff --git a/src/test/kotlin/logic/usecase/state/GetStateByIdUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/GetStateByIdUseCaseTest.kt similarity index 69% rename from src/test/kotlin/logic/usecase/state/GetStateByIdUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/GetStateByIdUseCaseTest.kt index 522f573..9f5d3ab 100644 --- a/src/test/kotlin/logic/usecase/state/GetStateByIdUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/GetStateByIdUseCaseTest.kt @@ -1,6 +1,6 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state -import com.berlin.domain.usecase.state.GetStateByIdUseCase +import com.berlin.domain.exception.StateNotFoundException import com.berlin.domain.model.State import com.berlin.domain.repository.StateRepository import com.google.common.truth.Truth.assertThat @@ -15,10 +15,11 @@ import kotlin.test.Test class GetStateByIdUseCaseTest { private lateinit var getStateByIdUseCase: GetStateByIdUseCase - private val stateRepository: StateRepository = mockk(relaxed = true) + private lateinit var stateRepository: StateRepository @BeforeEach fun setup() { + stateRepository= mockk() getStateByIdUseCase = GetStateByIdUseCase(stateRepository) } @@ -26,26 +27,26 @@ class GetStateByIdUseCaseTest { fun `should return state when valid state id exists`() { // Given val expectedState = State(id = "S1", name = "Active", projectId = "P1") - every { stateRepository.getStateById("S1") } returns expectedState + every { stateRepository.getStateById("S1") } returns Result.success(expectedState) // When val result = getStateByIdUseCase.getStateById("S1") // Then - assertThat(result).isEqualTo(expectedState) + assertThat(result).isEqualTo(Result.success(expectedState)) } @Test fun `should throw exception when state id does not exist`() { // Given val input = "S2" - every { stateRepository.getStateById(any()) } returns null + every { stateRepository.getStateById(any()) } returns Result.failure(StateNotFoundException(input)) // When - val exception = assertThrows { getStateByIdUseCase.getStateById("S2") } + val result = getStateByIdUseCase.getStateById("S2") // Then - assertThat(exception.message).isEqualTo("State with ID $input does not exist") + assertThat(result.isFailure).isTrue() } @ParameterizedTest diff --git a/src/test/kotlin/logic/usecase/state/GetStateByTaskIdUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/GetStateByTaskIdUseCaseTest.kt similarity index 93% rename from src/test/kotlin/logic/usecase/state/GetStateByTaskIdUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/GetStateByTaskIdUseCaseTest.kt index 111ff85..b619bf7 100644 --- a/src/test/kotlin/logic/usecase/state/GetStateByTaskIdUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/GetStateByTaskIdUseCaseTest.kt @@ -1,6 +1,5 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state -import com.berlin.domain.usecase.state.GetStateByTaskIdUseCase import com.berlin.domain.model.State import com.berlin.domain.repository.StateRepository import com.berlin.domain.repository.TaskRepository diff --git a/src/test/kotlin/logic/usecase/state/GetTasksByStateIdUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/GetTasksByStateIdUseCaseTest.kt similarity index 85% rename from src/test/kotlin/logic/usecase/state/GetTasksByStateIdUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/GetTasksByStateIdUseCaseTest.kt index 5a1acc6..c5ca912 100644 --- a/src/test/kotlin/logic/usecase/state/GetTasksByStateIdUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/GetTasksByStateIdUseCaseTest.kt @@ -1,6 +1,7 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state -import com.berlin.domain.usecase.state.GetTasksByStateIdUseCase +import com.berlin.domain.exception.InvalidStateIdException +import com.berlin.domain.exception.StateNotFoundException import com.berlin.domain.model.Task import com.berlin.domain.repository.StateRepository import com.google.common.truth.Truth.assertThat @@ -60,10 +61,12 @@ class GetTasksByStateIdUseCaseTest { @Test fun `should throw exception when state id does not exist`() { // Given - every { stateRepository.getStateById("S2") } returns null + every { stateRepository.getStateById("S2") } returns Result.failure(StateNotFoundException("S2")) // When & Then - val exception = assertThrows { getTasksByStateIdUseCase.getAllTasksByStateId("S2") } + val exception = assertThrows { + getTasksByStateIdUseCase.getAllTasksByStateId("S2") + } assertThat(exception.message).isEqualTo("State with ID S2 does not exist") } diff --git a/src/test/kotlin/logic/usecase/state/UpdateStateUseCaseTest.kt b/src/test/kotlin/domain/usecase/state/UpdateStateUseCaseTest.kt similarity index 85% rename from src/test/kotlin/logic/usecase/state/UpdateStateUseCaseTest.kt rename to src/test/kotlin/domain/usecase/state/UpdateStateUseCaseTest.kt index 3a1d9a6..e27c0d6 100644 --- a/src/test/kotlin/logic/usecase/state/UpdateStateUseCaseTest.kt +++ b/src/test/kotlin/domain/usecase/state/UpdateStateUseCaseTest.kt @@ -1,6 +1,5 @@ -package com.berlin.logic.usecase.state +package com.berlin.domain.usecase.state -import com.berlin.domain.usecase.state.UpdateStateUseCase import com.berlin.domain.model.State import com.berlin.domain.repository.StateRepository import com.google.common.truth.Truth.assertThat @@ -29,7 +28,7 @@ class UpdateStateUseCaseTest { every { stateRepository.updateState(state) } returns Result.success("Updated Successfully") // When - val result = updateStateUseCase.updateState(state) + val result = updateStateUseCase.updateState(state.id,state.name,state.projectId) // Then assertThat(result).isEqualTo(Result.success("Updated Successfully")) @@ -42,7 +41,7 @@ class UpdateStateUseCaseTest { every { stateRepository.updateState(state) } returns Result.failure(Exception()) // When - val result = updateStateUseCase.updateState(state) + val result = updateStateUseCase.updateState(state.id,state.name,state.projectId) // Then result.onFailure { exception -> @@ -62,7 +61,7 @@ class UpdateStateUseCaseTest { // When && Then assertThrows { - updateStateUseCase.updateState(input) + updateStateUseCase.updateState(input.id,input.name,input.projectId) } } diff --git a/src/test/kotlin/presentation/state/CreateStateUiTest.kt b/src/test/kotlin/presentation/state/CreateStateUiTest.kt new file mode 100644 index 0000000..5d0de79 --- /dev/null +++ b/src/test/kotlin/presentation/state/CreateStateUiTest.kt @@ -0,0 +1,117 @@ +package presentation.state + +import com.berlin.domain.model.Project +import com.berlin.domain.usecase.state.CreateStateUseCase +import com.berlin.presentation.helper.choose +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer +import com.berlin.presentation.state.CreateStateUi +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class CreateStateUiTest { + private lateinit var createStateUseCase: CreateStateUseCase + private lateinit var createStateUi: CreateStateUi + private val viewer: Viewer = mockk(relaxed = true) + private val reader: Reader = mockk(relaxed = true) + + + private val testProject = Project( + id = "P1", + name = "Berlin Core", + description = "The back-end", + statesId = listOf("S1", "S2", "S3", "S4"), + tasksId = mutableListOf() + ) + + @BeforeEach + fun setup() { + createStateUseCase = mockk(relaxed = true) + createStateUi = CreateStateUi(createStateUseCase, viewer, reader) + + // Mock the choose function to return our test project + mockkStatic("com.berlin.presentation.helper.ChooserKt") + every { + choose( + any(), any(), any(), viewer, reader + ) + } returns testProject + } + + @Test + fun `should display error message when state name is null or empty`() { + // Given + every { reader.read() } returnsMany listOf("", null, "exit") + + // When + createStateUi.run() + + // Then + verify { viewer.show("State Name can not be empty") } + verify(exactly = 3) { reader.read() } + } + + @Test + fun `should exit state creation when user enters exit command`() { + // Given + every { reader.read() } returns "exit" + + // When + createStateUi.run() + + // Then + verify(exactly = 1) { reader.read() } + } + + @Test + fun `should create state successfully when valid state name is provided`() { + // Given + val stateName = "NewState" + + every { reader.read() } returnsMany listOf(stateName, "exit") + every { + createStateUseCase.createNewState(stateName, testProject.id) + } returns Result.success("State created successfully") + + // When + createStateUi.run() + + // Then + verify { createStateUseCase.createNewState(stateName, testProject.id) } + verify { viewer.show("State created successfully") } + } + + @Test + fun `should display error message when state creation fails`() { + // Given + val stateName = "InvalidState" + + every { reader.read() } returnsMany listOf(stateName, "exit") + every { + createStateUseCase.createNewState(stateName, testProject.id) + } returns Result.failure(Exception("Creation Failed")) + + // When + createStateUi.run() + + // Then + verify { createStateUseCase.createNewState(stateName, testProject.id) } + verify { viewer.show("Creation Failed") } + } + + @Test + fun `should handle case insensitive exit command`() { + // Given + every { reader.read() } returns "ExIt" + + // When + createStateUi.run() + + // Then + verify(exactly = 1) { reader.read() } + } +} diff --git a/src/test/kotlin/presentation/state/DeleteStateUiTest.kt b/src/test/kotlin/presentation/state/DeleteStateUiTest.kt new file mode 100644 index 0000000..6aca342 --- /dev/null +++ b/src/test/kotlin/presentation/state/DeleteStateUiTest.kt @@ -0,0 +1,130 @@ +package com.berlin.presentation.state + +import com.berlin.data.DummyData +import com.berlin.domain.exception.InvalidStateIdException +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.DeleteStateUseCase +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer +import com.google.common.truth.Truth.assertThat +import io.mockk.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + + + +class DeleteStateUiTest { + + private val printed = mutableListOf() + private val viewer: Viewer = mockk(relaxed = true) { + every { show(capture(printed)) } just Runs + } + private val reader: Reader = mockk() + private val deleteState: DeleteStateUseCase = mockk() + private val getAllStates: GetAllStatesUseCase = mockk() + + private lateinit var state: State + private lateinit var ui: DeleteStateUi + + @BeforeEach + fun setUp() { + DummyData.states.clear() + printed.clear() + + state = State( "S1", "To Do","L3") + DummyData.states += state + + ui = DeleteStateUi(deleteState, getAllStates, viewer, reader) + } + + @Test + fun `deleteState should deletes state and prints confirmation when confirm`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returnsMany listOf("1", "y") + every { deleteState.deleteState(state.id) } returns Result.success("Deleted Successfully") + + //when + ui.run() + + //Then + verify(exactly = 1) { deleteState.deleteState(state.id) } + assertThat(DummyData.states).doesNotContain(state) + assertThat(printed.last()).contains("Deleted.") + } + + @Test + fun `run should return cancelled when user aborts deletion at confirmation`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returnsMany listOf("1", "n") + + //when + ui.run() + + //Then + verify(exactly = 0) { deleteState.deleteState(any()) } + assertThat(DummyData.states).contains(state) + assertThat(printed.last()).contains("Cancelled.") + } + + @Test + fun `run should show Cancelled when user cancels in chooser`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returns "X" + + //when + ui.run() + + //Then + verify(exactly = 0) { deleteState.deleteState(any()) } + assertThat(printed.last()).contains("Cancelled.") + } + + @Test + fun `delete state should return Deletion Failed when failed`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returnsMany listOf("1", "y") + every { deleteState.deleteState(state.id) } returns Result.failure(IllegalStateException("Deletion Failed")) + + //when + ui.run() + + //Then + verify(exactly = 1) { deleteState.deleteState(state.id) } + assertThat(DummyData.states).contains(state) + assertThat(printed.last()).contains("Deletion Failed") + } + + @Test + fun `run should show error message Invalid selection when invalid index selected`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returns "99" + + //when + ui.run() + + //Then + verify(exactly = 0) { deleteState.deleteState(any()) } + assertThat(printed.last()).contains("Invalid selection") + } + + @Test + fun `deleteState should throw InvalidStateIdExceptionwhen id is not valid`() { + //Given + every { getAllStates() } returns listOf(state) + every { reader.read() } returnsMany listOf("1", "y") + every { deleteState.deleteState(state.id) } throws InvalidStateIdException("State ID must not be empty or blank") + + //when + ui.run() + + //Then + assertThat(printed.last()).contains("invalid state id") + verify(exactly = 1) { deleteState.deleteState(state.id) } + } +} diff --git a/src/test/kotlin/presentation/state/GetAllStatesByProjectIdUiTest.kt b/src/test/kotlin/presentation/state/GetAllStatesByProjectIdUiTest.kt new file mode 100644 index 0000000..60ba5dc --- /dev/null +++ b/src/test/kotlin/presentation/state/GetAllStatesByProjectIdUiTest.kt @@ -0,0 +1,133 @@ +package com.berlin.presentation.state + +import com.berlin.data.DummyData +import com.berlin.domain.exception.InvalidProjectIdException +import com.berlin.domain.model.Project +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.GetAllStatesByProjectIdUseCase +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer +import com.google.common.truth.Truth.assertThat +import io.mockk.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GetAllStatesByProjectIdUiTest { + + private val printed = mutableListOf() + private val viewer: Viewer = mockk(relaxed = true) { + every { show(capture(printed)) } just Runs + } + private val reader: Reader = mockk() + private lateinit var useCase: GetAllStatesByProjectIdUseCase + private lateinit var ui: GetAllStatesByProjectIdUi + + + @BeforeEach + fun setUp() { + DummyData.projects.clear() + DummyData.states.clear() + printed.clear() + + DummyData.projects += projectP1 + DummyData.states += listOf(stateTodo, stateDone) + + useCase = mockk() + ui = GetAllStatesByProjectIdUi(useCase, viewer, reader) + } + + @Test + fun `shows swimlane with states`() { + //Given + every { reader.read() } returns "1" + every { useCase.getAllStatesByProjectId(projectIdWithNoStates) } returns Result.success( + listOf( + stateTodo, + stateDone + ) + ) + + //Given + ui.run() + + //Then + assertThat(printed).contains("\n=== States for project P1 ===") + assertThat(printed).contains("- S1: TODO") + assertThat(printed).contains("- S2: DONE") + } + + @Test + fun `shows (no states) when project has no states`() { + //Given + every { reader.read() } returns "1" + every { useCase.getAllStatesByProjectId(projectIdWithNoStates) } returns Result.success(emptyList()) + + //When + ui.run() + + //Then + assertThat(printed).contains(" (no states)") + } + + @Test + fun `cancelling input shows Cancelled`() { + //Given + every { reader.read() } returns "X" + + //When + ui.run() + + //Then + assertThat(printed.last()).contains("Cancelled.") + verify(exactly = 0) { useCase.getAllStatesByProjectId(any()) } + } + + @Test + fun `invalid selection shows error`() { + //Given + every { reader.read() } returns "99" + + //When + ui.run() + + //Then + assertThat(printed.last()).contains("Invalid selection") + verify(exactly = 0) { useCase.getAllStatesByProjectId(any()) } + } + + @Test + fun `getAllStatesByProjectId shows message Failed to load when failure`() { + //Given + every { reader.read() } returns "1" + every { useCase.getAllStatesByProjectId(projectIdWithNoStates) } returns Result.failure(RuntimeException("Failed to load")) + + //When + ui.run() + + //Then + assertThat(printed.last()).contains("Failed to load") + verify(exactly = 1) { useCase.getAllStatesByProjectId("P1") } + } + + //TODO review exception thrown + @Test + fun `throws InvalidProjectIdException and shows invalid project id`() { + //Given + every { reader.read() } returns "1" + every { useCase.getAllStatesByProjectId(projectIdWithNoStates) } throws InvalidProjectIdException("invalid project id") + + //When + ui.run() + + //Then + assertThat(printed.last()).contains("invalid project id") + verify(exactly = 1) { useCase.getAllStatesByProjectId(projectIdWithNoStates) } + } + + private companion object { + val projectP1 = Project("P1", "Core", null, emptyList(), emptyList()) + val stateTodo = State("S1", "TODO", "P1") + val stateDone = State("S2", "DONE", "P1") + const val projectIdWithNoStates = "P1" + } +} diff --git a/src/test/kotlin/presentation/state/GetStateByIdUiTest.kt b/src/test/kotlin/presentation/state/GetStateByIdUiTest.kt new file mode 100644 index 0000000..84d2586 --- /dev/null +++ b/src/test/kotlin/presentation/state/GetStateByIdUiTest.kt @@ -0,0 +1,79 @@ +package presentation.state + + +import com.berlin.domain.exception.StateNotFoundException +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.GetStateByIdUseCase +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer +import com.berlin.presentation.state.GetStateByIdUi +import com.google.common.truth.Truth.assertThat +import io.mockk.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GetStateByIdUiTest { + + private lateinit var viewer: Viewer + private lateinit var reader: Reader + private lateinit var getStateById: GetStateByIdUseCase + private lateinit var ui: GetStateByIdUi + private val printed = mutableListOf() + + @BeforeEach + fun setup() { + viewer = mockk(relaxed = true) { + every { show(capture(printed)) } just Runs + } + reader = mockk() + getStateById = mockk() + ui = GetStateByIdUi(getStateById, viewer, reader) + printed.clear() + } + + private companion object { + val stateExists = State("Q1", "Menna", "P5") + val stateIdNotExist = "Q999" + val idWhenExceptionNull = "T3" + } + + @Test + fun `getStateById should return state when its id exists`() { + //given + every { getStateById.getStateById("Q1") } returns Result.success(stateExists) + every { reader.read() } returns "Q1" + //when + ui.run() + //Then + verify { viewer.show("Enter state ID: ") } + verify { getStateById.getStateById("Q1") } + + } + + @Test + fun `getStateById should show message when id doesn't exist`() { + // Given + every { reader.read() } returns stateIdNotExist + every { getStateById.getStateById(stateIdNotExist) } returns Result.failure(StateNotFoundException("State with ID Q999 not found")) + + // When + ui.run() + + // Then + assertThat(printed.last()).isEqualTo("No state found with ID “Q999”") + } + + @Test + fun `lookup failed fallback when exception message null`() { + //given + every { reader.read() } returns idWhenExceptionNull + every { getStateById.getStateById(idWhenExceptionNull) } returns Result.failure(IllegalStateException("Lookup failed")) + + //When + ui.run() + + //Then + assertThat(printed.last()).contains("Lookup failed") + } + +} \ No newline at end of file diff --git a/src/test/kotlin/presentation/state/UpdateStateUiTest.kt b/src/test/kotlin/presentation/state/UpdateStateUiTest.kt new file mode 100644 index 0000000..de588fc --- /dev/null +++ b/src/test/kotlin/presentation/state/UpdateStateUiTest.kt @@ -0,0 +1,96 @@ +package presentation.state + +import com.berlin.domain.exception.InvalidStateException +import com.berlin.domain.exception.InvalidStateNameException +import com.berlin.domain.model.State +import com.berlin.domain.usecase.state.GetAllStatesUseCase +import com.berlin.domain.usecase.state.UpdateStateUseCase +import com.berlin.presentation.io.Reader +import com.berlin.presentation.io.Viewer +import com.berlin.presentation.state.UpdateStateUi +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import kotlin.test.Test + +class UpdateStateUiTest { + private lateinit var updateState: UpdateStateUseCase + private lateinit var getAllStates: GetAllStatesUseCase + private lateinit var updateStateUi: UpdateStateUi + private var viewer: Viewer = mockk(relaxed = true) + private var reader: Reader = mockk() + + @BeforeEach + fun setup() { + updateState = mockk() + getAllStates = mockk() + updateStateUi = UpdateStateUi(updateState, getAllStates, viewer, reader) + } + + + @Test + fun `run should return successfully updated when every thing is correct`() { + //Given + every { getAllStates() } returns states + every { reader.read() } returnsMany listOf("1", "done") + every { + updateState.updateState(any(), any(), any()) + } returns Result.success("Updated Successfully") + + //When + updateStateUi.run() + + //Then + verify { getAllStates() } + verify { viewer.show(any()) } + verify { + updateState.updateState( + state.id, + successfullyStateNewName, + state.projectId + ) + } + verify { viewer.show("Updated Successfully") } + } + + @Test + fun `run should return update failed when update fails`() { + //Given + every { getAllStates() } returns states + every { reader.read() } returnsMany listOf("1", "done") + every { + updateState.updateState(any(), any(), any()) + } returns Result.failure(InvalidStateException("can not update state")) + + //When + updateStateUi.run() + + //Then + verify { viewer.show("Update Failed") } + } + + @Test + fun `run should throw InvalidStateNameException when State Name is empty or blank`() { + //Given + every { getAllStates() } returns listOf(State("Q1", "Menna", "P5")) + every { reader.read() } returnsMany listOf("1", " ") + every { + updateState.updateState(any(), emptyStateName, any()) + } throws InvalidStateNameException("State Name must not be empty or blank") + + //When + updateStateUi.run() + + //Then + verify { viewer.show("State Name must not be empty or blank") } + } + + private companion object { + private val state = State("Q1", "Menna", "P5") + private val states = listOf(state) + private const val successfullyStateNewName = "done" + private const val emptyStateName = "" + } + +} \ No newline at end of file