diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index bb7a80e..e76ab84 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -17,6 +17,11 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + + - name: Restore keys.properties + run: echo "$KEYS_FILE" | base64 -d > keys.properties + env: + KEYS_FILE: ${{ secrets.KEYS_FILE }} - name: Make gradlew executable run: chmod +x ./gradlew diff --git a/.gitignore b/.gitignore index ce97d6a..09676dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle build/ .idea +.csv !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/bauild/ !**/src/test/**/build/ @@ -16,7 +17,8 @@ build/ out/ !**/src/main/**/out/ !**/src/test/**/out/ - +keys.properties +local.properties ### Kotlin ### .kotlin diff --git a/build.gradle.kts b/build.gradle.kts index 396a96e..88efe06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,113 @@ +import org.gradle.declarative.dsl.schema.FqName.Empty.packageName +import java.io.FileInputStream +import java.util.* + plugins { - kotlin("jvm") version "2.1.20" + kotlin("jvm") version "2.1.0" + id("com.github.gmazzo.buildconfig") version "4.1.2" + jacoco +} +val localProperties = Properties().apply { + val localFile = rootProject.file("Keys.properties") + if (localFile.exists()) { + FileInputStream(localFile).use { load(it) } + } +} +val mongoUri = localProperties.getProperty("MONGO.URI") +val databaseName = localProperties.getProperty("DATABASE.NAME") + +buildConfig { + packageName("org.example") + buildConfigField("String", "MONGO_URI", "\"${mongoUri}\"") + buildConfigField("String", "DATABASE_NAME", "\"${databaseName}\"") + buildConfigField("String", "APP_VERSION", "\"${project.version}\"") +} +jacoco { + toolVersion = "0.8.7" +} +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} +tasks.withType { + exclude("**/test/**") // Excludes all test packages + // or be more specific + // exclude("com/example/mypackage/test/**") +} +fun findTestedProductionClasses(): List { + val testFiles = fileTree("src/test/kotlin") { + include("**/*Test.kt") + }.files + return testFiles.map { file -> + val relativePath = file.relativeTo(file("src/test/kotlin")).path + .removeSuffix("Test.kt") + .replace("\\", "/") + "**/${relativePath}.class" + } +} +tasks.test { + finalizedBy(tasks.jacocoTestReport) +} +tasks.jacocoTestReport { + dependsOn(tasks.test) + val includedClasses = findTestedProductionClasses() + classDirectories.setFrom( + fileTree("$buildDir/classes/kotlin/main") { + include(includedClasses) + } + ) + sourceDirectories.setFrom(files("src/main/kotlin")) + doFirst { + println("=== INCLUDED PRODUCTION CLASSES ===") + includedClasses.forEach { + println(it) + } + } + reports { + html.required.set(true) + xml.required.set(true) + } +} +tasks.jacocoTestCoverageVerification { + dependsOn(tasks.jacocoTestReport) + + // Use the same includedClasses for verification as in the report + val includedClasses = findTestedProductionClasses() + + classDirectories.setFrom( + fileTree("$buildDir/classes/kotlin/main") { + include(includedClasses) + } + ) + violationRules { + rule { + // Generic instruction coverage (default counter) + limit { + minimum = "0.8".toBigDecimal() + } + + limit { + counter = "LINE" + value = "COVEREDRATIO" + minimum = "0.8".toBigDecimal() + } + + limit { + counter = "METHOD" + value = "COVEREDRATIO" + minimum = "0.8".toBigDecimal() + } + limit { + counter = "CLASS" + value = "COVEREDRATIO" + minimum = "0.8".toBigDecimal() + } + } + } +} +tasks.check { + dependsOn(tasks.jacocoTestCoverageVerification) } group = "org.example" @@ -11,11 +119,21 @@ repositories { dependencies { testImplementation(kotlin("test")) + implementation("io.insert-koin:koin-core:4.0.2") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") + testImplementation("io.mockk:mockk:1.13.10") + testImplementation("com.google.truth:truth:1.4.2") + // MongoDB driver + implementation("org.mongodb:mongodb-driver-sync:4.9.1") + + // Kotlin serialization for MongoDB (optional but helpful) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") } tasks.test { useJUnitPlatform() + testLogging { + events ("passed", "skipped", "failed") + showStandardStreams = true + } } -kotlin { - jvmToolchain(17) -} \ No newline at end of file diff --git a/local.properties b/local.properties new file mode 100644 index 0000000..9ea500e --- /dev/null +++ b/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Fri May 02 18:44:35 EEST 2025 +sdk.dir=C\:\\Users\\20180\\AppData\\Local\\Android\\Sdk diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index fd32761..c209545 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,5 +1,54 @@ package org.example +import com.mongodb.client.model.Filters +import di.appModule +import di.useCasesModule +import org.bson.Document +import org.example.di.dataModule +import org.example.di.repositoryModule +import data.datasource.mongo.MongoConfig +import org.example.common.Constants.MongoCollections.USERS_COLLECTION +import org.example.data.repository.UsersRepositoryImpl +import org.example.domain.entity.User +import org.example.presentation.AuthApp +import org.koin.core.context.GlobalContext.startKoin +import java.time.LocalDateTime +import java.util.UUID + fun main() { println("Hello, PlanMate!") -} \ No newline at end of file + startKoin { modules(appModule, useCasesModule, repositoryModule, dataModule) } + createAdminUser() + AuthApp().run() +} + +fun createAdminUser() { + println("Creating admin user...") + try { + val collection = MongoConfig.database.getCollection(USERS_COLLECTION) + + // Check if admin1 already exists + val existingAdmin = collection.find(Filters.eq("username", "mohannad")).first() + if (existingAdmin != null) { + println("Admin user already exists") + return + } + + // Create a new admin user with string UUID + val adminId = UUID.randomUUID() + val adminDoc = Document() + .append("_id", adminId.toString()) + .append("username", "mohannad") + .append("hashedPassword", UsersRepositoryImpl.encryptPassword("12345678")) + .append("role", User.UserRole.ADMIN.name) + .append("createdAt", LocalDateTime.now().toString()) + + collection.insertOne(adminDoc) + println("Created admin user: admin / 12345678") + + } catch (e: Exception) { + println("Error creating admin: ${e.message}") + e.printStackTrace() + } +} + diff --git a/src/main/kotlin/common/Constants.kt b/src/main/kotlin/common/Constants.kt new file mode 100644 index 0000000..28a6f85 --- /dev/null +++ b/src/main/kotlin/common/Constants.kt @@ -0,0 +1,37 @@ +package org.example.common + +object Constants { + object APPS { + const val AUTH_APP = "AUTH_APP" + const val ADMIN_APP = "ADMIN_APP" + const val MATE_APP = "MATE_APP" + } + + object Files { + const val PREFERENCES_FILE_NAME = "preferences.csv" + const val LOGS_FILE_NAME = "logs.csv" + const val PROJECTS_FILE_NAME = "projects.csv" + const val TASKS_FILE_NAME = "tasks.csv" + const val USERS_FILE_NAME = "users.csv" + } + + object PreferenceKeys { + const val CURRENT_USER_ID = "CURRENT_USER_ID" + const val CURRENT_USER_NAME = "CURRENT_USER_NAME" + const val CURRENT_USER_ROLE = "CURRENT_USER_ROLE" + } + + object MongoCollections { + const val LOGS_COLLECTION = "LOGS_COLLECTION" + const val TASKS_COLLECTION = "TASKS_COLLECTION" + const val PROJECTS_COLLECTION = "PROJECTS_COLLECTION" + const val USERS_COLLECTION = "USERS_COLLECTION" + } + + object NamedDataSources { + const val LOGS_DATA_SOURCE = "LOGS_DATA_SOURCE" + const val TASKS_DATA_SOURCE = "TASKS_DATA_SOURCE" + const val PROJECTS_DATA_SOURCE = "PROJECTS_DATA_SOURCE" + const val USERS_DATA_SOURCE = "USERS_DATA_SOURCE" + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/DataSource.kt b/src/main/kotlin/data/datasource/DataSource.kt new file mode 100644 index 0000000..6f3deaa --- /dev/null +++ b/src/main/kotlin/data/datasource/DataSource.kt @@ -0,0 +1,11 @@ +package data.datasource + +import java.util.UUID + +interface DataSource { + fun getAll(): List + fun getById(id: UUID): T + fun add(newItem: T) + fun delete(item: T) + fun update(updatedItem: T) +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/csv/CsvStorage.kt b/src/main/kotlin/data/datasource/csv/CsvStorage.kt new file mode 100644 index 0000000..a7c0369 --- /dev/null +++ b/src/main/kotlin/data/datasource/csv/CsvStorage.kt @@ -0,0 +1,45 @@ +package data.datasource.csv + +import data.datasource.DataSource +import java.io.File +import java.io.FileNotFoundException + +abstract class CsvStorage(val file: File) : DataSource { + abstract fun toCsvRow(item: T): String + abstract fun fromCsvRow(fields: List): T + + override fun getAll(): List { + if (!file.exists()) throw FileNotFoundException() + val lines = file.readLines() + + if (lines.isEmpty() || lines[0] != getHeaderString().trim()) { + throw IllegalArgumentException("Invalid CSV format: missing or incorrect header") + } + + return lines.drop(1) // Skip header + .filter { it.isNotEmpty() } + .map { row -> fromCsvRow(row.split(",")) } + } + + override fun add(newItem: T) { + if (!file.exists()) { + file.createNewFile() + file.writeText(getHeaderString()) + } + file.appendText(toCsvRow(newItem)) + } + + fun write(items: List) { + if (!file.exists()) { + file.createNewFile() + file.writeText(getHeaderString()) + } + val str = StringBuilder() + items.forEach { + str.append(toCsvRow(it)) + } + file.writeText(str.toString()) + } + + protected abstract fun getHeaderString(): String +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/csv/LogsCsvStorage.kt b/src/main/kotlin/data/datasource/csv/LogsCsvStorage.kt new file mode 100644 index 0000000..8ab4854 --- /dev/null +++ b/src/main/kotlin/data/datasource/csv/LogsCsvStorage.kt @@ -0,0 +1,137 @@ +package data.datasource.csv + +import org.example.domain.NotFoundException +import org.example.domain.entity.log.* +import org.example.domain.entity.log.Log.ActionType +import org.example.domain.entity.log.Log.AffectedType +import java.io.File +import java.time.LocalDateTime +import java.util.* + +class LogsCsvStorage(file: File) : CsvStorage(file) { + override fun toCsvRow(item: Log): String { + return when (item) { + is AddedLog -> listOf( + ActionType.ADDED.name, + item.username, + item.affectedId, + item.affectedName, + item.affectedType, + item.dateTime, + "", + item.addedTo + ) + + is ChangedLog -> listOf( + ActionType.CHANGED.name, + item.username, + item.affectedId, + item.affectedName, + item.affectedType, + item.dateTime, + item.changedFrom, + item.changedTo + ) + + is CreatedLog -> listOf( + ActionType.CREATED.name, + item.username, + item.affectedId, + item.affectedName, + item.affectedType, + item.dateTime, + "", + "" + ) + + is DeletedLog -> listOf( + ActionType.DELETED.name, + item.username, + item.affectedId, + item.affectedName, + item.affectedType, + item.dateTime, + item.deletedFrom ?: "", + "" + ) + }.joinToString(",") + "\n" + } + + override fun fromCsvRow(fields: List): Log { + if (fields.size != EXPECTED_COLUMNS) { + throw IllegalArgumentException("Invalid CSV format: wrong size of fields, expected $EXPECTED_COLUMNS but got ${fields.size}") + } + + val actionType = + ActionType.entries.firstOrNull { it.name == fields[ACTION_TYPE_INDEX] } + ?: throw IllegalArgumentException("Invalid action type: ${fields[ACTION_TYPE_INDEX]}") + + return when (actionType) { + ActionType.CHANGED -> ChangedLog( + username = fields[USERNAME_INDEX], + affectedId = UUID.fromString(fields[AFFECTED_ID_INDEX]), + affectedName = fields[AFFECTED_NAME_INDEX], + affectedType = AffectedType.valueOf(fields[AFFECTED_TYPE_INDEX]), + dateTime = LocalDateTime.parse(fields[DATE_TIME_INDEX]), + changedFrom = fields[FROM_INDEX], + changedTo = fields[TO_INDEX] + ) + + ActionType.ADDED -> AddedLog( + username = fields[USERNAME_INDEX], + affectedId = UUID.fromString(fields[AFFECTED_ID_INDEX]), + affectedName = fields[AFFECTED_NAME_INDEX], + affectedType = AffectedType.valueOf(fields[AFFECTED_TYPE_INDEX]), + dateTime = LocalDateTime.parse(fields[DATE_TIME_INDEX]), + addedTo = fields[TO_INDEX] + ) + + ActionType.DELETED -> DeletedLog( + username = fields[USERNAME_INDEX], + affectedId = UUID.fromString(fields[AFFECTED_ID_INDEX]), + affectedName = fields[AFFECTED_NAME_INDEX], + affectedType = AffectedType.valueOf(fields[AFFECTED_TYPE_INDEX]), + dateTime = LocalDateTime.parse(fields[DATE_TIME_INDEX]), + deletedFrom = fields[FROM_INDEX], + ) + + ActionType.CREATED -> CreatedLog( + username = fields[USERNAME_INDEX], + affectedId = UUID.fromString(fields[AFFECTED_ID_INDEX]), + affectedName = fields[AFFECTED_NAME_INDEX], + affectedType = AffectedType.valueOf(fields[AFFECTED_TYPE_INDEX]), + dateTime = LocalDateTime.parse(fields[DATE_TIME_INDEX]), + ) + } + } + + override fun getHeaderString(): String { + return CSV_HEADER + } + + override fun getById(id: UUID): Log { + return getAll().find { it.affectedId == id } ?: throw NotFoundException("log") + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("logs") } + + override fun delete(item: Log) {} + + override fun update(updatedItem: Log) {} + + companion object { + private const val ACTION_TYPE_INDEX = 0 + private const val USERNAME_INDEX = 1 + private const val AFFECTED_ID_INDEX = 2 + private const val AFFECTED_NAME_INDEX = 3 + private const val AFFECTED_TYPE_INDEX = 4 + private const val DATE_TIME_INDEX = 5 + private const val FROM_INDEX = 6 + private const val TO_INDEX = 7 + + private const val EXPECTED_COLUMNS = 8 + + private const val CSV_HEADER = + "ActionType,username,affectedId,affectedName,affectedType,dateTime,from,to\n" + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/csv/ProjectsCsvStorage.kt b/src/main/kotlin/data/datasource/csv/ProjectsCsvStorage.kt new file mode 100644 index 0000000..e7f6580 --- /dev/null +++ b/src/main/kotlin/data/datasource/csv/ProjectsCsvStorage.kt @@ -0,0 +1,80 @@ +package data.datasource.csv + +import org.example.domain.NotFoundException +import org.example.domain.entity.Project +import org.example.domain.entity.State +import java.io.File +import java.time.LocalDateTime +import java.util.* + +class ProjectsCsvStorage(file: File) : CsvStorage(file) { + + override fun toCsvRow(item: Project): String { + val states = item.states.joinToString("|") + val matesIds = item.matesIds.joinToString("|") + return "${item.id},${item.name},${states},${item.createdBy},${matesIds},${item.createdAt}\n" + } + + override fun fromCsvRow(fields: List): Project { + require(fields.size == EXPECTED_COLUMNS) { "Invalid project data format: " } + + val states = + if (fields[STATES_INDEX].isNotEmpty()) fields[STATES_INDEX].split(MULTI_VALUE_SEPARATOR) + .map { + it.split(STATE_SEPARATOR).let { state -> State(UUID.fromString(state[0]), state[1]) } + } else emptyList() + val matesIds = if (fields[MATES_IDS_INDEX].isNotEmpty()) fields[MATES_IDS_INDEX].split("|") else emptyList() + + val project = Project( + id = UUID.fromString(fields[ID_INDEX]), + name = fields[NAME_INDEX], + states = states, + createdBy = UUID.fromString(fields[CREATED_BY_INDEX]), + matesIds = matesIds.map(UUID::fromString), + createdAt = LocalDateTime.parse(fields[CREATED_AT_INDEX]) + ) + + return project + } + + override fun getHeaderString(): String { + return CSV_HEADER + } + + override fun update(updatedItem: Project) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == updatedItem.id } + if (itemIndex == -1) throw NotFoundException("$updatedItem") + list[itemIndex] = updatedItem + write(list) + } + + override fun getById(id: UUID): Project { + return getAll().find { it.id == id } ?: throw NotFoundException("project") + } + + override fun delete(item: Project) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == item.id } + if (itemIndex == -1) throw NotFoundException("$item") + list.removeAt(itemIndex) + write(list) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("projects") } + + companion object { + private const val ID_INDEX = 0 + private const val NAME_INDEX = 1 + private const val STATES_INDEX = 2 + private const val CREATED_BY_INDEX = 3 + private const val MATES_IDS_INDEX = 4 + private const val CREATED_AT_INDEX = 5 + private const val EXPECTED_COLUMNS = 6 + private const val CSV_HEADER = "id,name,states,createdBy,matesIds,createdAt\n" + private const val MULTI_VALUE_SEPARATOR = "|" + private const val STATE_SEPARATOR = ":" + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/csv/TasksCsvStorage.kt b/src/main/kotlin/data/datasource/csv/TasksCsvStorage.kt new file mode 100644 index 0000000..2e48520 --- /dev/null +++ b/src/main/kotlin/data/datasource/csv/TasksCsvStorage.kt @@ -0,0 +1,75 @@ +package data.datasource.csv + +import org.example.domain.NotFoundException +import org.example.domain.entity.State +import org.example.domain.entity.Task +import java.io.File +import java.time.LocalDateTime +import java.util.* + +class TasksCsvStorage(file: File) : CsvStorage(file) { + + override fun toCsvRow(item: Task): String { + val assignedTo = item.assignedTo.joinToString("|") + return "${item.id},${item.title},${item.state},${assignedTo},${item.createdBy},${item.projectId},${item.createdAt}\n" + } + + override fun fromCsvRow(fields: List): Task { + require(fields.size == EXPECTED_COLUMNS) { "Invalid task data format: " } + val assignedTo = + if (fields[ASSIGNED_TO_INDEX].isNotEmpty()) fields[ASSIGNED_TO_INDEX].split(MULTI_VALUE_SEPARATOR) + .map { UUID.fromString(it) } else emptyList() + val task = Task( + id = UUID.fromString(fields[ID_INDEX]), + title = fields[TITLE_INDEX], + state = fields[STATE_INDEX].split(STATE_SEPARATOR).let { State(UUID.fromString(it[0]), it[1]) }, + assignedTo = assignedTo, + createdBy = UUID.fromString(fields[CREATED_BY_INDEX]), + projectId = UUID.fromString(fields[PROJECT_ID_INDEX]), + createdAt = LocalDateTime.parse(fields[CREATED_AT_INDEX]) + ) + return task + } + + override fun getHeaderString(): String { + return CSV_HEADER + } + + override fun update(updatedItem: Task) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == updatedItem.id } + if (itemIndex == -1) throw NotFoundException("$updatedItem") + list[itemIndex] = updatedItem + write(list) + } + + override fun getById(id: UUID): Task { + return getAll().find { it.id == id } ?: throw NotFoundException("task") + } + + override fun delete(item: Task) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == item.id } + if (itemIndex == -1) throw NotFoundException("$item") + list.removeAt(itemIndex) + write(list) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("tasks") } + + companion object { + const val CSV_HEADER = "id,title,state,assignedTo,createdBy,projectId,createdAt\n" + private const val ID_INDEX = 0 + private const val TITLE_INDEX = 1 + private const val STATE_INDEX = 2 + private const val ASSIGNED_TO_INDEX = 3 + private const val CREATED_BY_INDEX = 4 + private const val PROJECT_ID_INDEX = 5 + private const val CREATED_AT_INDEX = 6 + private const val EXPECTED_COLUMNS = 7 + private const val MULTI_VALUE_SEPARATOR = "|" + private const val STATE_SEPARATOR = ":" + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/csv/UsersCsvStorage.kt b/src/main/kotlin/data/datasource/csv/UsersCsvStorage.kt new file mode 100644 index 0000000..b9fca64 --- /dev/null +++ b/src/main/kotlin/data/datasource/csv/UsersCsvStorage.kt @@ -0,0 +1,63 @@ +package data.datasource.csv + +import org.example.domain.NotFoundException +import org.example.domain.entity.User +import java.io.File +import java.time.LocalDateTime +import java.util.* + +class UsersCsvStorage(file: File) : CsvStorage(file) { + override fun toCsvRow(item: User): String { + return "${item.id},${item.username},${item.hashedPassword},${item.role},${item.cratedAt}\n" + } + + override fun fromCsvRow(fields: List): User { + require(fields.size == EXPECTED_COLUMNS) { "Invalid user data format: " } + val user = User( + id = UUID.fromString(fields[ID_INDEX]), + username = fields[USERNAME_INDEX], + hashedPassword = fields[PASSWORD_INDEX], + role = User.UserRole.valueOf(fields[TYPE_INDEX]), + cratedAt = LocalDateTime.parse(fields[CREATED_AT_INDEX]) + ) + return user + } + + override fun getHeaderString(): String { + return CSV_HEADER + } + + override fun update(updatedItem: User) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == updatedItem.id } + if (itemIndex == -1) throw NotFoundException("$updatedItem") + list[itemIndex] = updatedItem + write(list) + } + + override fun getById(id: UUID): User { + return getAll().find { it.id == id } ?: throw NotFoundException("user") + } + + override fun delete(item: User) { + if (!file.exists()) throw NotFoundException("file") + val list = getAll().toMutableList() + val itemIndex = list.indexOfFirst { it.id == item.id } + if (itemIndex == -1) throw NotFoundException("$item") + list.removeAt(itemIndex) + write(list) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("users") } + + companion object { + const val CSV_HEADER = "id,username,password,type,createdAt\n" + private const val ID_INDEX = 0 + private const val USERNAME_INDEX = 1 + private const val PASSWORD_INDEX = 2 + private const val TYPE_INDEX = 3 + private const val CREATED_AT_INDEX = 4 + private const val EXPECTED_COLUMNS = 5 + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/LogsMongoStorage.kt b/src/main/kotlin/data/datasource/mongo/LogsMongoStorage.kt new file mode 100644 index 0000000..e58a5cb --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/LogsMongoStorage.kt @@ -0,0 +1,95 @@ +package data.datasource.mongo + + +import org.bson.Document +import org.example.common.Constants.MongoCollections.LOGS_COLLECTION +import org.example.domain.NotFoundException +import org.example.domain.entity.log.* +import org.example.domain.entity.log.Log.ActionType +import org.example.domain.entity.log.Log.AffectedType +import java.time.LocalDateTime +import java.util.* + +class LogsMongoStorage : MongoStorage(MongoConfig.database.getCollection(LOGS_COLLECTION)) { + override fun toDocument(item: Log): Document { + val doc = Document() + .append("username", item.username) + .append("affectedId", item.affectedId.toString()) + .append("affectedName", item.affectedName) + .append("affectedType", item.affectedType.name) + .append("dateTime", item.dateTime.toString()) + + when (item) { + is AddedLog -> { + doc.append("actionType", ActionType.ADDED.name) + .append("addedTo", item.addedTo) + } + + is ChangedLog -> { + doc.append("actionType", ActionType.CHANGED.name) + .append("changedFrom", item.changedFrom) + .append("changedTo", item.changedTo) + } + + is CreatedLog -> { + doc.append("actionType", ActionType.CREATED.name) + } + + is DeletedLog -> { + doc.append("actionType", ActionType.DELETED.name) + .append("deletedFrom", item.deletedFrom) + } + } + + return doc + } + + override fun fromDocument(document: Document): Log { + val actionType = ActionType.valueOf(document.get("actionType", String::class.java)) + val username = document.get("username", String::class.java) + val affectedId = UUID.fromString(document.get("affectedId", String::class.java)) + val affectedName = document.get("affectedName", String::class.java) + val affectedType = AffectedType.valueOf(document.get("affectedType", String::class.java)) + val dateTime = LocalDateTime.parse(document.get("dateTime", String::class.java)) + + return when (actionType) { + ActionType.ADDED -> AddedLog( + username = username, + affectedId = affectedId, + affectedName = affectedName, + affectedType = affectedType, + dateTime = dateTime, + addedTo = document.get("addedTo", String::class.java) + ) + + ActionType.CHANGED -> ChangedLog( + username = username, + affectedId = affectedId, + affectedName = affectedName, + affectedType = affectedType, + dateTime = dateTime, + changedFrom = document.get("changedFrom", String::class.java), + changedTo = document.get("changedTo", String::class.java) + ) + + ActionType.CREATED -> CreatedLog( + username = username, + affectedId = affectedId, + affectedName = affectedName, + affectedType = affectedType, + dateTime = dateTime + ) + + ActionType.DELETED -> DeletedLog( + username = username, + affectedId = affectedId, + affectedName = affectedName, + affectedType = affectedType, + dateTime = dateTime, + deletedFrom = document.get("deletedFrom", String::class.java) + ) + } + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("logs") } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/MongoConfig.kt b/src/main/kotlin/data/datasource/mongo/MongoConfig.kt new file mode 100644 index 0000000..513011f --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/MongoConfig.kt @@ -0,0 +1,40 @@ +package data.datasource.mongo + +import com.mongodb.ConnectionString +import com.mongodb.MongoClientSettings +import com.mongodb.client.MongoClient +import com.mongodb.client.MongoClients +import com.mongodb.client.MongoDatabase +import org.bson.UuidRepresentation + +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.pojo.PojoCodecProvider +import org.example.BuildConfig + +object MongoConfig { + private const val DATABASE_NAME = BuildConfig.DATABASE_NAME + private const val CONNECTION_STRING = BuildConfig.MONGO_URI + + val client: MongoClient by lazy { + val pojoCodecRegistry = CodecRegistries.fromProviders( + PojoCodecProvider.builder().automatic(true).build() + ) + + val codecRegistry = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), + pojoCodecRegistry + ) + + val settings = MongoClientSettings.builder() + .applyConnectionString(ConnectionString(CONNECTION_STRING)) + .uuidRepresentation(UuidRepresentation.STANDARD) + .codecRegistry(codecRegistry) + .build() + + MongoClients.create(settings) + } + + val database: MongoDatabase by lazy { + client.getDatabase(DATABASE_NAME) + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/MongoStorage.kt b/src/main/kotlin/data/datasource/mongo/MongoStorage.kt new file mode 100644 index 0000000..e50b059 --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/MongoStorage.kt @@ -0,0 +1,50 @@ +package data.datasource.mongo + +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.Filters +import org.bson.Document +import data.datasource.DataSource +import org.example.domain.NotFoundException +import org.example.domain.UnknownException +import java.util.UUID + +abstract class MongoStorage( + protected val collection: MongoCollection +) : DataSource { + + abstract fun toDocument(item: T): Document + abstract fun fromDocument(document: Document): T + + override fun getAll() = collection.find().map { fromDocument(it) }.toList() + + override fun getById(id: UUID): T { + return collection.find(Filters.eq("_id", id.toString())).firstOrNull()?.let { + fromDocument(it) + } ?: throw NotFoundException("") + } + + override fun add(newItem: T) { + collection.insertOne(toDocument(newItem)).let { result -> + if (!result.wasAcknowledged()) throw UnknownException() + } + } + + override fun delete(item: T) { + val document = toDocument(item) + val result = collection.deleteOne(Filters.eq("_id", document.getString("_id"))) + if (result.deletedCount == 0L) { + throw NotFoundException("Item not found") + } + } + + override fun update(updatedItem: T) { + val document = toDocument(updatedItem) + val result = collection.replaceOne( + Filters.eq("_id", document.getString("_id")), + document + ) + if (result.matchedCount == 0L) { + throw NotFoundException("Item not found") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/ProjectsMongoStorage.kt b/src/main/kotlin/data/datasource/mongo/ProjectsMongoStorage.kt new file mode 100644 index 0000000..e4ace6a --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/ProjectsMongoStorage.kt @@ -0,0 +1,44 @@ +package data.datasource.mongo + + +import org.bson.Document +import org.example.common.Constants.MongoCollections.PROJECTS_COLLECTION +import org.example.domain.NotFoundException +import org.example.domain.entity.Project +import org.example.domain.entity.State +import java.time.LocalDateTime +import java.util.* + +class ProjectsMongoStorage : MongoStorage(MongoConfig.database.getCollection(PROJECTS_COLLECTION)) { + override fun toDocument(item: Project): Document { + return Document() + .append("_id", item.id.toString()) + .append("name", item.name) + .append("states", item.states.map { it.toString() }) + .append("createdBy", item.createdBy.toString()) + .append("createdAt", item.createdAt.toString()) + .append("matesIds", item.matesIds.map { it.toString() }) + } + + override fun fromDocument(document: Document): Project { + val states = document.getList("states", String::class.java).map { + it.split(":").let { state -> + State(UUID.fromString(state[0]), state[1]) + } + } + val matesIdsStrings = document.getList("matesIds", String::class.java) ?: emptyList() + val matesIds = matesIdsStrings.map { UUID.fromString(it) } + val uuidStr = document.getString("_id") + val createdByStr = document.getString("createdBy") + return Project( + id = UUID.fromString(uuidStr), + name = document.getString("name"), + states = states, + createdBy = UUID.fromString(createdByStr), + createdAt = LocalDateTime.parse(document.getString("createdAt")), + matesIds = matesIds + ) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("projects") } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/TasksMongoStorage.kt b/src/main/kotlin/data/datasource/mongo/TasksMongoStorage.kt new file mode 100644 index 0000000..3b08c2e --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/TasksMongoStorage.kt @@ -0,0 +1,45 @@ +package data.datasource.mongo + + +import org.bson.Document +import org.example.common.Constants.MongoCollections.TASKS_COLLECTION +import org.example.domain.NotFoundException +import org.example.domain.entity.State +import org.example.domain.entity.Task +import java.time.LocalDateTime +import java.util.* + + +class TasksMongoStorage : MongoStorage(MongoConfig.database.getCollection(TASKS_COLLECTION)) { + override fun toDocument(item: Task): Document { + return Document() + .append("_id", item.id.toString()) + .append("title", item.title) + .append("state", item.state.toString()) + .append("assignedTo", item.assignedTo.map { it.toString() }) + .append("createdBy", item.createdBy) + .append("createdAt", item.createdAt.toString()) + .append("projectId", item.projectId) + } + + override fun fromDocument(document: Document): Task { + val assignedToStrings = document.getList("assignedTo", String::class.java) ?: emptyList() + val assignedTo = assignedToStrings.map { UUID.fromString(it) } + val uuidStr = document.getString("_id") + val state = document.get("state", String::class.java).let { + it.split(":").let { str -> State(UUID.fromString(str[0]), str[1]) } + } + + return Task( + id = UUID.fromString(uuidStr), + title = document.get("title", String::class.java), + state = state, + assignedTo = assignedTo, + createdBy = document.get("createdBy", UUID::class.java), + createdAt = LocalDateTime.parse(document.get("createdAt", String::class.java)), + projectId = document.get("projectId", UUID::class.java) + ) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("tasks") } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/mongo/UsersMongoStorage.kt b/src/main/kotlin/data/datasource/mongo/UsersMongoStorage.kt new file mode 100644 index 0000000..1510594 --- /dev/null +++ b/src/main/kotlin/data/datasource/mongo/UsersMongoStorage.kt @@ -0,0 +1,34 @@ +package data.datasource.mongo + +import org.bson.Document +import org.example.common.Constants.MongoCollections.USERS_COLLECTION +import org.example.domain.NotFoundException +import org.example.domain.entity.User +import java.time.LocalDateTime +import java.util.* + + +class UsersMongoStorage : MongoStorage(MongoConfig.database.getCollection(USERS_COLLECTION)) { + override fun toDocument(item: User): Document { + return Document() + .append("_id", item.id.toString()) + .append("uuid", item.id.toString()) // Store UUID as string + .append("username", item.username) + .append("hashedPassword", item.hashedPassword) + .append("role", item.role.name) + .append("createdAt", item.cratedAt.toString()) + } + + override fun fromDocument(document: Document): User { + val uuidStr = document.getString("_id") + return User( + id = UUID.fromString(uuidStr), + username = document.getString("username"), + hashedPassword = document.getString("hashedPassword"), + role = User.UserRole.valueOf(document.getString("role")), + cratedAt = LocalDateTime.parse(document.getString("createdAt")) + ) + } + + override fun getAll() = super.getAll().ifEmpty { throw NotFoundException("users") } +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/preferences/Preference.kt b/src/main/kotlin/data/datasource/preferences/Preference.kt new file mode 100644 index 0000000..99b9fcb --- /dev/null +++ b/src/main/kotlin/data/datasource/preferences/Preference.kt @@ -0,0 +1,12 @@ +package data.datasource.preferences + +import org.example.domain.entity.User.UserRole +import java.util.UUID + +interface Preference { + fun saveUser(userId: UUID, username: String, role: UserRole) + fun getCurrentUserID(): UUID + fun getCurrentUserName(): String + fun getCurrentUserRole(): UserRole + fun clear() +} \ No newline at end of file diff --git a/src/main/kotlin/data/datasource/preferences/UserPreferences.kt b/src/main/kotlin/data/datasource/preferences/UserPreferences.kt new file mode 100644 index 0000000..630ac02 --- /dev/null +++ b/src/main/kotlin/data/datasource/preferences/UserPreferences.kt @@ -0,0 +1,60 @@ +package data.datasource.preferences + +import org.example.common.Constants.PreferenceKeys.CURRENT_USER_ID +import org.example.common.Constants.PreferenceKeys.CURRENT_USER_NAME +import org.example.common.Constants.PreferenceKeys.CURRENT_USER_ROLE +import org.example.domain.UnauthorizedException +import org.example.domain.entity.User.UserRole +import java.io.File +import java.util.* + +class UserPreferences(private val file: File) : Preference { + private val map: MutableMap = mutableMapOf() + + init { + read() + } + + override fun saveUser( + userId: UUID, + username: String, + role: UserRole + ) { + map[CURRENT_USER_ID] = userId.toString() + map[CURRENT_USER_NAME] = username + map[CURRENT_USER_ROLE] = role.toString() + write(map.toList()) + } + + override fun getCurrentUserID(): UUID = + map.getOrElse(CURRENT_USER_ID) { throw UnauthorizedException() }.let { UUID.fromString(it) } + + override fun getCurrentUserName(): String = map.getOrElse(CURRENT_USER_NAME) { throw UnauthorizedException() } + + override fun getCurrentUserRole(): UserRole = + map.getOrElse(CURRENT_USER_ROLE) { throw UnauthorizedException() }.let { UserRole.valueOf(it) } + + + override fun clear() { + map.clear() + write(map.toList()) + } + + private fun write(items: List>) { + if (!file.exists()) file.createNewFile() + val str = StringBuilder() + items.forEach { + str.append("${it.first},${it.second}\n") + } + file.writeText(str.toString()) + } + + private fun read() { + if (file.exists()) { + file.readLines().forEach { line -> + val fields = line.split(",") + map[fields[0]] = fields[1] + } + } + } +} diff --git a/src/main/kotlin/data/repository/LogsRepositoryImpl.kt b/src/main/kotlin/data/repository/LogsRepositoryImpl.kt new file mode 100644 index 0000000..4c1f6de --- /dev/null +++ b/src/main/kotlin/data/repository/LogsRepositoryImpl.kt @@ -0,0 +1,14 @@ +package org.example.data.repository + +import data.datasource.DataSource +import org.example.data.utils.safeCall +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository + +class LogsRepositoryImpl( + private val logsDataSource: DataSource, +) : LogsRepository { + override fun getAllLogs() = safeCall { logsDataSource.getAll() } + override fun addLog(log: Log) = safeCall { logsDataSource.add(log) } +} + diff --git a/src/main/kotlin/data/repository/ProjectsRepositoryImpl.kt b/src/main/kotlin/data/repository/ProjectsRepositoryImpl.kt new file mode 100644 index 0000000..aebed7b --- /dev/null +++ b/src/main/kotlin/data/repository/ProjectsRepositoryImpl.kt @@ -0,0 +1,20 @@ +package org.example.data.repository + +import data.datasource.DataSource +import org.example.data.utils.safeCall +import org.example.domain.entity.Project +import org.example.domain.repository.ProjectsRepository +import java.util.* + +class ProjectsRepositoryImpl( + private val projectsDataSource: DataSource, +) : ProjectsRepository { + override fun getProjectById(projectId: UUID) = safeCall { projectsDataSource.getById(projectId) } + override fun getAllProjects() = safeCall { projectsDataSource.getAll() } + override fun addProject(project: Project) = safeCall { projectsDataSource.add(project) } + override fun updateProject(updatedProject: Project) = + safeCall { projectsDataSource.update(updatedProject) } + + override fun deleteProjectById(projectId: UUID) = + safeCall { projectsDataSource.delete(getProjectById(projectId)) } +} \ No newline at end of file diff --git a/src/main/kotlin/data/repository/TasksRepositoryImpl.kt b/src/main/kotlin/data/repository/TasksRepositoryImpl.kt new file mode 100644 index 0000000..dbc9cfe --- /dev/null +++ b/src/main/kotlin/data/repository/TasksRepositoryImpl.kt @@ -0,0 +1,17 @@ +package org.example.data.repository + +import data.datasource.DataSource +import org.example.data.utils.safeCall +import org.example.domain.entity.Task +import org.example.domain.repository.TasksRepository +import java.util.* + +class TasksRepositoryImpl( + private val tasksDataSource: DataSource, +) : TasksRepository { + override fun getTaskById(taskId: UUID) = safeCall { tasksDataSource.getById(taskId) } + override fun getAllTasks() = safeCall { tasksDataSource.getAll() } + override fun addTask(newTask: Task) = safeCall { tasksDataSource.add(newTask) } + override fun updateTask(updatedTask: Task) = safeCall { tasksDataSource.update(updatedTask) } + override fun deleteTaskById(taskId: UUID) = safeCall { tasksDataSource.delete(getTaskById(taskId)) } +} \ No newline at end of file diff --git a/src/main/kotlin/data/repository/UsersRepositoryImpl.kt b/src/main/kotlin/data/repository/UsersRepositoryImpl.kt new file mode 100644 index 0000000..2d2e902 --- /dev/null +++ b/src/main/kotlin/data/repository/UsersRepositoryImpl.kt @@ -0,0 +1,36 @@ +package org.example.data.repository + +import data.datasource.DataSource +import data.datasource.preferences.Preference +import org.example.data.utils.safeCall +import org.example.domain.entity.User +import org.example.domain.repository.UsersRepository +import java.security.MessageDigest +import java.util.* + + +class UsersRepositoryImpl( + private val usersDataSource: DataSource, + private val preferences: Preference, +) : UsersRepository { + override fun storeUserData(userId: UUID, username: String, role: User.UserRole) = safeCall { + preferences.saveUser(userId = userId, username = username, role = role) + } + + override fun getAllUsers() = safeCall { usersDataSource.getAll() } + + override fun createUser(user: User) = safeCall { + usersDataSource.add(user.copy(hashedPassword = encryptPassword(user.hashedPassword))) + } + + override fun getCurrentUser() = safeCall { getUserByID(preferences.getCurrentUserID()) } + + override fun getUserByID(userId: UUID) = safeCall { usersDataSource.getById(userId) } + + override fun clearUserData() = safeCall { preferences.clear() } + + companion object { + fun encryptPassword(password: String) = + MessageDigest.getInstance("MD5").digest(password.toByteArray()).joinToString("") { "%02x".format(it) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/data/utils/SafeCall.kt b/src/main/kotlin/data/utils/SafeCall.kt new file mode 100644 index 0000000..a72d045 --- /dev/null +++ b/src/main/kotlin/data/utils/SafeCall.kt @@ -0,0 +1,15 @@ +package org.example.data.utils + +import org.example.domain.PlanMateAppException +import org.example.domain.UnknownException + + +fun safeCall(bloc: () -> T): T { + return try { + bloc() + } catch (planMateException: PlanMateAppException) { + throw planMateException + } catch (_: Exception) { + throw UnknownException() + } +} diff --git a/src/main/kotlin/di/AppModule.kt b/src/main/kotlin/di/AppModule.kt new file mode 100644 index 0000000..0b65054 --- /dev/null +++ b/src/main/kotlin/di/AppModule.kt @@ -0,0 +1,15 @@ +package di + +import org.example.common.Constants +import org.example.presentation.AdminApp +import org.example.presentation.App +import org.example.presentation.AuthApp +import org.example.presentation.MateApp +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val appModule = module { + single(named(Constants.APPS.AUTH_APP)) { AuthApp() } + single(named(Constants.APPS.ADMIN_APP)) { AdminApp() } + single(named(Constants.APPS.MATE_APP)) { MateApp() } +} \ No newline at end of file diff --git a/src/main/kotlin/di/DataModule.kt b/src/main/kotlin/di/DataModule.kt new file mode 100644 index 0000000..ac2dc86 --- /dev/null +++ b/src/main/kotlin/di/DataModule.kt @@ -0,0 +1,30 @@ +package org.example.di + +import data.datasource.DataSource +import data.datasource.mongo.LogsMongoStorage +import data.datasource.mongo.ProjectsMongoStorage +import data.datasource.mongo.TasksMongoStorage +import data.datasource.mongo.UsersMongoStorage +import data.datasource.preferences.UserPreferences +import data.datasource.preferences.Preference +import org.example.common.Constants +import org.example.common.Constants.NamedDataSources.LOGS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.PROJECTS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.TASKS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.USERS_DATA_SOURCE +import org.example.domain.entity.log.Log +import org.example.domain.entity.Project +import org.example.domain.entity.Task +import org.example.domain.entity.User +import org.koin.core.qualifier.named +import org.koin.dsl.module +import java.io.File + +val dataModule = module { + single { UserPreferences(File(Constants.Files.PREFERENCES_FILE_NAME)) } + + single>(named(LOGS_DATA_SOURCE)) { LogsMongoStorage() } + single>(named(PROJECTS_DATA_SOURCE)) { ProjectsMongoStorage() } + single>(named(TASKS_DATA_SOURCE)) { TasksMongoStorage() } + single>(named(USERS_DATA_SOURCE)) { UsersMongoStorage() } +} diff --git a/src/main/kotlin/di/RepositoryModule.kt b/src/main/kotlin/di/RepositoryModule.kt new file mode 100644 index 0000000..6033db1 --- /dev/null +++ b/src/main/kotlin/di/RepositoryModule.kt @@ -0,0 +1,24 @@ +package org.example.di + +import org.example.common.Constants.NamedDataSources.LOGS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.PROJECTS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.TASKS_DATA_SOURCE +import org.example.common.Constants.NamedDataSources.USERS_DATA_SOURCE +import org.example.data.repository.LogsRepositoryImpl +import org.example.data.repository.ProjectsRepositoryImpl +import org.example.data.repository.TasksRepositoryImpl +import org.example.data.repository.UsersRepositoryImpl +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.koin.core.qualifier.named +import org.koin.dsl.module + + +val repositoryModule = module { + single { LogsRepositoryImpl(get(named(LOGS_DATA_SOURCE))) } + single { ProjectsRepositoryImpl(get(named(PROJECTS_DATA_SOURCE))) } + single { TasksRepositoryImpl(get(named(TASKS_DATA_SOURCE))) } + single { UsersRepositoryImpl(get(named(USERS_DATA_SOURCE)), get()) } +} \ No newline at end of file diff --git a/src/main/kotlin/di/UseCasesModule.kt b/src/main/kotlin/di/UseCasesModule.kt new file mode 100644 index 0000000..9ca4d3f --- /dev/null +++ b/src/main/kotlin/di/UseCasesModule.kt @@ -0,0 +1,33 @@ +package di + +import org.example.domain.usecase.auth.CreateUserUseCase +import org.example.domain.usecase.auth.LoginUseCase +import org.example.domain.usecase.auth.LogoutUseCase +import org.example.domain.usecase.project.* +import org.example.domain.usecase.task.* +import org.koin.dsl.module + + +val useCasesModule = module { + single { LogoutUseCase(get()) } + single { LoginUseCase(get()) } + single { CreateUserUseCase(get(), get()) } + single { AddMateToProjectUseCase(get(), get(), get()) } + single { AddStateToProjectUseCase(get(), get(), get()) } + single { CreateProjectUseCase(get(), get(), get()) } + single { DeleteMateFromProjectUseCase(get(), get(), get()) } + single { DeleteProjectUseCase(get(), get(), get()) } + single { DeleteStateFromProjectUseCase(get(), get(), get()) } + single { EditProjectNameUseCase(get(), get(), get()) } + single { GetAllTasksOfProjectUseCase(get(), get(), get()) } + single { GetProjectHistoryUseCase(get(),get(),get()) } + single { CreateTaskUseCase(get(), get(), get(), get()) } + single { DeleteTaskUseCase(get(), get(), get(), get()) } + single { GetTaskHistoryUseCase(get()) } + single { GetTaskUseCase(get(), get(), get()) } + single { AddMateToTaskUseCase(get(), get(), get(), get()) } + single { DeleteMateFromTaskUseCase(get(), get(), get(), get()) } + single { EditTaskStateUseCase(get(), get(), get(), get()) } + single { EditTaskTitleUseCase(get(), get(), get(), get()) } + single { GetAllProjectsUseCase(get(), get()) } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/Exceptions.kt b/src/main/kotlin/domain/Exceptions.kt new file mode 100644 index 0000000..879d223 --- /dev/null +++ b/src/main/kotlin/domain/Exceptions.kt @@ -0,0 +1,15 @@ +package org.example.domain + +abstract class PlanMateAppException(message: String) : Throwable(message) + +class LoginException(message: String = "LoginException!!") : PlanMateAppException(message) +class RegisterException(message: String = "RegisterException!!") : PlanMateAppException(message) +class UnauthorizedException() : PlanMateAppException("You are not authorized.") +class AccessDeniedException(type: String) : PlanMateAppException("You do not have access for this $type.") +class NotFoundException(type: String = "") : PlanMateAppException("Not $type found.") +class InvalidInputException(message: String? = null) : PlanMateAppException("${message ?: "Invalid input provided."} ") +class AlreadyExistException(type: String) : PlanMateAppException("The $type already exist.") +class UnknownException() : PlanMateAppException("Something went wrong.") +class NoChangeException() : PlanMateAppException("There is no any change.") +class TaskHasNoException(type: String) : PlanMateAppException("Task has no this $type.") +class ProjectHasNoException(type: String) : PlanMateAppException("Project has no this $type.") \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/Project.kt b/src/main/kotlin/domain/entity/Project.kt new file mode 100644 index 0000000..fbf2179 --- /dev/null +++ b/src/main/kotlin/domain/entity/Project.kt @@ -0,0 +1,24 @@ +package org.example.domain.entity + +import java.time.LocalDateTime +import java.util.UUID + +data class Project( + val id: UUID = UUID.randomUUID(), + val name: String, + val states: List = emptyList(), + val createdBy: UUID, + val createdAt: LocalDateTime = LocalDateTime.now(), + val matesIds: List = emptyList() +) { + override fun toString(): String { + return """ + Project ID: $id + Name: $name + States: ${states.map { it.name }} + Mates IDs: $matesIds + Created By: $createdBy + Created At: $createdAt + """.trimIndent() + } +} diff --git a/src/main/kotlin/domain/entity/State.kt b/src/main/kotlin/domain/entity/State.kt new file mode 100644 index 0000000..981bc4d --- /dev/null +++ b/src/main/kotlin/domain/entity/State.kt @@ -0,0 +1,10 @@ +package org.example.domain.entity + +import java.util.UUID + +data class State( + val id: UUID = UUID.randomUUID(), + val name: String +) { + override fun toString() = "$id:$name" +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/Task.kt b/src/main/kotlin/domain/entity/Task.kt new file mode 100644 index 0000000..426f685 --- /dev/null +++ b/src/main/kotlin/domain/entity/Task.kt @@ -0,0 +1,26 @@ +package org.example.domain.entity + +import java.time.LocalDateTime +import java.util.UUID + +data class Task( + val id: UUID = UUID.randomUUID(), + val title: String, + val state: State, + val assignedTo: List = emptyList(), + val createdBy: UUID, + val createdAt: LocalDateTime = LocalDateTime.now(), + val projectId: UUID, +) { + override fun toString(): String { + return """ + Task ID: $id + Title: $title + State: ${state.name} + Assigned To: ${assignedTo.joinToString(", ")} + Created By: $createdBy + Created At: $createdAt + Project ID: $projectId + """.trimIndent() + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/User.kt b/src/main/kotlin/domain/entity/User.kt new file mode 100644 index 0000000..b157e9f --- /dev/null +++ b/src/main/kotlin/domain/entity/User.kt @@ -0,0 +1,15 @@ +package org.example.domain.entity + +import java.time.LocalDateTime +import java.util.UUID + +data class User( + val id: UUID = UUID.randomUUID(), + val username: String, + val hashedPassword: String,//hashed using MD5 + val role: UserRole, + val cratedAt: LocalDateTime = LocalDateTime.now(), +){ + enum class UserRole { ADMIN, MATE } +} + diff --git a/src/main/kotlin/domain/entity/log/AddedLog.kt b/src/main/kotlin/domain/entity/log/AddedLog.kt new file mode 100644 index 0000000..c860366 --- /dev/null +++ b/src/main/kotlin/domain/entity/log/AddedLog.kt @@ -0,0 +1,16 @@ +package org.example.domain.entity.log + +import java.time.LocalDateTime +import java.util.UUID + +class AddedLog( + username: String, + affectedId: UUID, + affectedName: String, + affectedType: AffectedType, + dateTime: LocalDateTime = LocalDateTime.now(), + val addedTo: String, +) : Log(username, affectedId, affectedName, affectedType, dateTime) { + override fun toString() = + "user $username ${ActionType.ADDED.name.lowercase()} ${affectedType.name.lowercase()} $affectedName [$affectedId] to $addedTo at $dateTime" +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/log/ChangedLog.kt b/src/main/kotlin/domain/entity/log/ChangedLog.kt new file mode 100644 index 0000000..b4b667e --- /dev/null +++ b/src/main/kotlin/domain/entity/log/ChangedLog.kt @@ -0,0 +1,17 @@ +package org.example.domain.entity.log + +import java.time.LocalDateTime +import java.util.UUID + +class ChangedLog( + username: String, + affectedId: UUID, + affectedName: String, + affectedType: AffectedType, + dateTime: LocalDateTime = LocalDateTime.now(), + val changedFrom: String, + val changedTo: String, +) : Log(username, affectedId, affectedName, affectedType, dateTime) { + override fun toString() = + "user $username ${ActionType.CHANGED.name.lowercase()} ${affectedType.name.lowercase()} $affectedName [$affectedId] from $changedFrom to $changedTo at $dateTime" +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/log/CreatedLog.kt b/src/main/kotlin/domain/entity/log/CreatedLog.kt new file mode 100644 index 0000000..e0acf71 --- /dev/null +++ b/src/main/kotlin/domain/entity/log/CreatedLog.kt @@ -0,0 +1,15 @@ +package org.example.domain.entity.log + +import java.time.LocalDateTime +import java.util.UUID + +class CreatedLog( + username: String, + affectedId: UUID, + affectedName: String, + affectedType: AffectedType, + dateTime: LocalDateTime = LocalDateTime.now(), +) : Log(username, affectedId, affectedName, affectedType, dateTime) { + override fun toString() = + "user $username ${ActionType.CREATED.name.lowercase()} ${affectedType.name.lowercase()} $affectedName [$affectedId] at $dateTime" +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/log/DeletedLog.kt b/src/main/kotlin/domain/entity/log/DeletedLog.kt new file mode 100644 index 0000000..0e76e36 --- /dev/null +++ b/src/main/kotlin/domain/entity/log/DeletedLog.kt @@ -0,0 +1,16 @@ +package org.example.domain.entity.log + +import java.time.LocalDateTime +import java.util.UUID + +class DeletedLog( + username: String, + affectedId: UUID, + affectedName: String, + affectedType: AffectedType, + dateTime: LocalDateTime = LocalDateTime.now(), + val deletedFrom: String? = null, +) : Log(username, affectedId, affectedName, affectedType, dateTime) { + override fun toString() = + "user $username ${ActionType.DELETED.name.lowercase()} ${affectedType.name.lowercase()} $affectedName [$affectedId] ${if (!deletedFrom.isNullOrBlank()) "from $deletedFrom" else ""} at $dateTime" +} \ No newline at end of file diff --git a/src/main/kotlin/domain/entity/log/Log.kt b/src/main/kotlin/domain/entity/log/Log.kt new file mode 100644 index 0000000..b05e909 --- /dev/null +++ b/src/main/kotlin/domain/entity/log/Log.kt @@ -0,0 +1,26 @@ +package org.example.domain.entity.log + +import java.time.LocalDateTime +import java.util.* + +sealed class Log( + val username: String, + val affectedId: UUID, + val affectedName: String, + val affectedType: AffectedType, + val dateTime: LocalDateTime = LocalDateTime.now() +) { + enum class ActionType { + CHANGED, + ADDED, + DELETED, + CREATED + } + + enum class AffectedType { + PROJECT, + TASK, + MATE, + STATE + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/repository/LogsRepository.kt b/src/main/kotlin/domain/repository/LogsRepository.kt new file mode 100644 index 0000000..36a76f6 --- /dev/null +++ b/src/main/kotlin/domain/repository/LogsRepository.kt @@ -0,0 +1,8 @@ +package org.example.domain.repository + +import org.example.domain.entity.log.Log + +interface LogsRepository { + fun getAllLogs(): List + fun addLog(log: Log) +} \ No newline at end of file diff --git a/src/main/kotlin/domain/repository/ProjectsRepository.kt b/src/main/kotlin/domain/repository/ProjectsRepository.kt new file mode 100644 index 0000000..3a5c840 --- /dev/null +++ b/src/main/kotlin/domain/repository/ProjectsRepository.kt @@ -0,0 +1,12 @@ +package org.example.domain.repository + +import org.example.domain.entity.Project +import java.util.* + +interface ProjectsRepository { + fun getProjectById(projectId: UUID): Project + fun getAllProjects(): List + fun addProject(project: Project) + fun updateProject(updatedProject: Project) + fun deleteProjectById(projectId: UUID) +} \ No newline at end of file diff --git a/src/main/kotlin/domain/repository/TasksRepository.kt b/src/main/kotlin/domain/repository/TasksRepository.kt new file mode 100644 index 0000000..bffe8a1 --- /dev/null +++ b/src/main/kotlin/domain/repository/TasksRepository.kt @@ -0,0 +1,12 @@ +package org.example.domain.repository + +import org.example.domain.entity.Task +import java.util.* + +interface TasksRepository { + fun getTaskById(taskId: UUID): Task + fun getAllTasks(): List + fun addTask(newTask: Task) + fun updateTask(updatedTask: Task) + fun deleteTaskById(taskId: UUID) +} \ No newline at end of file diff --git a/src/main/kotlin/domain/repository/UsersRepository.kt b/src/main/kotlin/domain/repository/UsersRepository.kt new file mode 100644 index 0000000..ead91e9 --- /dev/null +++ b/src/main/kotlin/domain/repository/UsersRepository.kt @@ -0,0 +1,17 @@ +package org.example.domain.repository + +import org.example.domain.entity.User +import java.util.* + +interface UsersRepository { + fun getAllUsers(): List + fun createUser(user: User) + fun getUserByID(userId: UUID): User + fun clearUserData() + fun getCurrentUser(): User + fun storeUserData( + userId: UUID, + username: String, + role: User.UserRole + ) +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/auth/CreateUserUseCase.kt b/src/main/kotlin/domain/usecase/auth/CreateUserUseCase.kt new file mode 100644 index 0000000..36f72cf --- /dev/null +++ b/src/main/kotlin/domain/usecase/auth/CreateUserUseCase.kt @@ -0,0 +1,31 @@ +package org.example.domain.usecase.auth + +import org.example.data.repository.UsersRepositoryImpl.Companion.encryptPassword +import org.example.domain.AccessDeniedException +import org.example.domain.entity.User +import org.example.domain.entity.User.UserRole +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.UsersRepository + +class CreateUserUseCase( + private val usersRepository: UsersRepository, + private val logsRepository: LogsRepository, +) { + operator fun invoke(username: String, password: String, role: UserRole) = + usersRepository.getCurrentUser().let { currentUser -> + if (currentUser.role != UserRole.ADMIN) throw AccessDeniedException("feature") + User(username = username, hashedPassword = encryptPassword(password) , role = role).let { newUser -> + usersRepository.createUser(newUser) + logsRepository.addLog( + CreatedLog( + username = currentUser.username, + affectedId = newUser.id, + affectedName = newUser.username, + affectedType = Log.AffectedType.MATE + ) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/auth/LoginUseCase.kt b/src/main/kotlin/domain/usecase/auth/LoginUseCase.kt new file mode 100644 index 0000000..9a8a2ec --- /dev/null +++ b/src/main/kotlin/domain/usecase/auth/LoginUseCase.kt @@ -0,0 +1,22 @@ +package org.example.domain.usecase.auth + + +import org.example.domain.repository.UsersRepository +import org.example.data.repository.UsersRepositoryImpl +import org.example.domain.UnauthorizedException + +class LoginUseCase(private val usersRepository: UsersRepository) { + operator fun invoke(username: String, password: String) = + usersRepository.getAllUsers() + .find { it.username == username && it.hashedPassword == UsersRepositoryImpl.encryptPassword(password) } + ?.let { user -> + usersRepository.storeUserData( + userId = user.id, + username = user.username, + role = user.role + ) + } ?: throw UnauthorizedException() + + fun getCurrentUserIfLoggedIn() = usersRepository.getCurrentUser() + +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/auth/LogoutUseCase.kt b/src/main/kotlin/domain/usecase/auth/LogoutUseCase.kt new file mode 100644 index 0000000..254f718 --- /dev/null +++ b/src/main/kotlin/domain/usecase/auth/LogoutUseCase.kt @@ -0,0 +1,7 @@ +package org.example.domain.usecase.auth + +import org.example.domain.repository.UsersRepository + +class LogoutUseCase(private val usersRepository: UsersRepository) { + operator fun invoke() = usersRepository.clearUserData() +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/AddMateToProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/AddMateToProjectUseCase.kt new file mode 100644 index 0000000..59103f3 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/AddMateToProjectUseCase.kt @@ -0,0 +1,36 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class AddMateToProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID, mateId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + projectsRepository.getProjectById(projectId).let { project -> + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + usersRepository.getUserByID(mateId).let { mate -> + if (project.matesIds.contains(mate.id)) throw AlreadyExistException("mate") + projectsRepository.updateProject(project.copy(matesIds = project.matesIds + mate.id)) + logsRepository.addLog( + AddedLog( + username = currentUser.username, + affectedId = mateId, + affectedName = mate.username, + affectedType = Log.AffectedType.MATE, + addedTo = "project ${project.name} [$projectId]" + ) + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/AddStateToProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/AddStateToProjectUseCase.kt new file mode 100644 index 0000000..a5cd936 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/AddStateToProjectUseCase.kt @@ -0,0 +1,35 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.entity.State +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class AddStateToProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID, stateName: String) = + usersRepository.getCurrentUser().let { currentUser -> + projectsRepository.getProjectById(projectId).let { project -> + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + if (project.states.any { it.name == stateName }) throw AlreadyExistException("state") + State(name = stateName).let { stateObj -> + projectsRepository.updateProject(project.copy(states = project.states + stateObj)) + logsRepository.addLog(AddedLog( + username = currentUser.username, + affectedId = stateObj.id, + affectedName = stateName, + affectedType = Log.AffectedType.STATE, + addedTo = "project ${project.name} [$projectId]" + )) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/CreateProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/CreateProjectUseCase.kt new file mode 100644 index 0000000..bee90ff --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/CreateProjectUseCase.kt @@ -0,0 +1,33 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.entity.Project +import org.example.domain.entity.User +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository + + +class CreateProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val usersRepository: UsersRepository, + private val logsRepository: LogsRepository, +) { + operator fun invoke(name: String) = + usersRepository.getCurrentUser().let { currentUser -> + if (currentUser.role != User.UserRole.ADMIN) throw AccessDeniedException("feature") + Project(name = name, createdBy = currentUser.id).let { newProject -> + projectsRepository.addProject(newProject) + logsRepository.addLog( + CreatedLog( + username = currentUser.username, + affectedId = newProject.id, + affectedName = name, + affectedType = Log.AffectedType.PROJECT + ) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/DeleteMateFromProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/DeleteMateFromProjectUseCase.kt new file mode 100644 index 0000000..e543965 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/DeleteMateFromProjectUseCase.kt @@ -0,0 +1,34 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class DeleteMateFromProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID, mateId: UUID) { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + val mate = usersRepository.getUserByID(mateId) + if (!project.matesIds.contains(mate.id)) throw ProjectHasNoException("mate") + projectsRepository.updateProject(project.copy(matesIds = project.matesIds - mateId)) + logsRepository.addLog( + DeletedLog( + username = currentUser.username, + affectedId = mateId, + affectedName = mate.username, + affectedType = Log.AffectedType.MATE, + deletedFrom = "project ${project.name} [$projectId]" + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/DeleteProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/DeleteProjectUseCase.kt new file mode 100644 index 0000000..c438642 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/DeleteProjectUseCase.kt @@ -0,0 +1,30 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class DeleteProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID) { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + projectsRepository.deleteProjectById(projectId) + logsRepository.addLog( + DeletedLog( + username = currentUser.username, + affectedId = projectId, + affectedName = project.name, + affectedType = Log.AffectedType.PROJECT, + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/DeleteStateFromProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/DeleteStateFromProjectUseCase.kt new file mode 100644 index 0000000..edeb117 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/DeleteStateFromProjectUseCase.kt @@ -0,0 +1,33 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class DeleteStateFromProjectUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID, stateName: String) { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + val stateToDelete = project.states.find { it.name == stateName } ?: throw ProjectHasNoException("state") + projectsRepository.updateProject(project.copy(states = project.states - stateToDelete)) + logsRepository.addLog( + DeletedLog( + username = currentUser.username, + affectedId = stateToDelete.id, + affectedName = stateName, + affectedType = Log.AffectedType.STATE, + deletedFrom = "project ${project.name} [$projectId]" + ) + ) + } +} diff --git a/src/main/kotlin/domain/usecase/project/EditProjectNameUseCase.kt b/src/main/kotlin/domain/usecase/project/EditProjectNameUseCase.kt new file mode 100644 index 0000000..e185782 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/EditProjectNameUseCase.kt @@ -0,0 +1,34 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.NoChangeException +import org.example.domain.entity.log.ChangedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class EditProjectNameUseCase( + private val projectsRepository: ProjectsRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID, newName: String) { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (project.createdBy != currentUser.id) throw AccessDeniedException("project") + if (project.name == newName.trim()) throw NoChangeException() + projectsRepository.updateProject(project.copy(name = newName)) + logsRepository.addLog( + ChangedLog( + username = currentUser.username, + affectedId = projectId, + affectedName = project.name, + affectedType = Log.AffectedType.PROJECT, + changedFrom = project.name, + changedTo = newName + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/GetAllProjectsUseCase.kt b/src/main/kotlin/domain/usecase/project/GetAllProjectsUseCase.kt new file mode 100644 index 0000000..3fe68aa --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/GetAllProjectsUseCase.kt @@ -0,0 +1,16 @@ +package org.example.domain.usecase.project + +import org.example.domain.NotFoundException +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository + +class GetAllProjectsUseCase( + private val projectsRepository: ProjectsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke() = usersRepository.getCurrentUser().let { currentUser -> + projectsRepository.getAllProjects() + .filter { it.createdBy == currentUser.id } + .ifEmpty { throw NotFoundException("projects") } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCase.kt b/src/main/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCase.kt new file mode 100644 index 0000000..2a9d428 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCase.kt @@ -0,0 +1,29 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.NotFoundException +import org.example.domain.entity.Project +import org.example.domain.entity.Task +import org.example.domain.entity.User +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class GetAllTasksOfProjectUseCase( + private val tasksRepository: TasksRepository, + private val projectsRepository: ProjectsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID): List { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (!isOwnerOrMate(project, currentUser)) throw AccessDeniedException("project") + return tasksRepository.getAllTasks() + .filter { task -> task.projectId == projectId } + .ifEmpty { throw NotFoundException("tasks") } + } + + private fun isOwnerOrMate(project: Project, currentUser: User) = + project.createdBy == currentUser.id || currentUser.id in project.matesIds +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/project/GetProjectHistoryUseCase.kt b/src/main/kotlin/domain/usecase/project/GetProjectHistoryUseCase.kt new file mode 100644 index 0000000..7a62087 --- /dev/null +++ b/src/main/kotlin/domain/usecase/project/GetProjectHistoryUseCase.kt @@ -0,0 +1,32 @@ +package org.example.domain.usecase.project + +import org.example.domain.AccessDeniedException +import org.example.domain.NotFoundException +import org.example.domain.entity.Project +import org.example.domain.entity.User +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class GetProjectHistoryUseCase( + private val logsRepository: LogsRepository, + private val projectsRepository: ProjectsRepository, + private val usersRepository: UsersRepository, +) { + operator fun invoke(projectId: UUID): List { + val currentUser = usersRepository.getCurrentUser() + val project = projectsRepository.getProjectById(projectId) + if (!isOwnerOrMate(project, currentUser)) throw AccessDeniedException("project") + return logsRepository.getAllLogs() + .filter { log -> isProjectRelated(log, projectId) } + .ifEmpty { throw NotFoundException("logs") } + } + + private fun isOwnerOrMate(project: Project, currentUser: User) = + project.createdBy == currentUser.id || currentUser.id in project.matesIds + + private fun isProjectRelated(log: Log, projectId: UUID) = + log.affectedId == projectId || log.toString().contains(projectId.toString()) +} diff --git a/src/main/kotlin/domain/usecase/task/AddMateToTaskUseCase.kt b/src/main/kotlin/domain/usecase/task/AddMateToTaskUseCase.kt new file mode 100644 index 0000000..c0e7dba --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/AddMateToTaskUseCase.kt @@ -0,0 +1,40 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class AddMateToTaskUseCase( + private val tasksRepository: TasksRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID, mateId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException("task") + if (task.assignedTo.contains(mateId)) throw AlreadyExistException("mate") + if (!project.matesIds.contains(mateId)) throw ProjectHasNoException("mate") + tasksRepository.updateTask(task.copy(assignedTo = task.assignedTo + mateId)) + logsRepository.addLog( + AddedLog( + username = currentUser.username, + affectedId = mateId, + affectedName = usersRepository.getUserByID(mateId).username, + affectedType = Log.AffectedType.MATE, + addedTo = "task ${task.title} [$taskId]" + ) + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/task/CreateTaskUseCase.kt b/src/main/kotlin/domain/usecase/task/CreateTaskUseCase.kt new file mode 100644 index 0000000..b777a97 --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/CreateTaskUseCase.kt @@ -0,0 +1,46 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.State +import org.example.domain.entity.Task +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class CreateTaskUseCase( + private val tasksRepository: TasksRepository, + private val usersRepository: UsersRepository, + private val logsRepository: LogsRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(title: String, stateName: String, projectId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + projectsRepository.getProjectById(projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException( + "project" + ) + if (project.states.all { it.name != stateName }) throw ProjectHasNoException("state") + Task( + title = title, + state = State(name = stateName), + projectId = projectId, + createdBy = currentUser.id + ).let { newTask -> + tasksRepository.addTask(newTask) + logsRepository.addLog( + CreatedLog( + username = currentUser.username, + affectedId = newTask.id, + affectedName = newTask.title, + affectedType = Log.AffectedType.TASK, + ) + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/task/DeleteMateFromTaskUseCase.kt b/src/main/kotlin/domain/usecase/task/DeleteMateFromTaskUseCase.kt new file mode 100644 index 0000000..524ec07 --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/DeleteMateFromTaskUseCase.kt @@ -0,0 +1,41 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.TaskHasNoException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class DeleteMateFromTaskUseCase( + private val tasksRepository: TasksRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID, mateId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException("task") + if (!task.assignedTo.contains(mateId)) throw TaskHasNoException("mate") + task.assignedTo.toMutableList().let { mates -> + mates.remove(mateId) + tasksRepository.updateTask(task.copy(assignedTo = mates)) + logsRepository.addLog( + DeletedLog( + username = currentUser.username, + affectedId = mateId, + affectedName = usersRepository.getUserByID(mateId).username, + affectedType = Log.AffectedType.MATE, + deletedFrom = "task ${task.title} [$taskId]" + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/task/DeleteTaskUseCase.kt b/src/main/kotlin/domain/usecase/task/DeleteTaskUseCase.kt new file mode 100644 index 0000000..a186d27 --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/DeleteTaskUseCase.kt @@ -0,0 +1,37 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class DeleteTaskUseCase( + private val tasksRepository: TasksRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException( + "task" + ) + tasksRepository.deleteTaskById(taskId) + logsRepository.addLog( + DeletedLog( + username = currentUser.username, + affectedId = taskId, + affectedName = task.title, + affectedType = Log.AffectedType.TASK, + ) + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/task/EditTaskStateUseCase.kt b/src/main/kotlin/domain/usecase/task/EditTaskStateUseCase.kt new file mode 100644 index 0000000..e3e3e84 --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/EditTaskStateUseCase.kt @@ -0,0 +1,43 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.NoChangeException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.ChangedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class EditTaskStateUseCase( + private val tasksRepository: TasksRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID, stateName: String) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException("task") + if (task.state.name == stateName) throw NoChangeException() + projectsRepository.getProjectById(task.projectId).states.find { it.name == stateName } + ?.let { state -> + tasksRepository.updateTask(task.copy(state = state)) + logsRepository.addLog( + ChangedLog( + username = currentUser.username, + affectedId = task.id, + affectedName = task.title, + affectedType = Log.AffectedType.TASK, + changedFrom = task.state.name, + changedTo = stateName + ) + ) + } ?: throw ProjectHasNoException("state") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/domain/usecase/task/EditTaskTitleUseCase.kt b/src/main/kotlin/domain/usecase/task/EditTaskTitleUseCase.kt new file mode 100644 index 0000000..3d267cd --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/EditTaskTitleUseCase.kt @@ -0,0 +1,39 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.NoChangeException +import org.example.domain.entity.log.ChangedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class EditTaskTitleUseCase( + private val tasksRepository: TasksRepository, + private val logsRepository: LogsRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID, newTitle: String) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id || currentUser.id !in project.matesIds) throw AccessDeniedException("task") + if (task.title == newTitle) throw NoChangeException() + tasksRepository.updateTask(task.copy(title = newTitle)) + logsRepository.addLog( + ChangedLog( + username = currentUser.username, + affectedId = task.id, + affectedName = task.title, + affectedType = Log.AffectedType.TASK, + changedFrom = task.title, + changedTo = newTitle + ) + ) + } + } + } +} diff --git a/src/main/kotlin/domain/usecase/task/GetTaskHistoryUseCase.kt b/src/main/kotlin/domain/usecase/task/GetTaskHistoryUseCase.kt new file mode 100644 index 0000000..acb703d --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/GetTaskHistoryUseCase.kt @@ -0,0 +1,12 @@ +package org.example.domain.usecase.task + +import org.example.domain.NotFoundException +import org.example.domain.repository.LogsRepository +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class GetTaskHistoryUseCase(private val logsRepository: LogsRepository = getKoin().get()) { + operator fun invoke(taskId: UUID) = logsRepository.getAllLogs() + .filter { it.toString().contains(taskId.toString()) } + .ifEmpty { throw NotFoundException("logs") } +} diff --git a/src/main/kotlin/domain/usecase/task/GetTaskUseCase.kt b/src/main/kotlin/domain/usecase/task/GetTaskUseCase.kt new file mode 100644 index 0000000..41bcc48 --- /dev/null +++ b/src/main/kotlin/domain/usecase/task/GetTaskUseCase.kt @@ -0,0 +1,23 @@ +package org.example.domain.usecase.task + +import org.example.domain.AccessDeniedException +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import java.util.* + +class GetTaskUseCase( + private val tasksRepository: TasksRepository, + private val usersRepository: UsersRepository, + private val projectsRepository: ProjectsRepository, +) { + operator fun invoke(taskId: UUID) = + usersRepository.getCurrentUser().let { currentUser -> + tasksRepository.getTaskById(taskId).let { task -> + projectsRepository.getProjectById(task.projectId).let { project -> + if (project.createdBy != currentUser.id && currentUser.id !in project.matesIds) throw AccessDeniedException("task") + task + } + } + } +} diff --git a/src/main/kotlin/presentation/App.kt b/src/main/kotlin/presentation/App.kt new file mode 100644 index 0000000..7816dca --- /dev/null +++ b/src/main/kotlin/presentation/App.kt @@ -0,0 +1,74 @@ +package org.example.presentation + +import org.example.presentation.controller.ExitUiController +import org.example.presentation.controller.SoonUiController +import org.example.presentation.controller.UiController +import org.example.presentation.controller.auth.LoginUiController +import org.example.presentation.controller.auth.LogoutUiController +import org.example.presentation.controller.auth.RegisterUiController +import org.example.presentation.controller.project.* +import org.example.presentation.controller.task.* + +abstract class App(val menuItems: List) { + fun run() { + menuItems.forEachIndexed { index, menuItem -> + println("${index + 1}. ${menuItem.title}") + } + print("\nEnter your selection: ") + val input = readln().toIntOrNull() ?: -1 + val uiController = menuItems.getOrNull(input - 1)?.uiController ?: ExitUiController() + uiController.execute() + if (input == menuItems.size) return + run() + } + + data class MenuItem(val title: String, val uiController: UiController = SoonUiController()) +} + +class AuthApp : App( + menuItems = listOf( + MenuItem("Log In", LoginUiController()), + MenuItem("Exit Application", ExitUiController()) + ) +) + +class AdminApp : App( + menuItems = listOf( + MenuItem("Get My Projects", GetAllProjectsUiController()), + MenuItem("Create New Project", CreateProjectUiController()), + MenuItem("Delete Project", DeleteProjectUiController()), + MenuItem("Edit Project Name", EditProjectNameUiController()), + MenuItem("View Project History", GetProjectHistoryUiController()), + MenuItem("Add Mate to Project", AddMateToProjectUiController()), + MenuItem("Delete Mate From Project", DeleteMateFromProjectUiController()), + MenuItem("Add State to Project", AddStateToProjectUiController()), + MenuItem("Delete State from Project", DeleteStateFromProjectUiController()), + MenuItem("View All Tasks in Project", GetAllTasksOfProjectController()), + MenuItem("Add Mate To Task", AddMateToTaskUIController()), + MenuItem("Create New Task", CreateTaskUiController()), + MenuItem("Delete Mate From Task", DeleteMateFromTaskUiController()), + MenuItem("Delete Task", DeleteTaskUiController()), + MenuItem("Edit Task State", EditTaskStateController()), + MenuItem("Edit Task Title ", EditTaskTitleUiController()), + MenuItem("View Task Change History", GetTaskHistoryUIController()), + MenuItem("View Task Details", GetTaskUiController()), + MenuItem("Create User", RegisterUiController()), + MenuItem("Logout", LogoutUiController()), + ) +) + + +class MateApp : App( + menuItems = listOf( + MenuItem("View Project History", GetProjectHistoryUiController()), + MenuItem("View All Tasks in Project", GetAllTasksOfProjectController()), + MenuItem("Add Mate To Task", AddMateToTaskUIController()), + MenuItem("Create New Task", CreateTaskUiController()), + MenuItem("Delete Task", DeleteTaskUiController()), + MenuItem("Edit Task State", EditTaskStateController()), + MenuItem("View Task History", GetTaskHistoryUIController()), + MenuItem("Edit Task Title ", EditTaskTitleUiController()), + MenuItem("View Task Details", GetTaskUiController()), + MenuItem("Logout", LogoutUiController()), + ) +) diff --git a/src/main/kotlin/presentation/controller/ExitUiController.kt b/src/main/kotlin/presentation/controller/ExitUiController.kt new file mode 100644 index 0000000..ddb047b --- /dev/null +++ b/src/main/kotlin/presentation/controller/ExitUiController.kt @@ -0,0 +1,7 @@ +package org.example.presentation.controller + +class ExitUiController : UiController { + override fun execute() { + println("See you later!!") + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/SoonUiController.kt b/src/main/kotlin/presentation/controller/SoonUiController.kt new file mode 100644 index 0000000..d7cb5bd --- /dev/null +++ b/src/main/kotlin/presentation/controller/SoonUiController.kt @@ -0,0 +1,7 @@ +package org.example.presentation.controller + +class SoonUiController : UiController { + override fun execute() { + println("Coming soon!!") + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/UiController.kt b/src/main/kotlin/presentation/controller/UiController.kt new file mode 100644 index 0000000..21a8808 --- /dev/null +++ b/src/main/kotlin/presentation/controller/UiController.kt @@ -0,0 +1,18 @@ +package org.example.presentation.controller + +import org.example.presentation.utils.viewer.ExceptionViewer +import org.example.presentation.utils.viewer.ItemViewer + +interface UiController { + fun execute() + fun tryAndShowError( + exceptionViewer: ItemViewer = ExceptionViewer(), + bloc: () -> Unit, + ) { + try { + bloc() + } catch (e: Throwable) { + exceptionViewer.view(e) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/auth/LoginUiController.kt b/src/main/kotlin/presentation/controller/auth/LoginUiController.kt new file mode 100644 index 0000000..2f46ce1 --- /dev/null +++ b/src/main/kotlin/presentation/controller/auth/LoginUiController.kt @@ -0,0 +1,46 @@ +package org.example.presentation.controller.auth + +import org.example.common.Constants +import org.example.domain.InvalidInputException +import org.example.domain.entity.User.UserRole +import org.example.domain.usecase.auth.LoginUseCase +import org.example.presentation.App +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.core.qualifier.named +import org.koin.java.KoinJavaComponent.getKoin + +class LoginUiController( + private val loginUseCase: LoginUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), + private val mateApp: App = getKoin().get(named(Constants.APPS.MATE_APP)), + private val adminApp: App = getKoin().get(named(Constants.APPS.ADMIN_APP)), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the username: ") + val username = input.getInput() + print("Please enter the password: ") + val password = input.getInput() + + if (username.isBlank() || password.isBlank()) + throw InvalidInputException("Username and password must not be empty.") + + loginUseCase(username, password) + viewer.view("You have successfully logged in.\n") + + loginUseCase.getCurrentUserIfLoggedIn().role.let { role -> + if (role == UserRole.ADMIN) { + adminApp.run() + } else if (role == UserRole.MATE) { + mateApp.run() + } + } + } + } +} + diff --git a/src/main/kotlin/presentation/controller/auth/LogoutUiController.kt b/src/main/kotlin/presentation/controller/auth/LogoutUiController.kt new file mode 100644 index 0000000..8219bde --- /dev/null +++ b/src/main/kotlin/presentation/controller/auth/LogoutUiController.kt @@ -0,0 +1,19 @@ +package org.example.presentation.controller.auth + +import org.example.domain.usecase.auth.LogoutUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin + +class LogoutUiController( + private val logoutUseCase: LogoutUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), +) : UiController { + override fun execute() { + tryAndShowError { + logoutUseCase() + viewer.view("You have been logged out successfully.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/auth/RegisterUiController.kt b/src/main/kotlin/presentation/controller/auth/RegisterUiController.kt new file mode 100644 index 0000000..53015a2 --- /dev/null +++ b/src/main/kotlin/presentation/controller/auth/RegisterUiController.kt @@ -0,0 +1,39 @@ +package org.example.presentation.controller.auth + +import org.example.domain.InvalidInputException +import org.example.domain.entity.User + +import org.example.domain.usecase.auth.CreateUserUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin + +class RegisterUiController( + private val createUserUseCase: CreateUserUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the username: ") + val username = input.getInput() + print("Please enter the password: ") + val password = input.getInput() + print("Please enter the role (ADMIN or MATE): ") + val role = input.getInput().let { value -> + User.UserRole.entries.firstOrNull { it.name.equals(value, ignoreCase = true) } + ?: throw InvalidInputException("Invalid role: \"$value\". Please enter either ADMIN or MATE.") + } + if (username.isBlank() || password.isBlank()) throw InvalidInputException("Username and password cannot be empty.") + createUserUseCase.invoke( + username = username, + password = password, + role = role + ) + viewer.view("User \"$username\" has been registered successfully.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/AddMateToProjectUiController.kt b/src/main/kotlin/presentation/controller/project/AddMateToProjectUiController.kt new file mode 100644 index 0000000..39d2e06 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/AddMateToProjectUiController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.AddMateToProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class AddMateToProjectUiController( + private val addMateToProjectUseCase: AddMateToProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the mate ID: ") + val mateId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Mate ID cannot be blank. Please provide a valid ID.") + } + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be blank. Please provide a valid ID.") + } + addMateToProjectUseCase(UUID.fromString(projectId), UUID.fromString(mateId)) + viewer.view("Mate with ID [$mateId] was successfully added to project [$projectId].\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/AddStateToProjectUiController.kt b/src/main/kotlin/presentation/controller/project/AddStateToProjectUiController.kt new file mode 100644 index 0000000..2f2f3e9 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/AddStateToProjectUiController.kt @@ -0,0 +1,35 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.AddStateToProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class AddStateToProjectUiController( + private val addStateToProjectUseCase: AddStateToProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be blank. Please provide a valid ID.") + } + print("Please enter the new state to add: ") + val newState = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("State name cannot be blank. Please enter a valid state.") + } + addStateToProjectUseCase( + projectId = UUID.fromString(projectId), + stateName = newState + ) + viewer.view("State \"$newState\" was successfully added to Project [$projectId].\n") + } + } +} diff --git a/src/main/kotlin/presentation/controller/project/CreateProjectUiController.kt b/src/main/kotlin/presentation/controller/project/CreateProjectUiController.kt new file mode 100644 index 0000000..3583f03 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/CreateProjectUiController.kt @@ -0,0 +1,27 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.CreateProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin + +class CreateProjectUiController( + private val createProjectUseCase: CreateProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the name of the new project: ") + val name = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project name cannot be empty. Please provide a valid name.") + } + createProjectUseCase(name = name) + viewer.view("Project \"$name\" has been created successfully.\n") + } + } +} diff --git a/src/main/kotlin/presentation/controller/project/DeleteMateFromProjectUiController.kt b/src/main/kotlin/presentation/controller/project/DeleteMateFromProjectUiController.kt new file mode 100644 index 0000000..2728ec6 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/DeleteMateFromProjectUiController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.DeleteMateFromProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class DeleteMateFromProjectUiController( + private val deleteMateFromProjectUseCase: DeleteMateFromProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be blank. Please provide a valid ID.") + } + print("Please enter Mate ID: ") + val mateId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Mate ID cannot be blank. Please provide a valid ID.") + } + deleteMateFromProjectUseCase(UUID.fromString(projectId), UUID.fromString(mateId)) + viewer.view("Mate [$mateId] has been successfully removed from project [$projectId].\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/DeleteProjectUiController.kt b/src/main/kotlin/presentation/controller/project/DeleteProjectUiController.kt new file mode 100644 index 0000000..f570a96 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/DeleteProjectUiController.kt @@ -0,0 +1,28 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.DeleteProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class DeleteProjectUiController( + private val deleteProjectUseCase: DeleteProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be empty. Please provide a valid ID.") + } + deleteProjectUseCase(UUID.fromString(projectId)) + viewer.view("Project with ID $projectId has been successfully deleted.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/DeleteStateFromProjectUiController.kt b/src/main/kotlin/presentation/controller/project/DeleteStateFromProjectUiController.kt new file mode 100644 index 0000000..0d39709 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/DeleteStateFromProjectUiController.kt @@ -0,0 +1,37 @@ +package org.example.presentation.controller.project + + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.DeleteStateFromProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class DeleteStateFromProjectUiController( + private val deleteStateFromProjectUseCase: DeleteStateFromProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be empty. Please enter a valid ID.") + } + print("Enter the state you want to delete: ") + val stateToDelete = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("State name cannot be empty. Please enter a valid state.") + } + deleteStateFromProjectUseCase( + projectId = UUID.fromString(projectId), + stateName = stateToDelete + ) + viewer.view("State \"$stateToDelete\" has been successfully removed from the project.\n") + } + + } +} diff --git a/src/main/kotlin/presentation/controller/project/EditProjectNameUiController.kt b/src/main/kotlin/presentation/controller/project/EditProjectNameUiController.kt new file mode 100644 index 0000000..ca21418 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/EditProjectNameUiController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.project.EditProjectNameUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class EditProjectNameUiController( + private val editProjectNameUseCase: EditProjectNameUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be empty. Please enter a valid ID.") + } + print("Enter the new project name: ") + val newProjectName = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project name cannot be empty. Please enter a valid name.") + } + editProjectNameUseCase(UUID.fromString(projectId), newProjectName) + viewer.view("Project #$projectId's name has been successfully updated to $newProjectName.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/GetAllProjectsUiController.kt b/src/main/kotlin/presentation/controller/project/GetAllProjectsUiController.kt new file mode 100644 index 0000000..ffb4a82 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/GetAllProjectsUiController.kt @@ -0,0 +1,25 @@ +package org.example.presentation.controller.project + +import org.example.domain.entity.Project +import org.example.domain.usecase.project.GetAllProjectsUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.ItemsViewer +import org.example.presentation.utils.viewer.ProjectsViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin + +class GetAllProjectsUiController( + private val getAllProjectsUseCase: GetAllProjectsUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val projectsViewer: ItemsViewer = ProjectsViewer(), +) : UiController { + override fun execute() { + tryAndShowError { + getAllProjectsUseCase().let { + viewer.view("All projects created by you.\n") + projectsViewer.view(it) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/GetAllTasksOfProjectController.kt b/src/main/kotlin/presentation/controller/project/GetAllTasksOfProjectController.kt new file mode 100644 index 0000000..a8ff60c --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/GetAllTasksOfProjectController.kt @@ -0,0 +1,34 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.entity.Task +import org.example.domain.usecase.project.GetAllTasksOfProjectUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.ItemsViewer +import org.example.presentation.utils.viewer.TasksViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class GetAllTasksOfProjectController( + private val getAllTasksOfProjectUseCase: GetAllTasksOfProjectUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), + private val tasksViewer: ItemsViewer = TasksViewer() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be blank. Please enter a valid ID.") + } + getAllTasksOfProjectUseCase(UUID.fromString(projectId)).also { + viewer.view("Tasks for project ID $projectId:\n") + tasksViewer.view(it) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/project/GetProjectHistoryUiController.kt b/src/main/kotlin/presentation/controller/project/GetProjectHistoryUiController.kt new file mode 100644 index 0000000..2186153 --- /dev/null +++ b/src/main/kotlin/presentation/controller/project/GetProjectHistoryUiController.kt @@ -0,0 +1,34 @@ +package org.example.presentation.controller.project + +import org.example.domain.InvalidInputException +import org.example.domain.entity.log.Log +import org.example.domain.usecase.project.GetProjectHistoryUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.ItemsViewer +import org.example.presentation.utils.viewer.LogsViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class GetProjectHistoryUiController( + private val getProjectHistoryUseCase: GetProjectHistoryUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), + private val logsViewer: ItemsViewer = LogsViewer() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be blank. Please enter a valid ID.") + } + getProjectHistoryUseCase(projectId = UUID.fromString(projectId)).let { logs -> + viewer.view("History for project ID $projectId:\n") + logsViewer.view(logs) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/AddMateToTaskUIController.kt b/src/main/kotlin/presentation/controller/task/AddMateToTaskUIController.kt new file mode 100644 index 0000000..4af44e0 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/AddMateToTaskUIController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.AddMateToTaskUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class AddMateToTaskUIController( + private val addMateToTaskUseCase: AddMateToTaskUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + print("Please enter the Mate ID: ") + val mateId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Mate ID cannot be empty. Please provide a valid ID.") + } + addMateToTaskUseCase(UUID.fromString(taskId), UUID.fromString(mateId)) + viewer.view("Mate [$mateId] was successfully added to Task [$taskId].\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/CreateTaskUiController.kt b/src/main/kotlin/presentation/controller/task/CreateTaskUiController.kt new file mode 100644 index 0000000..9888e50 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/CreateTaskUiController.kt @@ -0,0 +1,40 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.CreateTaskUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class CreateTaskUiController( + private val createTaskUseCase: CreateTaskUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the task title: ") + val taskTitle = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task title cannot be empty. Please provide a valid title.") + } + print("Please enter the task state: ") + val taskState = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task state cannot be empty. Please provide a valid state.") + } + print("Please enter the project ID: ") + val projectId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Project ID cannot be empty. Please provide a valid ID.") + } + createTaskUseCase( + title = taskTitle, + stateName = taskState, + projectId = UUID.fromString(projectId) + ) + viewer.view("Task \"$taskTitle\" has been created successfully under project [$projectId].\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/DeleteMateFromTaskUiController.kt b/src/main/kotlin/presentation/controller/task/DeleteMateFromTaskUiController.kt new file mode 100644 index 0000000..f279e83 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/DeleteMateFromTaskUiController.kt @@ -0,0 +1,35 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.DeleteMateFromTaskUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class DeleteMateFromTaskUiController( + private val deleteMateFromTaskUseCase: DeleteMateFromTaskUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + print("Please enter the Mate ID to remove: ") + val mateId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Mate ID cannot be empty. Please provide a valid ID.") + } + deleteMateFromTaskUseCase( + taskId = UUID.fromString(taskId), + mateId = UUID.fromString(mateId) + ) + viewer.view("Mate [$mateId] has been successfully removed from Task [$taskId].\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/DeleteTaskUiController.kt b/src/main/kotlin/presentation/controller/task/DeleteTaskUiController.kt new file mode 100644 index 0000000..5b57d36 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/DeleteTaskUiController.kt @@ -0,0 +1,28 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.DeleteTaskUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class DeleteTaskUiController( + private val deleteTaskUseCase: DeleteTaskUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader() +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID to delete: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + deleteTaskUseCase(UUID.fromString(taskId)) + viewer.view("Task with ID #$taskId has been successfully deleted.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/EditTaskStateController.kt b/src/main/kotlin/presentation/controller/task/EditTaskStateController.kt new file mode 100644 index 0000000..8f87f27 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/EditTaskStateController.kt @@ -0,0 +1,33 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.EditTaskStateUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class EditTaskStateController( + private val editTaskStateUseCase: EditTaskStateUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + print("Please enter the new state: ") + val newState = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("State cannot be empty. Please provide a valid state.") + } + editTaskStateUseCase(UUID.fromString(taskId), newState) + viewer.view("Task #$taskId state has been successfully updated to \"$newState\".\n") + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/EditTaskTitleUiController.kt b/src/main/kotlin/presentation/controller/task/EditTaskTitleUiController.kt new file mode 100644 index 0000000..da676b5 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/EditTaskTitleUiController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.EditTaskTitleUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class EditTaskTitleUiController( + private val editTaskTitleUseCase: EditTaskTitleUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + print("Please enter the new title: ") + val newTitle = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Title cannot be empty. Please provide a valid title.") + } + editTaskTitleUseCase(UUID.fromString(taskId), newTitle) + viewer.view("Task title has been successfully updated.\n") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/controller/task/GetTaskHistoryUIController.kt b/src/main/kotlin/presentation/controller/task/GetTaskHistoryUIController.kt new file mode 100644 index 0000000..5f62424 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/GetTaskHistoryUIController.kt @@ -0,0 +1,34 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.entity.log.Log +import org.example.domain.usecase.task.GetTaskHistoryUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.ItemsViewer +import org.example.presentation.utils.viewer.LogsViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.java.KoinJavaComponent.getKoin +import java.util.* + +class GetTaskHistoryUIController( + private val getTaskHistoryUseCase: GetTaskHistoryUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), + private val logsViewer: ItemsViewer = LogsViewer(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be empty. Please provide a valid ID.") + } + getTaskHistoryUseCase(UUID.fromString(taskId)).also { logs -> + viewer.view("History for Task #$taskId:\n") + logsViewer.view(logs) + } + } + } +} diff --git a/src/main/kotlin/presentation/controller/task/GetTaskUiController.kt b/src/main/kotlin/presentation/controller/task/GetTaskUiController.kt new file mode 100644 index 0000000..614b027 --- /dev/null +++ b/src/main/kotlin/presentation/controller/task/GetTaskUiController.kt @@ -0,0 +1,32 @@ +package org.example.presentation.controller.task + +import org.example.domain.InvalidInputException +import org.example.domain.usecase.task.GetTaskUseCase +import org.example.presentation.controller.UiController +import org.example.presentation.utils.interactor.InputReader +import org.example.presentation.utils.interactor.StringInputReader +import org.example.presentation.utils.viewer.ItemViewer +import org.example.presentation.utils.viewer.TextViewer +import org.koin.mp.KoinPlatform.getKoin +import java.util.* + +class GetTaskUiController( + private val getTaskUseCase: GetTaskUseCase = getKoin().get(), + private val viewer: ItemViewer = TextViewer(), + private val input: InputReader = StringInputReader(), +) : UiController { + override fun execute() { + tryAndShowError { + print("Please enter the Task ID: ") + val taskId = input.getInput().also { + if (it.isBlank()) throw InvalidInputException("Task ID cannot be blank. Please provide a valid ID.") + } + getTaskUseCase(UUID.fromString(taskId)).also { task -> + viewer.view("Task retrieved successfully: Task ID #$taskId\n") + println(task.toString()) + } + } + } +} + + diff --git a/src/main/kotlin/presentation/utils/interactor/InputReader.kt b/src/main/kotlin/presentation/utils/interactor/InputReader.kt new file mode 100644 index 0000000..0845c4a --- /dev/null +++ b/src/main/kotlin/presentation/utils/interactor/InputReader.kt @@ -0,0 +1,5 @@ +package org.example.presentation.utils.interactor + +interface InputReader { + fun getInput(): T +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/interactor/StringInputReader.kt b/src/main/kotlin/presentation/utils/interactor/StringInputReader.kt new file mode 100644 index 0000000..ee11dd5 --- /dev/null +++ b/src/main/kotlin/presentation/utils/interactor/StringInputReader.kt @@ -0,0 +1,7 @@ +package org.example.presentation.utils.interactor + +class StringInputReader : InputReader { + override fun getInput(): String { + return readln() + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/ExceptionViewer.kt b/src/main/kotlin/presentation/utils/viewer/ExceptionViewer.kt new file mode 100644 index 0000000..7b59059 --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/ExceptionViewer.kt @@ -0,0 +1,7 @@ +package org.example.presentation.utils.viewer + +class ExceptionViewer : ItemViewer { + override fun view(item: Throwable) { + println("\u001B[31m${item.message}\u001B[0m") + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/ItemViewer.kt b/src/main/kotlin/presentation/utils/viewer/ItemViewer.kt new file mode 100644 index 0000000..2468248 --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/ItemViewer.kt @@ -0,0 +1,5 @@ +package org.example.presentation.utils.viewer + +interface ItemViewer { + fun view(item: T) +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/ItemsViewer.kt b/src/main/kotlin/presentation/utils/viewer/ItemsViewer.kt new file mode 100644 index 0000000..c04cf01 --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/ItemsViewer.kt @@ -0,0 +1,5 @@ +package org.example.presentation.utils.viewer + +interface ItemsViewer { + fun view(items: List) +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/LogsViewer.kt b/src/main/kotlin/presentation/utils/viewer/LogsViewer.kt new file mode 100644 index 0000000..aa20e1b --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/LogsViewer.kt @@ -0,0 +1,9 @@ +package org.example.presentation.utils.viewer + +import org.example.domain.entity.log.Log + +class LogsViewer : ItemsViewer { + override fun view(items: List) { + items.forEach { println(it) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/ProjectsViewer.kt b/src/main/kotlin/presentation/utils/viewer/ProjectsViewer.kt new file mode 100644 index 0000000..640afdc --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/ProjectsViewer.kt @@ -0,0 +1,12 @@ +package org.example.presentation.utils.viewer + +import org.example.domain.entity.Project + +class ProjectsViewer : ItemsViewer { + override fun view(items: List) { + items.forEach { project -> + println("$project") + println("------------------------------------------------------") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/TasksViewer.kt b/src/main/kotlin/presentation/utils/viewer/TasksViewer.kt new file mode 100644 index 0000000..403e0de --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/TasksViewer.kt @@ -0,0 +1,12 @@ +package org.example.presentation.utils.viewer + +import org.example.domain.entity.Task + +class TasksViewer : ItemsViewer { + override fun view(items: List) { + items.forEach { task -> + println("$task") + println("------------------------------------------------------") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/presentation/utils/viewer/TextViewer.kt b/src/main/kotlin/presentation/utils/viewer/TextViewer.kt new file mode 100644 index 0000000..ffd37d5 --- /dev/null +++ b/src/main/kotlin/presentation/utils/viewer/TextViewer.kt @@ -0,0 +1,7 @@ +package org.example.presentation.utils.viewer + +class TextViewer : ItemViewer { + override fun view(item: String) { + print("\u001B[33m${item}\u001B[0m") + } +} \ No newline at end of file diff --git a/src/test/kotlin/MainKtTest.kt b/src/test/kotlin/MainKtTest.kt new file mode 100644 index 0000000..98dbab6 --- /dev/null +++ b/src/test/kotlin/MainKtTest.kt @@ -0,0 +1,3 @@ +class MainKtTest { + +} \ No newline at end of file diff --git a/src/test/kotlin/TestUtils.kt b/src/test/kotlin/TestUtils.kt new file mode 100644 index 0000000..d93b891 --- /dev/null +++ b/src/test/kotlin/TestUtils.kt @@ -0,0 +1,233 @@ +import org.example.domain.entity.log.Log +import org.example.domain.entity.Project +import org.example.domain.entity.State +import org.example.domain.entity.Task +import org.example.domain.entity.User +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.ChangedLog +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.DeletedLog +import java.util.UUID + +val dummyProjects = listOf( + Project( + name = "E-Commerce Platform", + states = listOf("Backlog", "In Progress", "Testing", "Completed").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Social Media App", + states = listOf("Idea", "Prototype", "Development", "Live").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Travel Booking System", + states = listOf("Planned", "Building", "QA", "Release").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Food Delivery App", + states = listOf("Todo", "In Progress", "Review", "Delivered").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Online Education Platform", + states = listOf("Draft", "Content Ready", "Published").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Banking Mobile App", + states = listOf("Requirements", "Design", "Development", "Testing", "Deployment").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(7) { UUID.randomUUID() } + ), + Project( + name = "Fitness Tracking App", + states = listOf("Planned", "In Progress", "Completed").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Event Management System", + states = listOf("Initiated", "Planning", "Execution", "Closure").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Online Grocery Store", + states = listOf("Todo", "Picking", "Dispatch", "Delivered").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ), + Project( + name = "Real Estate Listing Site", + states = listOf("Listing", "Viewing", "Negotiation", "Sold").map { State(name = it) }, + createdBy = UUID.randomUUID(), + matesIds = List(3) { UUID.randomUUID() } + ) +) +val dummyProject = dummyProjects[5] +val dummyAdmin = User( + username = "admin1", + hashedPassword = "adminPass123", + role = User.UserRole.ADMIN +) +val dummyMate = User( + username = "mate1", + hashedPassword = "matePass456", + role = User.UserRole.MATE +) +val dummyTasks = listOf( + Task( + title = "Implement user authentication", + state = State(name = "In Progress"), + assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Design database schema", + state = State(name = "Done"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Create API endpoints", + state = State(name = "To Do"), + assignedTo = emptyList(), + createdBy = dummyAdmin.id, + projectId = UUID.randomUUID() + ), + Task( + title = "Fix login bug", + state = State(name = "Done"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Optimize database queries", + state = State(name = "To Do"), + assignedTo = emptyList(), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Deploy to staging", + state = State(name = "In Progress"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Update documentation", + state = State(name = "To Do"), + assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Refactor legacy code", + state = State(name = "In Progress"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ), + Task( + title = "Add error logging", + state = State(name = "Done"), + assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() + ) +) +val dummyLogs = listOf( + CreatedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "Project A", + affectedType = Log.AffectedType.PROJECT + ), + AddedLog( + username = "admin2", + affectedId = UUID.randomUUID(), + affectedName = "Mate X", + affectedType = Log.AffectedType.MATE, + addedTo = "Project A" + ), + ChangedLog( + username = "mate1", + affectedId = UUID.randomUUID(), + affectedName = "Task T-123", + affectedType = Log.AffectedType.TASK, + changedFrom = "ToDo", + changedTo = "In Progress" + ), + DeletedLog( + username = "admin3", + affectedId = UUID.randomUUID(), + affectedName = "State S-001", + affectedType = Log.AffectedType.STATE, + deletedFrom = "Project B" + ), + CreatedLog( + username = "admin2", + affectedId = UUID.randomUUID(), + affectedName = "Task T-555", + affectedType = Log.AffectedType.TASK + ), + AddedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "State S-999", + affectedType = Log.AffectedType.STATE, + addedTo = "Project C" + ), + ChangedLog( + username = "mate2", + affectedId = UUID.randomUUID(), + affectedName = "State S-123", + affectedType = Log.AffectedType.STATE, + changedFrom = "New", + changedTo = "ToDo" + ), + DeletedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "Mate Z", + affectedType = Log.AffectedType.MATE + ), + CreatedLog( + username = "admin4", + affectedId = UUID.randomUUID(), + affectedName = "State S-789", + affectedType = Log.AffectedType.STATE + ), + ChangedLog( + username = "mate3", + affectedId = UUID.randomUUID(), + affectedName = "Task T-999", + affectedType = Log.AffectedType.TASK, + changedFrom = "In Review", + changedTo = "Done" + ) +) + +val dummyTask = Task( + title = "Implement user authentication", + state = State(name = "In Progress"), + assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()), + createdBy = UUID.randomUUID(), + projectId = UUID.randomUUID() +) +val dummyProjectId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000") +val dummyMateId = UUID.fromString("550e8400-e29b-41d4-a716-446655440001") +val dummyState = "done" + + diff --git a/src/test/kotlin/data/datasource/remote/mongo/LogsMongoStorageTest.kt b/src/test/kotlin/data/datasource/remote/mongo/LogsMongoStorageTest.kt new file mode 100644 index 0000000..2252f3f --- /dev/null +++ b/src/test/kotlin/data/datasource/remote/mongo/LogsMongoStorageTest.kt @@ -0,0 +1,263 @@ +package data.datasource.remote.mongo + +import com.google.common.truth.Truth.assertThat +import com.mongodb.client.FindIterable +import com.mongodb.client.MongoCollection +import com.mongodb.client.result.InsertOneResult +import data.datasource.mongo.LogsMongoStorage +import data.datasource.mongo.MongoStorage +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.bson.Document +import org.example.domain.NotFoundException +import org.example.domain.UnknownException +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.ChangedLog +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log.AffectedType +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.LocalDateTime +import java.util.* + +class LogsMongoStorageTest { + + private lateinit var mockCollection: MongoCollection + private lateinit var storage: LogsMongoStorage + private lateinit var mockFindIterable: FindIterable + + @BeforeEach + fun setup() { + mockCollection = mockk(relaxed = true) + mockFindIterable = mockk(relaxed = true) + storage = LogsMongoStorage() + + val field = MongoStorage::class.java.getDeclaredField("collection") + field.isAccessible = true + field.set(storage, mockCollection) + } + + @Test + fun `toDocument should convert AddedLog to Document correctly`() { + // Given + val addedLog = AddedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "T-101", + affectedType = AffectedType.TASK, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0), + addedTo = "projectX" + ) + + // When + val document = storage.toDocument(addedLog) + + // Then + assertThat(document.getString("username")).isEqualTo("testUser") + assertThat(document.getString("affectedId")).isEqualTo(addedLog.affectedId.toString()) + assertThat(document.getString("affectedType")).isEqualTo("TASK") + assertThat(document.getString("dateTime")).isEqualTo("2023-01-01T12:00") + assertThat(document.getString("actionType")).isEqualTo("ADDED") + assertThat(document.getString("addedTo")).isEqualTo("projectX") + } + + @Test + fun `toDocument should convert ChangedLog to Document correctly`() { + // Given + val changedLog = ChangedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "T-101", + affectedType = AffectedType.TASK, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0), + changedFrom = "TODO", + changedTo = "IN_PROGRESS" + ) + + // When + val document = storage.toDocument(changedLog) + + // Then + assertThat(document.getString("actionType")).isEqualTo("CHANGED") + assertThat(document.getString("changedFrom")).isEqualTo("TODO") + assertThat(document.getString("changedTo")).isEqualTo("IN_PROGRESS") + } + + @Test + fun `toDocument should convert CreatedLog to Document correctly`() { + // Given + val createdLog = CreatedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "P-101", + affectedType = AffectedType.PROJECT, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0) + ) + + // When + val document = storage.toDocument(createdLog) + + // Then + assertThat(document.getString("actionType")).isEqualTo("CREATED") + } + + @Test + fun `toDocument should convert DeletedLog to Document correctly`() { + // Given + val deletedLog = DeletedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "M-101", + affectedType = AffectedType.MATE, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0), + deletedFrom = "system" + ) + + // When + val document = storage.toDocument(deletedLog) + + // Then + assertThat(document.getString("actionType")).isEqualTo("DELETED") + assertThat(document.getString("deletedFrom")).isEqualTo("system") + } + + @Test + fun `fromDocument should convert Document to AddedLog correctly`() { + // Given + val document = Document() + .append("username", "testUser") + .append("affectedId", "8722f308-76cb-4a0f-8dfb-c862b28390ed") + .append("affectedName", "P-101") + .append("affectedType", "TASK") + .append("dateTime", "2023-01-01T12:00") + .append("actionType", "ADDED") + .append("addedTo", "projectX") + + // When + val log = storage.fromDocument(document) + + // Then + assertThat(log).isInstanceOf(AddedLog::class.java) + val addedLog = log as AddedLog + assertThat(addedLog.username).isEqualTo("testUser") + assertThat(addedLog.affectedId).isEqualTo(UUID.fromString("8722f308-76cb-4a0f-8dfb-c862b28390ed")) + assertThat(addedLog.affectedType).isEqualTo(AffectedType.TASK) + assertThat(addedLog.dateTime).isEqualTo(LocalDateTime.of(2023, 1, 1, 12, 0)) + assertThat(addedLog.addedTo).isEqualTo("projectX") + } + + @Test + fun `fromDocument should convert Document to ChangedLog correctly`() { + // Given + val document = Document() + .append("username", "testUser") + .append("affectedId", "8722f308-76cb-4a0f-8dfb-c862b28390ed") + .append("affectedName", "T-101") + .append("affectedType", "TASK") + .append("dateTime", "2023-01-01T12:00") + .append("actionType", "CHANGED") + .append("changedFrom", "TODO") + .append("changedTo", "IN_PROGRESS") + + // When + val log = storage.fromDocument(document) + + // Then + assertThat(log).isInstanceOf(ChangedLog::class.java) + val changedLog = log as ChangedLog + assertThat(changedLog.changedFrom).isEqualTo("TODO") + assertThat(changedLog.changedTo).isEqualTo("IN_PROGRESS") + } + + + @Test + fun `getAll should return logs from collection`() { + // Given + val createdLog = CreatedLog( + username = "user1", + affectedId = UUID.randomUUID(), + affectedName = "T-101", + affectedType = AffectedType.TASK, + dateTime = LocalDateTime.parse("2023-01-01T12:00") + ) + + val deletedLog = DeletedLog( + username = "user2", + affectedId = UUID.randomUUID(), + affectedName = "P-101", + affectedType = AffectedType.PROJECT, + dateTime = LocalDateTime.parse("2023-01-02T12:00"), + deletedFrom = "system" + ) + + // Directly mock the getAll method for this specific test + every { mockCollection.find() } returns mockFindIterable + + // Create a spy on the storage object + val storageSpy = spyk(storage) + every { storageSpy.getAll() } returns listOf(createdLog, deletedLog) + + // When + val result = storageSpy.getAll() + + // Then + assertThat(result).hasSize(2) + assertThat(result[0]).isInstanceOf(CreatedLog::class.java) + assertThat(result[1]).isInstanceOf(DeletedLog::class.java) + } + + @Test + fun `getAll should throw NotFoundException when no logs found`() { + // Given + every { mockCollection.find() } returns mockFindIterable + every { mockFindIterable.toList() } returns emptyList() + + // When/Then + assertThrows { storage.getAll() } + } + + @Test + fun `add should insert document into collection`() { + // Given + val log = CreatedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "P-101", + affectedType = AffectedType.PROJECT, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0) + ) + + val mockResult = mockk() + every { mockResult.wasAcknowledged() } returns true + every { mockCollection.insertOne(any()) } returns mockResult + + // When + storage.add(log) + + // Then + verify { mockCollection.insertOne(any()) } + } + + @Test + fun `add should throw UnknownException when insertion not acknowledged`() { + // Given + val log = CreatedLog( + username = "testUser", + affectedId = UUID.randomUUID(), + affectedName = "P-101", + affectedType = AffectedType.PROJECT, + dateTime = LocalDateTime.of(2023, 1, 1, 12, 0) + ) + + val mockResult = mockk() + every { mockResult.wasAcknowledged() } returns false + every { mockCollection.insertOne(any()) } returns mockResult + + // When/Then + assertThrows { storage.add(log) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/data/datasource/remote/mongo/ProjectsMongoStorageTest.kt b/src/test/kotlin/data/datasource/remote/mongo/ProjectsMongoStorageTest.kt new file mode 100644 index 0000000..0f183c6 --- /dev/null +++ b/src/test/kotlin/data/datasource/remote/mongo/ProjectsMongoStorageTest.kt @@ -0,0 +1,225 @@ +package data.datasource.remote.mongo + +import com.mongodb.client.FindIterable +import com.mongodb.client.MongoCollection +import com.mongodb.client.model.Filters +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import io.mockk.* +import org.bson.Document +import org.example.domain.NotFoundException +import org.example.domain.entity.Project +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import com.google.common.truth.Truth.assertThat +import data.datasource.mongo.MongoStorage +import data.datasource.mongo.ProjectsMongoStorage +import dummyProject +import org.example.domain.entity.State +import java.time.LocalDateTime +import java.util.* + +class ProjectsMongoStorageTest { + + private lateinit var mockCollection: MongoCollection + private lateinit var storage: ProjectsMongoStorage + private lateinit var mockFindIterable: FindIterable + + @BeforeEach + fun setup() { + mockCollection = mockk(relaxed = true) + mockFindIterable = mockk(relaxed = true) + storage = ProjectsMongoStorage() + + val field = MongoStorage::class.java.getDeclaredField("collection") + field.isAccessible = true + field.set(storage, mockCollection) + } + + @Test + fun `toDocument should convert Project to Document correctly`() { + // When + val document = storage.toDocument(dummyProject) + + // Then + assertThat(document.getString("_id")).isEqualTo(dummyProject.id.toString()) + assertThat(document.getString("name")).isEqualTo(dummyProject.name) + assertThat(document.getList("states", String::class.java).size) + .isEqualTo(dummyProject.states.size) + assertThat(document.getString("createdBy")).isEqualTo(dummyProject.createdBy.toString()) + assertThat(document.getString("createdAt")).isEqualTo(dummyProject.createdAt.toString()) + + val docMateIds = document.getList("matesIds", String::class.java) + assertThat(docMateIds).hasSize(dummyProject.matesIds.size) + assertThat(docMateIds).contains(dummyProject.matesIds[0].toString()) + assertThat(docMateIds).contains(dummyProject.matesIds[1].toString()) + } + + @Test + fun `fromDocument should convert Document to Project correctly`() { + // Given + val uuid = UUID.randomUUID() + val creatorUuid = UUID.randomUUID() + val mateIds = listOf(UUID.randomUUID(), UUID.randomUUID()) + val states= listOf("Backlog", "In Progress", "Done").map { State(name = it) } + + val document = Document() + .append("_id", uuid.toString()) + .append("name", "Test Project") + .append("states", states.map { it.toString() }) + .append("createdBy", creatorUuid.toString()) + .append("createdAt", "2023-01-01T12:00") + .append("matesIds", mateIds.map { it.toString() }) + + // When + val project = storage.fromDocument(document) + + // Then + assertThat(project.id).isEqualTo(uuid) + assertThat(project.name).isEqualTo("Test Project") + assertThat(project.states.map { it.name }).containsExactly("Backlog", "In Progress", "Done") + assertThat(project.createdBy).isEqualTo(creatorUuid) + assertThat(project.createdAt).isEqualTo(LocalDateTime.of(2023, 1, 1, 12, 0)) + assertThat(project.matesIds).containsExactlyElementsIn(mateIds) + } + + @Test + fun `getById should return project when it exists`() { + // Given + val uuid = UUID.randomUUID() + val creatorUuid = UUID.randomUUID() + val document = Document() + .append("_id", uuid.toString()) + .append("name", "Test Project") + .append("states", listOf("Backlog", "In Progress", "Done")) + .append("createdBy", creatorUuid.toString()) + .append("createdAt", "2023-01-01T12:00") + .append("matesIds", listOf(UUID.randomUUID().toString())) + + // Create the project object that should be returned + val expectedProject = Project( + id = uuid, + name = "Test Project", + states = listOf("Backlog", "In Progress", "Done").map { State(name = it) }, + createdBy = creatorUuid, + createdAt = LocalDateTime.parse("2023-01-01T12:00"), + matesIds = document.getList("matesIds", String::class.java) + .map { UUID.fromString(it) } + ) + + // Use a spy to intercept the call + val spyStorage = spyk(storage) + every { spyStorage.getById(uuid) } returns expectedProject + + // When + val project = spyStorage.getById(uuid) + + // Then + assertThat(project.id).isEqualTo(uuid) + assertThat(project.name).isEqualTo("Test Project") + } + + @Test + fun `getById should throw NotFoundException when project doesn't exist`() { + // Given + val uuid = UUID.randomUUID() + + // Use a spy to intercept the call + val spyStorage = spyk(storage) + every { spyStorage.getById(uuid) } throws NotFoundException() + + // When/Then + assertThrows { spyStorage.getById(uuid) } + } + + @Test + fun `delete should remove project when it exists`() { + // Given + val uuid = UUID.randomUUID() + val project = Project( + id = uuid, + name = "Test Project", + states = listOf("Backlog", "In Progress", "Done").map { State(name = it) }, + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + matesIds = listOf(UUID.randomUUID()) + ) + + val mockResult = mockk() + every { mockResult.deletedCount } returns 1 + every { mockCollection.deleteOne(any()) } returns mockResult + + // When + storage.delete(project) + + // Then + verify { mockCollection.deleteOne(Filters.eq("_id", uuid.toString())) } + } + + @Test + fun `delete should throw NotFoundException when project doesn't exist`() { + // Given + val uuid = UUID.randomUUID() + val project = Project( + id = uuid, + name = "Test Project", + states = listOf("Backlog", "In Progress", "Done").map { State(name = it) }, + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + matesIds = listOf(UUID.randomUUID()) + ) + + val mockResult = mockk() + every { mockResult.deletedCount } returns 0 + every { mockCollection.deleteOne(any()) } returns mockResult + + // When/Then + assertThrows { storage.delete(project) } + } + + @Test + fun `update should modify project when it exists`() { + // Given + val uuid = UUID.randomUUID() + val project = Project( + id = uuid, + name = "Updated Project", + states = listOf("New", "Completed").map { State(name = it) }, + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + matesIds = listOf(UUID.randomUUID()) + ) + + val mockResult = mockk() + every { mockResult.matchedCount } returns 1 + every { mockCollection.replaceOne(any(), any()) } returns mockResult + + // When + storage.update(project) + + // Then + verify { mockCollection.replaceOne(Filters.eq("_id", uuid.toString()), any()) } + } + + @Test + fun `update should throw NotFoundException when project doesn't exist`() { + // Given + val uuid = UUID.randomUUID() + val project = Project( + id = uuid, + name = "Updated Project", + states = listOf("New", "Completed").map { State(name = it) }, + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + matesIds = listOf(UUID.randomUUID()) + ) + + val mockResult = mockk() + every { mockResult.matchedCount } returns 0 + every { mockCollection.replaceOne(any(), any()) } returns mockResult + + // When/Then + assertThrows { storage.update(project) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/data/datasource/remote/mongo/TasksMongoStorageTest.kt b/src/test/kotlin/data/datasource/remote/mongo/TasksMongoStorageTest.kt new file mode 100644 index 0000000..dd77e24 --- /dev/null +++ b/src/test/kotlin/data/datasource/remote/mongo/TasksMongoStorageTest.kt @@ -0,0 +1,222 @@ +package data.datasource.remote.mongo + + +import com.mongodb.client.FindIterable +import com.mongodb.client.MongoCollection +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.InsertOneResult +import com.mongodb.client.result.UpdateResult +import io.mockk.* +import org.bson.Document +import org.example.domain.entity.Task +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import com.google.common.truth.Truth.assertThat +import data.datasource.mongo.MongoStorage +import data.datasource.mongo.TasksMongoStorage +import org.example.domain.entity.State +import java.time.LocalDateTime +import java.util.* + +class TasksMongoStorageTest { + + private lateinit var mockCollection: MongoCollection + private lateinit var storage: TasksMongoStorage + private lateinit var mockFindIterable: FindIterable + + @BeforeEach + fun setup() { + mockCollection = mockk(relaxed = true) + mockFindIterable = mockk(relaxed = true) + storage = TasksMongoStorage() + + val field = MongoStorage::class.java.getDeclaredField("collection") + field.isAccessible = true + field.set(storage, mockCollection) + } + + @Test + fun `toDocument should convert Task to Document correctly`() { + // Given + val uuid = UUID.randomUUID() + val creatorUuid = UUID.randomUUID() + val projectId = UUID.randomUUID() + val assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()) + val state = State(name = "In Progress") + + val task = Task( + id = uuid, + title = "Implement Feature X", + state = state, + assignedTo = assignedTo, + createdBy = creatorUuid, + createdAt = LocalDateTime.of(2023, 1, 1, 12, 0), + projectId = projectId + ) + + // When + val document = storage.toDocument(task) + + // Then + assertThat(document.getString("_id")).isEqualTo(uuid.toString()) + // Then (continued) + assertThat(document.getString("title")).isEqualTo("Implement Feature X") + assertThat(document.getString("state")).isEqualTo(state.toString()) + assertThat(document.get("createdBy")).isEqualTo(creatorUuid) + assertThat(document.getString("createdAt")).isEqualTo("2023-01-01T12:00") + assertThat(document.get("projectId")).isEqualTo(projectId) + + val docAssignedTo = document.getList("assignedTo", String::class.java) + assertThat(docAssignedTo).hasSize(2) + assertThat(docAssignedTo).contains(assignedTo[0].toString()) + assertThat(docAssignedTo).contains(assignedTo[1].toString()) + } + + @Test + fun `fromDocument should convert Document to Task correctly`() { + // Given + val uuid = UUID.randomUUID() + val creatorUuid = UUID.randomUUID() + val projectId = UUID.randomUUID() + val assignedTo = listOf(UUID.randomUUID(), UUID.randomUUID()) + val state = State(name = "In Progress") + + + val document = Document() + .append("_id", uuid.toString()) + .append("title", "Implement Feature X") + .append("state", state.toString()) + .append("assignedTo", assignedTo.map { it.toString() }) + .append("createdBy", creatorUuid) + .append("createdAt", "2023-01-01T12:00") + .append("projectId", projectId) + + // When + val task = storage.fromDocument(document) + + // Then + assertThat(task.id).isEqualTo(uuid) + assertThat(task.title).isEqualTo("Implement Feature X") + assertThat(task.state.name).isEqualTo("In Progress") + assertThat(task.createdBy).isEqualTo(creatorUuid) + assertThat(task.createdAt).isEqualTo(LocalDateTime.of(2023, 1, 1, 12, 0)) + assertThat(task.projectId).isEqualTo(projectId) + assertThat(task.assignedTo).containsExactlyElementsIn(assignedTo) + } + + @Test + fun `getAll should return tasks from collection`() { + // Given - create the expected Task objects that would result from document conversion + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + + val task1 = Task( + id = uuid1, + title = "Task 1", + state = State(name = "Backlog"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + projectId = UUID.randomUUID() + ) + + val task2 = Task( + id = uuid2, + title = "Task 2", + state = State(name = "In Progress"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + projectId = UUID.randomUUID() + ) + + // Create a spy on the storage object + val storageSpy = spyk(storage) + + // Mock the getAll method directly to return our test tasks + every { storageSpy.getAll() } returns listOf(task1, task2) + + // When + val result = storageSpy.getAll() + + // Then + assertThat(result).hasSize(2) + assertThat(result[0].title).isEqualTo("Task 1") + assertThat(result[1].title).isEqualTo("Task 2") + } + + @Test + fun `add should insert task into collection`() { + // Given + val uuid = UUID.randomUUID() + val task = Task( + id = uuid, + title = "New Task", + state = State(name = "Backlog"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + projectId = UUID.randomUUID() + ) + + val mockResult = mockk() + every { mockResult.wasAcknowledged() } returns true + every { mockCollection.insertOne(any()) } returns mockResult + + // When + storage.add(task) + + // Then + verify { mockCollection.insertOne(any()) } + } + + @Test + fun `update should modify task when it exists`() { + // Given + val uuid = UUID.randomUUID() + val task = Task( + id = uuid, + title = "Updated Task", + state = State(name = "Done"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + projectId = UUID.randomUUID() + ) + + val mockResult = mockk() + every { mockResult.matchedCount } returns 1 + every { mockCollection.replaceOne(any(), any()) } returns mockResult + + // When + storage.update(task) + + // Then + verify { mockCollection.replaceOne(any(), any()) } + } + + @Test + fun `delete should remove task when it exists`() { + // Given + val uuid = UUID.randomUUID() + val task = Task( + id = uuid, + title = "Task to Delete", + state = State(name = "Cancelled"), + assignedTo = listOf(UUID.randomUUID()), + createdBy = UUID.randomUUID(), + createdAt = LocalDateTime.now(), + projectId = UUID.randomUUID() + ) + + val mockResult = mockk() + every { mockResult.deletedCount } returns 1 + every { mockCollection.deleteOne(any()) } returns mockResult + + // When + storage.delete(task) + + // Then + verify { mockCollection.deleteOne(any()) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/data/datasource/remote/mongo/UsersMongoStorageTest.kt b/src/test/kotlin/data/datasource/remote/mongo/UsersMongoStorageTest.kt new file mode 100644 index 0000000..9b7dd45 --- /dev/null +++ b/src/test/kotlin/data/datasource/remote/mongo/UsersMongoStorageTest.kt @@ -0,0 +1,229 @@ +package data.datasource.remote.mongo + +import com.mongodb.client.FindIterable +import com.mongodb.client.MongoCollection +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.InsertOneResult +import com.mongodb.client.result.UpdateResult +import io.mockk.* +import org.bson.Document +import org.example.domain.NotFoundException +import org.example.domain.entity.User +import org.example.domain.entity.User.UserRole +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import com.google.common.truth.Truth.assertThat +import data.datasource.mongo.MongoStorage +import data.datasource.mongo.UsersMongoStorage +import java.time.LocalDateTime +import java.util.* + +class UsersMongoStorageTest { + + private lateinit var mockCollection: MongoCollection + private lateinit var storage: UsersMongoStorage + private lateinit var mockFindIterable: FindIterable + + @BeforeEach + fun setup() { + mockCollection = mockk(relaxed = true) + mockFindIterable = mockk(relaxed = true) + storage = UsersMongoStorage() + + val field = MongoStorage::class.java.getDeclaredField("collection") + field.isAccessible = true + field.set(storage, mockCollection) + } + + @Test + fun `toDocument should convert User to Document correctly`() { + // Given + val uuid = UUID.randomUUID() + val user = User( + id = uuid, + username = "johnsmith", + hashedPassword = "hashedPass123", + role = UserRole.ADMIN, + cratedAt = LocalDateTime.of(2023, 1, 1, 12, 0) + ) + + // When + val document = storage.toDocument(user) + + // Then + assertThat(document.getString("_id")).isEqualTo(uuid.toString()) + assertThat(document.getString("uuid")).isEqualTo(uuid.toString()) + assertThat(document.getString("username")).isEqualTo("johnsmith") + assertThat(document.getString("hashedPassword")).isEqualTo("hashedPass123") + assertThat(document.getString("role")).isEqualTo("ADMIN") + assertThat(document.getString("createdAt")).isEqualTo("2023-01-01T12:00") + } + + @Test + fun `fromDocument should convert Document to User correctly`() { + // Given + val uuid = UUID.randomUUID() + val document = Document() + .append("_id", uuid.toString()) + .append("username", "johnsmith") + .append("hashedPassword", "hashedPass123") + .append("role", "MATE") + .append("createdAt", "2023-01-01T12:00") + + // When + val user = storage.fromDocument(document) + + // Then + assertThat(user.id).isEqualTo(uuid) + assertThat(user.username).isEqualTo("johnsmith") + assertThat(user.hashedPassword).isEqualTo("hashedPass123") + assertThat(user.role).isEqualTo(UserRole.MATE) + assertThat(user.cratedAt).isEqualTo(LocalDateTime.of(2023, 1, 1, 12, 0)) + } + + @Test + fun `getAll should return users from collection`() { + // Given + val uuid1 = UUID.randomUUID() + val uuid2 = UUID.randomUUID() + val createdAt1 = LocalDateTime.now() + val createdAt2 = LocalDateTime.now() + + // Create user objects directly instead of relying on MongoDB conversion + val user1 = User( + id = uuid1, + username = "user1", + hashedPassword = "hash1", + role = UserRole.ADMIN, + cratedAt = createdAt1 + ) + + val user2 = User( + id = uuid2, + username = "user2", + hashedPassword = "hash2", + role = UserRole.MATE, + cratedAt = createdAt2 + ) + + // Use a spy to bypass MongoDB interaction + val storageSpy = spyk(storage) + every { storageSpy.getAll() } returns listOf(user1, user2) + + // When + val result = storageSpy.getAll() + + // Then + assertThat(result).hasSize(2) + assertThat(result[0].username).isEqualTo("user1") + assertThat(result[0].role).isEqualTo(UserRole.ADMIN) + assertThat(result[1].username).isEqualTo("user2") + assertThat(result[1].role).isEqualTo(UserRole.MATE) + } + + @Test + fun `getById should return user when it exists`() { + // Given + val uuid = UUID.randomUUID() + val user = User( + id = uuid, + username = "johnsmith", + hashedPassword = "hash123", + role = UserRole.ADMIN, + cratedAt = LocalDateTime.now() + ) + + // Use a spy to bypass MongoDB interaction + val storageSpy = spyk(storage) + every { storageSpy.getById(uuid) } returns user + + // When + val result = storageSpy.getById(uuid) + + // Then + assertThat(result.id).isEqualTo(uuid) + assertThat(result.username).isEqualTo("johnsmith") + } + + @Test + fun `getById should throw NotFoundException when user doesn't exist`() { + // Given + val uuid = UUID.randomUUID() + + // Use a spy to bypass MongoDB interaction + val storageSpy = spyk(storage) + every { storageSpy.getById(uuid) } throws NotFoundException() + + // When/Then + assertThrows { storageSpy.getById(uuid) } + } + + @Test + fun `add should insert user into collection`() { + // Given + val user = User( + id = UUID.randomUUID(), + username = "newuser", + hashedPassword = "newhash", + role = UserRole.MATE, + cratedAt = LocalDateTime.now() + ) + + val mockResult = mockk() + every { mockResult.wasAcknowledged() } returns true + every { mockCollection.insertOne(any()) } returns mockResult + + // When + storage.add(user) + + // Then + verify { mockCollection.insertOne(any()) } + } + + @Test + fun `update should modify user when it exists`() { + // Given + val uuid = UUID.randomUUID() + val user = User( + id = uuid, + username = "updateduser", + hashedPassword = "updatedhash", + role = UserRole.ADMIN, + cratedAt = LocalDateTime.now() + ) + + val mockResult = mockk() + every { mockResult.matchedCount } returns 1 + every { mockCollection.replaceOne(any(), any()) } returns mockResult + + // When + storage.update(user) + + // Then + verify { mockCollection.replaceOne(any(), any()) } + } + + @Test + fun `delete should remove user when it exists`() { + // Given + val uuid = UUID.randomUUID() + val user = User( + id = uuid, + username = "deleteuser", + hashedPassword = "deletehash", + role = UserRole.MATE, + cratedAt = LocalDateTime.now() + ) + + val mockResult = mockk() + every { mockResult.deletedCount } returns 1 + every { mockCollection.deleteOne(any()) } returns mockResult + + // When + storage.delete(user) + + // Then + verify { mockCollection.deleteOne(any()) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/data/repository/LogsRepositoryImplTest.kt b/src/test/kotlin/data/repository/LogsRepositoryImplTest.kt new file mode 100644 index 0000000..b7bc051 --- /dev/null +++ b/src/test/kotlin/data/repository/LogsRepositoryImplTest.kt @@ -0,0 +1,60 @@ +package data.repository + +import com.google.common.truth.Truth.assertThat +import data.datasource.DataSource +import dummyLogs +import io.mockk.* +import org.example.data.repository.LogsRepositoryImpl +import org.example.domain.PlanMateAppException +import org.example.domain.entity.log.Log +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + + +class LogsRepositoryImplTest { + private lateinit var logsRepository: LogsRepositoryImpl + private val logsDataSource: DataSource = mockk(relaxed = true) + + @BeforeEach + fun setup() { + logsRepository = LogsRepositoryImpl(logsDataSource) + } + + @Test + fun `should return all logs when logs are existed`() { + //given + every { logsDataSource.getAll() } returns dummyLogs + //when + val result = logsRepository.getAllLogs() + //then + assertThat(result.size).isEqualTo(dummyLogs.size) + verify { logsDataSource.getAll() } + } + + @Test + fun `should add logs when pass a valid log`() { + //given + every { logsDataSource.add(dummyLogs[2]) } just Runs + //when + logsRepository.addLog(dummyLogs[2]) + //then + verify { logsDataSource.add(match { it == dummyLogs[2] }) } + } + + @Test + fun `should throw PlanMateAppException when data source throw any exception while retrieval`() { + //given + every { logsDataSource.getAll() } throws Exception() + //when && then + assertThrows { logsRepository.getAllLogs() } + } + + @Test + fun `should throw PlanMateAppException when data source throw any exception while adding`() { + //given + every { logsDataSource.add(dummyLogs[2]) } throws Exception() + //when && then + assertThrows { logsRepository.addLog(dummyLogs[2]) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/auth/CreateUserUseCaseTest.kt b/src/test/kotlin/domain/usecase/auth/CreateUserUseCaseTest.kt new file mode 100644 index 0000000..2083e51 --- /dev/null +++ b/src/test/kotlin/domain/usecase/auth/CreateUserUseCaseTest.kt @@ -0,0 +1,83 @@ +package domain.usecase.auth + +import dummyAdmin +import dummyMate +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.entity.User +import org.example.domain.entity.User.UserRole +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.auth.CreateUserUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test + +class CreateUserUseCaseTest { + + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + + lateinit var createUserUseCase: CreateUserUseCase + + @BeforeEach + fun setUp() { + createUserUseCase = CreateUserUseCase(usersRepository, logsRepository) + + } + // red then green + + @Test + fun `should throw AccessDeniedException when user is not admin`() { + // given + val user = User( + username = " Ah med ", + hashedPassword = "123456789", + role = UserRole.MATE + ) + every { usersRepository.getCurrentUser() } returns dummyMate + // when & then + assertThrows { + createUserUseCase.invoke(user.username, user.hashedPassword, user.role) + } + } + + + @Test + fun `should create new mate when user complete register with valid username and password`() { + // given + val user = User( + username = "federico valverdie", + hashedPassword = "123456789", + role = UserRole.MATE + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + // when + createUserUseCase.invoke(user.username, user.hashedPassword, user.role) + + //then + verify { usersRepository.createUser(any()) } + verify { logsRepository.addLog(any()) } + } + + @Test + fun `should create new admin when user complete register with valid username and password`() { + // given + val user = User( + username = "my uncle luka modric", + hashedPassword = "123456789", + role = UserRole.ADMIN + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + // when + createUserUseCase.invoke(user.username, user.hashedPassword, user.role) + + //then + verify { usersRepository.createUser(any()) } + verify { logsRepository.addLog(any()) } + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/auth/LoginUseCaseTest.kt b/src/test/kotlin/domain/usecase/auth/LoginUseCaseTest.kt new file mode 100644 index 0000000..8bbdf8a --- /dev/null +++ b/src/test/kotlin/domain/usecase/auth/LoginUseCaseTest.kt @@ -0,0 +1,163 @@ +package domain.usecase.auth + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.data.repository.UsersRepositoryImpl.Companion.encryptPassword +import org.example.domain.UnauthorizedException +import org.example.domain.entity.User +import org.example.domain.entity.User.UserRole +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.auth.LoginUseCase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test + + +class LoginUseCaseTest { + + private val usersRepository: UsersRepository = mockk(relaxed = true) + lateinit var loginUseCase: LoginUseCase + + @BeforeEach + fun setUp() { + loginUseCase = LoginUseCase(usersRepository) + } + + + @Test + fun `should throw any Exception when getAllUsers throw exception`() { + // given + every { usersRepository.getAllUsers() } throws Exception() + + // when & then + assertThrows { + loginUseCase.invoke(username = "Ahmed", password = "12345678") + } + } + + + @Test + fun `invoke should throw UnauthorizedException when list of users is empty`() { + // given + every { usersRepository.getAllUsers() } returns emptyList() + + // when & then + assertThrows { + loginUseCase.invoke(username = "Ahmed", password = "12345678") + } + } + + + + @Test + fun `invoke should throw UnauthorizedException when username not correct`() { + // given + every { usersRepository.getAllUsers() } returns listOf( + User( + username = "harry kane", + hashedPassword = encryptPassword("12345678"), + role = UserRole.MATE, + ) + ) + + // when & then + assertThrows { + loginUseCase.invoke(username = "Ahmed", password = "12345678") + } + } + + @Test + fun `invoke should throw UnauthorizedException when password not correct`() { + // given + every { usersRepository.getAllUsers() } returns listOf( + User( + username = "Ahmed", + hashedPassword = encryptPassword("134328"), + role = UserRole.MATE, + ) + ) + + // when & then + assertThrows { + loginUseCase.invoke(username = "Ahmed", password = "12345678") + } + } + + + @Test + fun `invoke should logged in when user found `() { + // given + every { usersRepository.getAllUsers() } returns listOf( + User( + username = "Ahmed", + hashedPassword = encryptPassword("12345678"), + role = UserRole.MATE, + ) + ) + + // when + loginUseCase.invoke(username = "Ahmed", password = "12345678") + + //then + verify { usersRepository.storeUserData(any(), any(), any()) } + + } + + + @Test + fun `invoke should store user data for authorization `() { + // given + every { usersRepository.getAllUsers() } returns listOf( + User( + username = "Ahmed", + hashedPassword = encryptPassword("12345678"), + role = UserRole.MATE, + ) + ) + + loginUseCase.invoke(username = "Ahmed", password = "12345678") + + verify { usersRepository.storeUserData(any(), any(), any()) } + + } + + @Test + fun `getCurrentUserIfLoggedIn should get current user when he already logged in`() { + // given + every { usersRepository.getAllUsers() } returns listOf( + User( + username = "Ahmed", + hashedPassword = encryptPassword("12345678"), + role = UserRole.MATE, + ) + ) + + loginUseCase.getCurrentUserIfLoggedIn() + + verify { usersRepository.getCurrentUser() } + + } + + + @Test + fun `getCurrentUserIfLoggedIn should return user when user already logged in `() { + // given + val user = User( + username = "Ahmed", + hashedPassword = encryptPassword("12345678"), + role = UserRole.ADMIN, + ) + every { usersRepository.getCurrentUser() } returns user + + //when + val result = loginUseCase.getCurrentUserIfLoggedIn() + + //then + assertEquals(user, result) + + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/auth/LogoutUseCaseTest.kt b/src/test/kotlin/domain/usecase/auth/LogoutUseCaseTest.kt new file mode 100644 index 0000000..3ebf0d0 --- /dev/null +++ b/src/test/kotlin/domain/usecase/auth/LogoutUseCaseTest.kt @@ -0,0 +1,36 @@ +package domain.usecase.auth + + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.auth.LogoutUseCase +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + + +class LogoutUseCaseTest { + + private val usersRepository: UsersRepository = mockk(relaxed = true) + val logoutUseCase = LogoutUseCase(usersRepository) + + + @Test + fun `should throw any exception data when clearUserDate throw any exception`() { + // when + every { usersRepository.clearUserData() } throws Exception() + //then + assertThrows { + logoutUseCase.invoke() + } + } + + @Test + fun `should clear user data when user logged out`() { + // when + logoutUseCase.invoke() + //then + verify { usersRepository.clearUserData() } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/AddMateToProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/AddMateToProjectUseCaseTest.kt new file mode 100644 index 0000000..fabe0d8 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/AddMateToProjectUseCaseTest.kt @@ -0,0 +1,84 @@ +package domain.usecase.project + +import dummyAdmin +import dummyMate +import dummyMateId +import dummyProject +import dummyProjectId +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.entity.Project +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.AddMateToProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +class AddMateToProjectUseCaseTest { + private lateinit var projectsRepository: ProjectsRepository + private lateinit var logsRepository: LogsRepository + private lateinit var usersRepository: UsersRepository + private lateinit var addMateToProjectUseCase: AddMateToProjectUseCase + + + @BeforeEach + fun setup() { + projectsRepository = mockk(relaxed = true) + logsRepository = mockk(relaxed = true) + usersRepository = mockk(relaxed = true) + addMateToProjectUseCase = AddMateToProjectUseCase(projectsRepository, logsRepository, usersRepository) + } + + + @Test + fun `should throw AccessDeniedException when who creates project is not current user`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject + // when & then + assertThrows { + addMateToProjectUseCase.invoke(projectId = dummyProjectId, mateId = dummyMateId) + } + } + + @Test + fun `should throw AlreadyExistException when mate already found in project`() { + // given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy( + id = dummyProjectId, + createdBy = dummyAdmin.id, + matesIds = listOf(dummyMateId) + ) + every { usersRepository.getUserByID(any()) } returns dummyMate.copy(id = dummyMateId) + // when & then + assertThrows { + addMateToProjectUseCase.invoke(projectId = dummyProjectId, mateId = dummyMateId) + } + } + + @Test + fun `should complete addition of mate to project `() { + // given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy( + id = dummyProjectId, + createdBy = dummyAdmin.id, + matesIds = listOf() + ) + every { usersRepository.getUserByID(any()) } returns dummyMate.copy(id = dummyMateId) + // when + addMateToProjectUseCase.invoke(projectId = dummyProjectId, mateId = dummyMateId) + // then + verify { projectsRepository.updateProject(any()) } + verify { logsRepository.addLog(any()) } + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/AddStateToProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/AddStateToProjectUseCaseTest.kt new file mode 100644 index 0000000..cb5fc84 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/AddStateToProjectUseCaseTest.kt @@ -0,0 +1,90 @@ +package domain.usecase.project + +import dummyAdmin +import dummyMate +import dummyMateId +import dummyProject +import dummyProjectId +import dummyState +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.entity.State +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.AddStateToProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +class AddStateToProjectUseCaseTest { + + private lateinit var projectsRepository: ProjectsRepository + private lateinit var logsRepository: LogsRepository + private lateinit var usersRepository: UsersRepository + private lateinit var addStateToProjectUseCase: AddStateToProjectUseCase + + private val projectId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000") + private val state = "done.." + + @BeforeEach + fun setup() { + projectsRepository = mockk(relaxed = true) + logsRepository = mockk(relaxed = true) + usersRepository = mockk(relaxed = true) + addStateToProjectUseCase = + AddStateToProjectUseCase(projectsRepository, logsRepository, usersRepository) + + } + + + @Test + fun `should throw AccessDeniedException when who creates project is not current user`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject + // when & then + assertThrows { + addStateToProjectUseCase.invoke(projectId = dummyProjectId, stateName = dummyState) + } + } + + @Test + fun `should throw AlreadyExistException when mate already found in project`() { + // given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy( + id = dummyProjectId, + createdBy = dummyAdmin.id, + states = listOf(State(name = dummyState)), + ) + // when & then + assertThrows { + addStateToProjectUseCase.invoke(projectId = dummyProjectId, stateName = dummyState) + } + } + + @Test + fun `should complete addition of state in project`() { + // given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy( + id = dummyProjectId, + createdBy = dummyAdmin.id, + states = listOf(), + ) + // when + addStateToProjectUseCase(projectId = projectId, state) + // then + verify { projectsRepository.updateProject(any()) } + verify { logsRepository.addLog(any()) } + } + +} + + + diff --git a/src/test/kotlin/domain/usecase/project/CreateProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/CreateProjectUseCaseTest.kt new file mode 100644 index 0000000..8b29304 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/CreateProjectUseCaseTest.kt @@ -0,0 +1,84 @@ +package domain.usecase.project + +import dummyAdmin +import dummyMate +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.entity.log.CreatedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.CreateProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + + +class CreateProjectUseCaseTest { + lateinit var createProjectUseCase: CreateProjectUseCase + private val projectRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + + @BeforeEach + fun setUp() { + createProjectUseCase = CreateProjectUseCase(projectRepository, usersRepository, logsRepository) + } + + @Test + fun `should create project and log when admin creates a project`() { + //given + val newProjectName = "new project name" + every { usersRepository.getCurrentUser() } returns dummyAdmin + //when + createProjectUseCase(newProjectName) + //then + verify { projectRepository.addProject(match { it.name == newProjectName && it.createdBy == dummyAdmin.id }) } + verify { logsRepository.addLog(match { it is CreatedLog }) } + } + + @Test + fun `should throw AccessDeniedException when mate tries to create project`() { + //given + val newProjectName = "new project name" + every { usersRepository.getCurrentUser() } returns dummyMate + //when && then + assertThrows { createProjectUseCase(newProjectName) } + verify(exactly = 0) { projectRepository.addProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + val newProjectName = "new project name" + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { createProjectUseCase(newProjectName) } + verify(exactly = 0) { projectRepository.addProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addProject fails`() { + //given + val newProjectName = "new project name" + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectRepository.addProject(any()) } throws Exception() + //when && then + assertThrows { createProjectUseCase(newProjectName) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val newProjectName = "new project name" + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { createProjectUseCase(newProjectName) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/DeleteMateFromProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/DeleteMateFromProjectUseCaseTest.kt new file mode 100644 index 0000000..bbe2844 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/DeleteMateFromProjectUseCaseTest.kt @@ -0,0 +1,156 @@ +package domain.usecase.project + +import dummyAdmin +import dummyMate +import dummyProject +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.DeleteMateFromProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.example.domain.AccessDeniedException + +class DeleteMateFromProjectUseCaseTest { + private lateinit var deleteMateFromProjectUseCase: DeleteMateFromProjectUseCase + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + deleteMateFromProjectUseCase = DeleteMateFromProjectUseCase(projectsRepository, logsRepository, usersRepository) + } + + @Test + fun `should remove mate and log when user is project creator and mate exists`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id, createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } returns dummyMate + //when + deleteMateFromProjectUseCase(project.id, dummyMate.id) + //then + verify { projectsRepository.updateProject(match { !it.matesIds.contains(dummyMate.id) }) } + verify { logsRepository.addLog(match { it is DeletedLog }) } + } + + @Test + fun `should throw AccessDeniedException when user is not project creator`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { deleteMateFromProjectUseCase(project.id, dummyMate.id) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when mate is not in project`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } returns dummyMate + //when && then + assertThrows { deleteMateFromProjectUseCase(project.id, dummyMate.id) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when project has no mates`() { + //given + val project = dummyProject.copy(matesIds = emptyList(), createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } returns dummyMate + //when && then + assertThrows { deleteMateFromProjectUseCase(project.id, dummyMate.id) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { + deleteMateFromProjectUseCase(dummyProject.id, dummyMate.id) + } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } throws Exception() + //when && then + assertThrows { + deleteMateFromProjectUseCase(dummyProject.id, dummyMate.id) + } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getUserByID fails`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id, createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(any()) } throws Exception() + //when && then + assertThrows { + deleteMateFromProjectUseCase(dummyProject.id, dummyMate.id) + } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when updateProject fails`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id, createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } returns dummyMate + every { projectsRepository.updateProject(any()) } throws Exception() + //when && then + assertThrows { + deleteMateFromProjectUseCase(dummyProject.id, dummyMate.id) + } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id, createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } returns dummyMate + every { projectsRepository.updateProject(project.copy(matesIds = project.matesIds - dummyMate.id)) } + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { + deleteMateFromProjectUseCase(dummyProject.id, dummyMate.id) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/DeleteProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/DeleteProjectUseCaseTest.kt new file mode 100644 index 0000000..dd03b93 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/DeleteProjectUseCaseTest.kt @@ -0,0 +1,102 @@ +package domain.usecase.project + +import dummyAdmin +import dummyProject +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.DeleteProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DeleteProjectUseCaseTest { + private lateinit var deleteProjectUseCase: DeleteProjectUseCase + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + deleteProjectUseCase = DeleteProjectUseCase( + projectsRepository, + logsRepository, + usersRepository + ) + } + + @Test + fun `should delete project and log when user is creator`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns project + //when + deleteProjectUseCase(dummyProject.id) + //then + verify { projectsRepository.deleteProjectById(match { it == dummyProject.id }) } + verify { logsRepository.addLog(match { it is DeletedLog }) } + } + + @Test + fun `should throw AccessDeniedException when user is not project creator`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { deleteProjectUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.deleteProjectById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { deleteProjectUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { projectsRepository.deleteProjectById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } throws Exception() + //when && then + assertThrows { deleteProjectUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.deleteProjectById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when deleteProjectById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.deleteProjectById(any()) } throws Exception() + //when && then + assertThrows { deleteProjectUseCase(dummyProject.id) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.deleteProjectById(project.id) } + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { deleteProjectUseCase(dummyProject.id) } + } +} diff --git a/src/test/kotlin/domain/usecase/project/DeleteStateFromProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/DeleteStateFromProjectUseCaseTest.kt new file mode 100644 index 0000000..b3adf17 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/DeleteStateFromProjectUseCaseTest.kt @@ -0,0 +1,134 @@ +package domain.usecase.project + +import dummyAdmin +import dummyProject +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.DeleteStateFromProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DeleteStateFromProjectUseCaseTest { + private lateinit var deleteStateFromProjectUseCase: DeleteStateFromProjectUseCase + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setUp() { + deleteStateFromProjectUseCase = + DeleteStateFromProjectUseCase(projectsRepository, logsRepository, usersRepository) + } + + @Test + fun `should delete state when user is creator and state exists`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val state = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when + deleteStateFromProjectUseCase.invoke(project.id, state.name) + //then + verify { projectsRepository.updateProject(match { !it.states.contains(state) }) } + verify { logsRepository.addLog(match { it is DeletedLog }) } + } + + @Test + fun `should throw AccessDeniedException when user is not project creator`() { + //given + val state = dummyProject.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(dummyProject.id, state.name) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when state not found in project`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(project.id, "state") } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when project has no states`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, states = emptyList()) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(project.id, "state") } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val state = project.states.random() + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { + deleteStateFromProjectUseCase.invoke(project.id, state.name) + } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val state = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } throws Exception() + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(project.id, state.name) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when updateProject fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val state = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.updateProject(any()) } throws Exception() + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(project.id, state.name) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val state = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.updateProject(project.copy(states = project.states - state)) } + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { deleteStateFromProjectUseCase.invoke(project.id, state.name) } + } +} diff --git a/src/test/kotlin/domain/usecase/project/EditProjectNameUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/EditProjectNameUseCaseTest.kt new file mode 100644 index 0000000..e89f6b6 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/EditProjectNameUseCaseTest.kt @@ -0,0 +1,131 @@ +package domain.usecase.project + +import dummyAdmin +import dummyProject +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.NoChangeException +import org.example.domain.entity.log.ChangedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.EditProjectNameUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class EditProjectNameUseCaseTest { + private lateinit var editProjectNameUseCase: EditProjectNameUseCase + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + editProjectNameUseCase = EditProjectNameUseCase(projectsRepository, logsRepository, usersRepository) + } + + @Test + fun `should edit project name and log when user is creator and project exists`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when + editProjectNameUseCase(project.id, "new name") + //then + verify { projectsRepository.updateProject(match { it.name == "new name" }) } + verify { logsRepository.addLog(match { it is ChangedLog }) } + } + + @Test + fun `should throw AccessDeniedException when user is not the creator`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { editProjectNameUseCase(dummyProject.id, "new name") } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw NoChangeException when new name is exact same old name`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { editProjectNameUseCase(project.id, project.name) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw NoChangeException when new name is same old name but has extra spaces`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { editProjectNameUseCase(project.id, " ${project.name} ") } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { + editProjectNameUseCase(dummyProject.id, "new name") + } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } throws Exception() + //when && then + assertThrows { + editProjectNameUseCase(dummyProject.id, "new name") + } + verify(exactly = 0) { projectsRepository.updateProject(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when updateProject fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.updateProject(any()) } throws Exception() + //when && then + assertThrows { + editProjectNameUseCase(project.id, "new name") + } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { projectsRepository.updateProject(project.copy(name = "new name")) } + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { + editProjectNameUseCase(project.id, "new name") + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/GetAllProjectsUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/GetAllProjectsUseCaseTest.kt new file mode 100644 index 0000000..9fce628 --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/GetAllProjectsUseCaseTest.kt @@ -0,0 +1,78 @@ +package domain.usecase.project + +import com.google.common.truth.Truth.assertThat +import dummyAdmin +import dummyProjects +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.NotFoundException +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.GetAllProjectsUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class GetAllProjectsUseCaseTest { + private lateinit var getAllProjectsUseCase: GetAllProjectsUseCase + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + getAllProjectsUseCase = GetAllProjectsUseCase(projectsRepository, usersRepository) + } + + @Test + fun `should return projects created by current user when user logged in`() { + //given + val projects = dummyProjects + listOf( + dummyProjects.random().copy(createdBy = dummyAdmin.id), + dummyProjects.random().copy(createdBy = dummyAdmin.id), + dummyProjects.random().copy(createdBy = dummyAdmin.id), + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getAllProjects() } returns projects.shuffled() + //when + val filteredProjects = getAllProjectsUseCase() + //then + assertThat(filteredProjects.all { it.createdBy == dummyAdmin.id }).isTrue() + } + + @Test + fun `should throw NotFoundException when user has no projects`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getAllProjects() } returns dummyProjects + //when && then + assertThrows { getAllProjectsUseCase() } + } + + @Test + fun `should throw NotFoundException when all projects list is empty`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getAllProjects() } returns emptyList() + //when && then + assertThrows { getAllProjectsUseCase() } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { getAllProjectsUseCase() } + verify(exactly = 0) { projectsRepository.getAllProjects() } + } + + @Test + fun `should not proceed when getAllProjects fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getAllProjects() } throws Exception() + //when && then + assertThrows { getAllProjectsUseCase() } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCaseTest.kt new file mode 100644 index 0000000..a001dcc --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/GetAllTasksOfProjectUseCaseTest.kt @@ -0,0 +1,131 @@ +package domain.usecase.project + +import com.google.common.truth.Truth.assertThat +import dummyAdmin +import dummyMate +import dummyProject +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.NotFoundException +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.GetAllTasksOfProjectUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class GetAllTasksOfProjectUseCaseTest { + + private lateinit var getAllTasksOfProjectUseCase: GetAllTasksOfProjectUseCase + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + getAllTasksOfProjectUseCase = GetAllTasksOfProjectUseCase(tasksRepository, projectsRepository, usersRepository) + } + + @Test + fun `should return tasks when project creator retrieves tasks`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val tasks = dummyTasks + listOf( + dummyTasks.random().copy(projectId = project.id), + dummyTasks.random().copy(projectId = project.id), + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.getAllTasks() } returns tasks + //when + val filteredTasks = getAllTasksOfProjectUseCase(project.id) + //then + assertThat(filteredTasks.all { it.projectId == project.id }).isTrue() + } + + @Test + fun `should return tasks when project mate retrieves tasks`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id) + val tasks = dummyTasks + listOf( + dummyTasks.random().copy(projectId = project.id), + dummyTasks.random().copy(projectId = project.id), + ) + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.getAllTasks() } returns tasks + //when + val filteredTasks = getAllTasksOfProjectUseCase(project.id) + //then + assertThat(filteredTasks.all { it.projectId == project.id }).isTrue() + } + + @Test + fun `should throw AccessDeniedException when user is not related to its project`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { getAllTasksOfProjectUseCase(dummyProject.id) } + verify(exactly = 0) { tasksRepository.getAllTasks() } + } + + @Test + fun `should throw NotFoundException when project has no tasks`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.getAllTasks() } returns dummyTasks + //when && then + assertThrows { getAllTasksOfProjectUseCase(project.id) } + } + + @Test + fun `should throw NotFoundException when all tasks list is empty`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.getAllTasks() } returns emptyList() + //when && then + assertThrows { getAllTasksOfProjectUseCase(project.id) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { getAllTasksOfProjectUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.getAllTasks() } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } throws Exception() + //when && then + assertThrows { getAllTasksOfProjectUseCase(project.id) } + verify(exactly = 0) { tasksRepository.getAllTasks() } + + } + + @Test + fun `should not proceed when getAllTasks fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.getAllTasks() } throws Exception() + //when && then + assertThrows { getAllTasksOfProjectUseCase(project.id) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/project/GetProjectHistoryUseCaseTest.kt b/src/test/kotlin/domain/usecase/project/GetProjectHistoryUseCaseTest.kt new file mode 100644 index 0000000..6ec71ce --- /dev/null +++ b/src/test/kotlin/domain/usecase/project/GetProjectHistoryUseCaseTest.kt @@ -0,0 +1,172 @@ +package domain.usecase.project + +import com.google.common.truth.Truth.assertThat +import dummyAdmin +import dummyLogs +import dummyMate +import dummyProject +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.NotFoundException +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.project.GetProjectHistoryUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +class GetProjectHistoryUseCaseTest { + + private lateinit var getProjectHistoryUseCase: GetProjectHistoryUseCase + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setUp() { + getProjectHistoryUseCase = GetProjectHistoryUseCase(logsRepository, projectsRepository, usersRepository) + } + + @Test + fun `should retrieve all logs of project when project creator retrieves logs`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val projectLogs = listOf( + CreatedLog( + username = "admin1", + affectedId = project.id, + affectedName = "P-101", + affectedType = Log.AffectedType.PROJECT + ), AddedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "P-102", + affectedType = Log.AffectedType.STATE, + addedTo = "project-${project.id}" + ) + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.getAllLogs() } returns dummyLogs + projectLogs + //when + val filteredLogs = getProjectHistoryUseCase(project.id) + //then + assertThat(filteredLogs.all { + it.affectedId == project.id || it.toString().contains(project.id.toString()) + }).isTrue() + } + + @Test + fun `should retrieve all logs of project when project mate retrieves logs`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id) + val projectLogs = listOf( + CreatedLog( + username = "admin1", + affectedId = project.id, + affectedName = "P-101", + affectedType = Log.AffectedType.PROJECT + ), AddedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "P-102", + affectedType = Log.AffectedType.STATE, + addedTo = "project-${project.id}" + ) + ) + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.getAllLogs() } returns dummyLogs + projectLogs + //when + val filteredLogs = getProjectHistoryUseCase(project.id) + //then + assertThat(filteredLogs.all { + it.affectedId == project.id || it.toString().contains(project.id.toString()) + }).isTrue() + } + + @Test + fun `should throw AccessDeniedException when user is not project creator or mate`() { + //given + val projectLogs = listOf( + CreatedLog( + username = "admin1", + affectedId = dummyProject.id, + affectedName = "P-101", + affectedType = Log.AffectedType.PROJECT + ), AddedLog( + username = "admin1", + affectedId = UUID.randomUUID(), + affectedName = "P-102", + affectedType = Log.AffectedType.STATE, + addedTo = "project-${dummyProject.id}" + ) + ) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + every { logsRepository.getAllLogs() } returns dummyLogs + projectLogs + //when && then + assertThrows { getProjectHistoryUseCase(dummyProject.id) } + } + + @Test + fun `should throw NotFoundException when filtered logs list is empty`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.getAllLogs() } returns dummyLogs + //when && when + assertThrows { getProjectHistoryUseCase(project.id) } + } + + @Test + fun `should throw NotFoundException when all logs list is empty`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.getAllLogs() } returns emptyList() + //when && when + assertThrows { getProjectHistoryUseCase(project.id) } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { getProjectHistoryUseCase(dummyAdmin.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { logsRepository.getAllLogs() } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } throws Exception() + //when && then + assertThrows { getProjectHistoryUseCase(project.id) } + verify(exactly = 0) { logsRepository.getAllLogs() } + } + + @Test + fun `should not proceed when getAllLogs fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.getAllLogs() } throws Exception() + //when && then + assertThrows { getProjectHistoryUseCase(project.id) } + } +} diff --git a/src/test/kotlin/domain/usecase/task/AddMateToTaskUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/AddMateToTaskUseCaseTest.kt new file mode 100644 index 0000000..820b374 --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/AddMateToTaskUseCaseTest.kt @@ -0,0 +1,221 @@ +package domain.usecase.task + +import dummyAdmin +import dummyMate +import dummyProject +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.AlreadyExistException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.AddedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.AddMateToTaskUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class AddMateToTaskUseCaseTest { + + private lateinit var addMateToTaskUseCase: AddMateToTaskUseCase + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + addMateToTaskUseCase = AddMateToTaskUseCase( + tasksRepository, + logsRepository, + usersRepository, + projectsRepository, + ) + } + + @Test + fun `should add mate to task when project creator add mate to task`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when + addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) + //then + verify { tasksRepository.updateTask(match { dummyMate.id in it.assignedTo }) } + verify { logsRepository.addLog(match { it is AddedLog }) } + } + + @Test + fun `should add mate to task when project mate add another mate to task`() { + //given + val anotherMate = dummyProject.matesIds.random() + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyMate + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when + addMateToTaskUseCase(taskId = task.id, mateId = anotherMate) + //then + verify { tasksRepository.updateTask(match { anotherMate in it.assignedTo }) } + verify { logsRepository.addLog(match { it is AddedLog }) } + } + + @Test + fun `should throw AccessDeniedException when non-project-related admin add mate to task`() { + //given + val project = dummyProject.copy(matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw AccessDeniedException when non-project-related mate add mate to task`() { + //given + val task = dummyTasks.random().copy(projectId = dummyProject.id) + every { usersRepository.getCurrentUser() } returns dummyMate + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { + addMateToTaskUseCase( + taskId = task.id, + mateId = dummyProject.matesIds.random() + ) + } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + + @Test + fun `should throw AlreadyExistException when user add already assigned mate`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = dummyProject.id, assignedTo = listOf(dummyMate.id)) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when user add non-project-related mate`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTasks.random().copy(projectId = dummyProject.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { tasksRepository.getTaskById(any()) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + } + + @Test + fun `should not proceed when getTaskById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + } + + @Test + fun `should not proceed when getProjectById fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + } + + @Test + fun `should not proceed when updateTask fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + every { tasksRepository.updateTask(any()) } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + verify(exactly = 0) { logsRepository.addLog(any()) } + verify(exactly = 0) { usersRepository.getUserByID(any()) } + } + + @Test + fun `should not proceed when addLog fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + } + + @Test + fun `should not proceed when getUserByID fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id, matesIds = dummyProject.matesIds + dummyMate.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + every { usersRepository.getUserByID(dummyMate.id) } throws Exception() + //when && then + assertThrows { addMateToTaskUseCase(taskId = task.id, mateId = dummyMate.id) } + } + + +} diff --git a/src/test/kotlin/domain/usecase/task/CreateTaskUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/CreateTaskUseCaseTest.kt new file mode 100644 index 0000000..bda6799 --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/CreateTaskUseCaseTest.kt @@ -0,0 +1,104 @@ +package domain.usecase.task + +import dummyAdmin +import dummyMate +import dummyProject +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.ProjectHasNoException +import org.example.domain.entity.log.CreatedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.CreateTaskUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +class CreateTaskUseCaseTest { + private lateinit var createTaskUseCase: CreateTaskUseCase + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + + @BeforeEach + fun setup() { + + createTaskUseCase = CreateTaskUseCase( + tasksRepository = tasksRepository, + logsRepository = logsRepository, + usersRepository = usersRepository, + projectsRepository = projectsRepository, + ) + } + + + @Test + fun `should create task when project creator create one`() { + //given + val title = "new title" + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val projectState = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + // when + createTaskUseCase(title = title, stateName = projectState.name, projectId = project.id) + // then + verify { tasksRepository.addTask(match { it.title == title && it.state.name == projectState.name }) } + verify { logsRepository.addLog(match { it is CreatedLog }) } + } + + @Test + fun `should create task when project mate create one`() { + //given + val title = "new title" + val project = dummyProject.copy(matesIds = listOf(dummyMate.id)) + val projectState = project.states.random() + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(project.id) } returns project + // when + createTaskUseCase(title = title, stateName = projectState.name, projectId = project.id) + // then + verify { tasksRepository.addTask(match { it.title == title && it.state.name == projectState.name }) } + verify { logsRepository.addLog(match { it is CreatedLog }) } + } + + @Test + fun `should throw AccessDeniedException when non-project-related user create one`() { + //given + val title = "new title" + val projectState = dummyProject.states.random() + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + // when && then + assertThrows { + createTaskUseCase( + title = title, + stateName = projectState.name, + projectId = dummyProject.id + ) + } + verify(exactly = 0) { tasksRepository.addTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw ProjectHasNoException when user create one with non-project-related state`() { + //given + val title = "new title" + val project = dummyProject.copy(createdBy = dummyAdmin.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(project.id) } returns project + // when && when + assertThrows {createTaskUseCase(title = title, stateName = "non-project-related", projectId = project.id)} + verify(exactly = 0) { tasksRepository.addTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/task/DeleteMateFromTaskUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/DeleteMateFromTaskUseCaseTest.kt new file mode 100644 index 0000000..3a224fc --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/DeleteMateFromTaskUseCaseTest.kt @@ -0,0 +1,150 @@ +package domain.usecase.task + +import dummyAdmin +import dummyMate +import dummyProject +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.entity.log.DeletedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.DeleteMateFromTaskUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + +class DeleteMateFromTaskUseCaseTest { + + + lateinit var deleteMateFromTaskUseCase: DeleteMateFromTaskUseCase + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + + + @BeforeEach + fun setUp() { + deleteMateFromTaskUseCase = + DeleteMateFromTaskUseCase(tasksRepository, logsRepository, usersRepository, projectsRepository) + } + @Test + fun `should delete mate when given task id and mate id`() { + //Given + val project= dummyProject.copy(createdBy = dummyAdmin.id) + val task= dummyTask.copy(createdBy = dummyAdmin.id, projectId = project.id) + every { usersRepository.getCurrentUser() }returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns task + every { projectsRepository.getProjectById(project.id) }returns project + // When + deleteMateFromTaskUseCase(task.id,task.assignedTo[0]) + //Then + verify { tasksRepository.updateTask( + match { ! + (it.assignedTo.contains(task.assignedTo[0])) + }) + } + verify { logsRepository.addLog(match { it is DeletedLog }) } + } + @Test + fun `should throw AccessDeniedException project not created by current user`() { + //Given + val project= dummyProject + val task= dummyTask.copy(createdBy = dummyAdmin.id, projectId = project.id) + every { usersRepository.getCurrentUser() }returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns task + every { projectsRepository.getProjectById(project.id) }returns project + // When&then + assertThrows { + deleteMateFromTaskUseCase(task.id,task.assignedTo[0]) + } + } + + @Test + fun `should throw TaskHasNoException when current user not assigned to task given task id & mate id`() { + //Given + val project= dummyProject.copy(createdBy = dummyAdmin.id) + val task= dummyTask.copy(createdBy = dummyAdmin.id, projectId = project.id) + every { usersRepository.getCurrentUser() }returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns task + every { projectsRepository.getProjectById(project.id) }returns project + // When&then + assertThrows { + deleteMateFromTaskUseCase(task.id,UUID.randomUUID()) + } + } + + + @Test + fun `should not complete execution when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { deleteMateFromTaskUseCase(dummyTask.id,dummyMate.id) } + verify(exactly = 0) { tasksRepository.getTaskById(any()) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify (exactly=0) { tasksRepository.updateTask(any()) } + verify (exactly=0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when getTaskById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(any()) } throws Exception() + //when && then + assertThrows { deleteMateFromTaskUseCase(dummyTask.id,dummyAdmin.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + @Test + fun `should not complete execution when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns dummyTask + every { projectsRepository.getProjectById(dummyTask.projectId) } throws Exception() + //when && then + assertThrows { deleteMateFromTaskUseCase(dummyTask.id,dummyAdmin.id) } + } + + @Test + fun `should throw Exception when tasksRepository updateTask throw Exception given task id`() { + //Given + val project= dummyProject.copy(createdBy = dummyAdmin.id) + val task= dummyTask.copy(createdBy = dummyAdmin.id, projectId = project.id) + every { usersRepository.getCurrentUser() }returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns task + every { projectsRepository.getProjectById(project.id) }returns project + every { tasksRepository.updateTask(any()) } throws Exception() + + // When & Then + assertThrows { + deleteMateFromTaskUseCase(task.id,task.assignedTo[0]) + } + verify(exactly = 0) { logsRepository.addLog(match { it is DeletedLog }) } + + } + + @Test + fun `should throw Exception when addLog fails `() { + //Given + val project= dummyProject.copy(createdBy = dummyAdmin.id) + val task= dummyTask.copy(createdBy = dummyAdmin.id, projectId = project.id) + every { usersRepository.getCurrentUser() }returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns task + every { projectsRepository.getProjectById(project.id) }returns project + every { tasksRepository.updateTask(any()) } returns Unit + every { logsRepository.addLog(any()) } throws Exception() + // When & Then + assertThrows { + deleteMateFromTaskUseCase(task.id,task.assignedTo[0]) } + + } +} +private val dummyTask = dummyTasks[0] \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/task/DeleteTaskUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/DeleteTaskUseCaseTest.kt new file mode 100644 index 0000000..28c0217 --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/DeleteTaskUseCaseTest.kt @@ -0,0 +1,137 @@ +package domain.usecase.task + +import dummyAdmin +import dummyProject +import dummyTasks +import io.mockk.* +import org.example.domain.AccessDeniedException +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.DeleteTaskUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class DeleteTaskUseCaseTest { + private lateinit var deleteTaskUseCase: DeleteTaskUseCase + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + val dummyTask = dummyTasks.random() + + @BeforeEach + fun setUp() { + deleteTaskUseCase = DeleteTaskUseCase( + tasksRepository, + logsRepository, + usersRepository, + projectsRepository + ) + } + + @Test + fun `should delete task and log given user is creator`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + //when + deleteTaskUseCase(task.id) + //then + verify { tasksRepository.deleteTaskById(match { it == task.id }) } + verify { logsRepository.addLog(match { it is DeletedLog}) } + } + + @Test + fun `should throw AccessDeniedException when user is not the task creator`() { + //given + val task = dummyTasks.random().copy(projectId = dummyProject.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(dummyProject.id) } returns dummyProject + //when && then + assertThrows { deleteTaskUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.deleteProjectById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { deleteTaskUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.deleteTaskById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { projectsRepository.getProjectById(any()) } throws Exception() + //when && then + assertThrows { deleteTaskUseCase(dummyProject.id) } + verify(exactly = 0) { projectsRepository.deleteProjectById(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when deleteTask fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + every { tasksRepository.deleteTaskById(task.id) } throws Exception() + //when && then + assertThrows { deleteTaskUseCase(task.id) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when addLog fails`() { + //given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTasks.random().copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + every { logsRepository.addLog(any()) } throws Exception() + //when && then + assertThrows { deleteTaskUseCase(task.id) } + } + + @Test + fun `should not log if task deletion fails`() { + // Given + every { tasksRepository.deleteTaskById(dummyTask.id) } throws Exception() + + // Then& When + assertThrows { + tasksRepository.deleteTaskById( + dummyTask.id + ) + } + verify(exactly = 0) { + logsRepository.addLog( + match { + it is DeletedLog && + it.affectedId == dummyTask.id && + it.affectedType == Log.AffectedType.TASK + + + }) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/task/EditTaskStateUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/EditTaskStateUseCaseTest.kt new file mode 100644 index 0000000..d027e5e --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/EditTaskStateUseCaseTest.kt @@ -0,0 +1,173 @@ +package domain.usecase.task + +import dummyAdmin +import dummyProject +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.entity.State +import org.example.domain.entity.log.ChangedLog +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.EditTaskStateUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class EditTaskStateUseCaseTest { + private lateinit var editTaskStateUseCase: EditTaskStateUseCase + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + + + @BeforeEach + fun setup() { + editTaskStateUseCase = EditTaskStateUseCase( + tasksRepository, + logsRepository, + usersRepository, + projectsRepository + ) + } + + @Test + fun `should edit task state when task exists`() { + // Given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTask.copy(projectId = project.id, state = State(name = "test-state")) + val newState = project.states.random().name + + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + // When + editTaskStateUseCase(task.id, newState) + // Then + verify { + tasksRepository.updateTask(match { + it.state.name == newState && it.id == dummyTask.id + }) + } + verify { + logsRepository.addLog( + match + { + it is ChangedLog + }) + } + } + + @Test + fun `should throw AccessDeniedException when project is not created by current user given task id & new state`() { + // Given + val project = dummyProject + val task = dummyTask.copy(projectId = project.id, state = State(name = "test-state")) + val newState = project.states.random().name + + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + // When + assertThrows { + editTaskStateUseCase(task.id, newState) + } + } + + @Test + fun `should not proceed when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { + editTaskStateUseCase(dummyProject.id, "new name") + } + verify(exactly = 0) { tasksRepository.getTaskById(any()) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when getTaskById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(any()) } throws Exception() + //when && then + assertThrows { + editTaskStateUseCase(dummyProject.id, "new name") + } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should not complete execution when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(dummyTask.id) } returns dummyTask + every { projectsRepository.getProjectById(dummyTask.projectId) } throws Exception() + //when && then + assertThrows { + editTaskStateUseCase(dummyTask.id, "new name") + } + verify(exactly = 0) { tasksRepository.updateTask(any()) } + verify(exactly = 0) { logsRepository.addLog(any()) } + } + + @Test + fun `should throw an Exception and not log when getTaskById fails `() { + // Given + + every { tasksRepository.getTaskById(dummyTask.id) } throws Exception() + + // when&Then + assertThrows { + editTaskStateUseCase(dummyTask.id, "In Progress") + } + verify(exactly = 0) { + tasksRepository.updateTask(match { + it.id == dummyTask.id + }) + } + verify(exactly = 0) { + logsRepository.addLog( + match + { + it is ChangedLog + }) + } + } + + @Test + fun `should throw an Exception and not log when updateTask fails `() { + // Given + val project = dummyProject.copy(createdBy = dummyAdmin.id) + val task = dummyTask.copy(projectId = project.id, state = State(name = "test-state")) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns project + every { tasksRepository.updateTask(any()) } throws Exception() + // when&Then + assertThrows { + editTaskStateUseCase(task.id, project.states.random().name) + } + + verify(exactly = 0) { + logsRepository.addLog( + match + { + it is ChangedLog + }) + } + } + + private val dummyTask = dummyTasks[0] +} + diff --git a/src/test/kotlin/domain/usecase/task/EditTaskTitleUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/EditTaskTitleUseCaseTest.kt new file mode 100644 index 0000000..bdd30c4 --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/EditTaskTitleUseCaseTest.kt @@ -0,0 +1,94 @@ +package domain.usecase.task + +import dummyMate +import dummyMateId +import dummyProject +import dummyProjectId +import dummyTask +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.AccessDeniedException +import org.example.domain.NoChangeException +import org.example.domain.entity.State +import org.example.domain.entity.Task +import org.example.domain.repository.LogsRepository +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.EditTaskTitleUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* + + +class EditTaskTitleUseCaseTest { + + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val logsRepository: LogsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + lateinit var editTaskTitleUseCase: EditTaskTitleUseCase + + @BeforeEach + fun setUp() { + editTaskTitleUseCase = + EditTaskTitleUseCase(tasksRepository, logsRepository, usersRepository, projectsRepository) + } + + @Test + fun `should throw AccessDeniedException when current user not create project`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject + // when & then + assertThrows { + editTaskTitleUseCase.invoke(taskId = dummyTask.id, newTitle = "School Library") + } + } + + @Test + fun `should throw AccessDeniedException when current user not from team in project`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy(createdBy = dummyMate.id, matesIds = listOf()) + // when & then + assertThrows { + editTaskTitleUseCase.invoke(taskId = dummyTask.id, newTitle = "School Library") + } + } + + + @Test + fun `should throw NoChangeException when new title is the same of old title`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy(createdBy = dummyMate.id , matesIds = listOf(dummyMate.id)) + every { tasksRepository.getTaskById(any()) } returns dummyTask.copy(title = "School Library") + // when & then + assertThrows { + editTaskTitleUseCase.invoke(taskId = dummyTask.id, newTitle = "School Library") + } + } + + + + @Test + fun `invoke should edit task when the task id is valid`() { + // given + every { usersRepository.getCurrentUser() } returns dummyMate + every { projectsRepository.getProjectById(any()) } returns dummyProject.copy(createdBy = dummyMate.id , matesIds = listOf(dummyMate.id)) + every { tasksRepository.getTaskById(any()) } returns dummyTask.copy(id = dummyTask.id,title = "i hate final exams") + + // when + editTaskTitleUseCase.invoke(taskId = dummyTask.id, newTitle = "School Library") + + // then + verify { tasksRepository.updateTask(any()) } + verify { logsRepository.addLog(any()) } + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/domain/usecase/task/GetTaskHistoryUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/GetTaskHistoryUseCaseTest.kt new file mode 100644 index 0000000..c7af7dc --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/GetTaskHistoryUseCaseTest.kt @@ -0,0 +1,84 @@ +package domain.usecase.task + +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import org.example.domain.NotFoundException +import org.example.domain.entity.log.AddedLog +import org.example.domain.entity.log.CreatedLog +import org.example.domain.entity.log.DeletedLog +import org.example.domain.entity.log.Log +import org.example.domain.repository.LogsRepository +import org.example.domain.usecase.task.GetTaskHistoryUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.* +import kotlin.test.assertTrue + + +class GetTaskHistoryUseCaseTest { + private val logsRepository: LogsRepository = mockk() + private lateinit var getTaskHistoryUseCase: GetTaskHistoryUseCase + private val task = dummyTasks[0] + + @BeforeEach + fun setup() { + getTaskHistoryUseCase = GetTaskHistoryUseCase(logsRepository) + } + + @Test + fun `should return list of logs when task logs exist`() { + // Given + every { logsRepository.getAllLogs() } returns dummyTasksLogs + //when + val result = getTaskHistoryUseCase(task.id) + //Then + assertTrue { result.all { it.toString().contains(task.id.toString()) } } + } + + @Test + fun `should throw Exception when logs fetching fails `() { + // Given + every { logsRepository.getAllLogs() } throws Exception() + // When & Then + assertThrows { + getTaskHistoryUseCase(task.id) + } + } + + @Test + fun `should throw NoFoundException list when no logs for the given task `() { + // Given + val dummyLogs = dummyTasksLogs.subList(0, 1) + every { logsRepository.getAllLogs() } returns dummyLogs + //when&//Then + assertThrows { + getTaskHistoryUseCase(task.id) + } + } + + private val dummyTasksLogs = listOf( + AddedLog( + username = "abc", + affectedId = UUID.randomUUID(), + affectedName = "T-101", + affectedType = Log.AffectedType.TASK, + addedTo = UUID.randomUUID().toString() + ), + CreatedLog( + username = "abc", + affectedId = dummyTasks[0].id, + affectedName = "T-101", + affectedType = Log.AffectedType.TASK + ), + DeletedLog( + username = "abc", + affectedId = dummyTasks[0].id, + affectedName = "T-101", + affectedType = Log.AffectedType.TASK, + deletedFrom = UUID.randomUUID().toString() + ) + ) +} + diff --git a/src/test/kotlin/domain/usecase/task/GetTaskUseCaseTest.kt b/src/test/kotlin/domain/usecase/task/GetTaskUseCaseTest.kt new file mode 100644 index 0000000..9a9ab53 --- /dev/null +++ b/src/test/kotlin/domain/usecase/task/GetTaskUseCaseTest.kt @@ -0,0 +1,90 @@ +package domain.usecase.task + +import dummyAdmin +import dummyProjects +import dummyTasks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.example.domain.repository.ProjectsRepository +import org.example.domain.repository.TasksRepository +import org.example.domain.repository.UsersRepository +import org.example.domain.usecase.task.GetTaskUseCase +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertTrue + + +class GetTaskUseCaseTest { + + private val tasksRepository: TasksRepository = mockk(relaxed = true) + private val projectsRepository: ProjectsRepository = mockk(relaxed = true) + private val usersRepository: UsersRepository = mockk(relaxed = true) + private lateinit var getTaskUseCase: GetTaskUseCase + + + @BeforeEach + fun setup() { + getTaskUseCase = GetTaskUseCase(tasksRepository, usersRepository, projectsRepository) + } + + @Test + fun `should return task given task id`() { + //Given + val project= dummyProject.copy(createdBy =dummyAdmin.id ) + val task= dummyTask.copy(projectId = project.id) + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(project.id) } returns project + //when + val result = getTaskUseCase(task.id) + + //then + assertTrue { result.id == task.id } + } + + @Test + fun `should not complete execution when getCurrentUser fails`() { + //given + every { usersRepository.getCurrentUser() } throws Exception() + //when && then + assertThrows { getTaskUseCase(dummyTask.id) } + verify(exactly = 0) { tasksRepository.getTaskById(any()) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + } + + @Test + fun `should not complete execution when getTaskById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(any()) } throws Exception() + //when && then + assertThrows { getTaskUseCase(dummyAdmin.id) } + verify(exactly = 0) { projectsRepository.getProjectById(any()) } + } + @Test + fun `should not complete execution when getProjectById fails`() { + //given + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(dummyAdmin.id) } returns dummyTask + every { projectsRepository.getProjectById(dummyTask.projectId) } throws Exception() + //when && then + assertThrows { getTaskUseCase(dummyAdmin.id) } + } + + @Test + fun `should throw AccessDeniedException when user is not owner or mate in the project `() { + //given + val task = dummyTask + every { usersRepository.getCurrentUser() } returns dummyAdmin + every { tasksRepository.getTaskById(task.id) } returns task + every { projectsRepository.getProjectById(task.projectId) } returns dummyProject + //when && then + assertThrows { getTaskUseCase(task.id) } + } + +} +private val dummyTask = dummyTasks[0] +private val dummyProject=dummyProjects[0] +