diff --git a/lessons/m6l3-konform/build.gradle.kts b/lessons/m6l3-konform/build.gradle.kts new file mode 100644 index 0000000..0fdc58b --- /dev/null +++ b/lessons/m6l3-konform/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("jvm") +} + +val coroutinesVersion: String by project + +dependencies { + implementation(kotlin("stdlib")) + + testImplementation(kotlin("test")) + testImplementation(kotlin("test-junit")) + + testImplementation("io.konform:konform:0.4.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") +} diff --git a/lessons/m6l3-konform/src/test/kotlin/KonformTest.kt b/lessons/m6l3-konform/src/test/kotlin/KonformTest.kt new file mode 100644 index 0000000..b45c7ca --- /dev/null +++ b/lessons/m6l3-konform/src/test/kotlin/KonformTest.kt @@ -0,0 +1,61 @@ +package ru.otus.otuskotlin.marketplace.lib.koform + +import io.konform.validation.Invalid +import io.konform.validation.Validation +import io.konform.validation.ValidationBuilder +import io.konform.validation.jsonschema.pattern +import kotlinx.datetime.* +import org.junit.Test +import kotlin.test.assertContains +import kotlin.test.assertIs +import kotlin.time.Duration.Companion.days + +// Konform - DSL для валидации +class KonformTest { + @Test + fun konform() { + val objValidator = Validation { + SomeObject::userId { + pattern("^[0-9a-zA-Z_-]{1,64}\$") + } + SomeObject::dob { + minAge(15) + } + } + + val resultUserId = objValidator.validate(SomeObject()) + assertIs>(resultUserId) + val errorUserId = resultUserId.errors.firstOrNull { it.dataPath == ".userId" } + assertContains(errorUserId?.message ?: "", "pattern") + println("RESULT: ${errorUserId?.dataPath} ${errorUserId?.message}") + + val resultAge = objValidator.validate(SomeObject(userId = "987987987")) + assertIs>(resultAge) + val errorAge = resultAge.errors.firstOrNull { it.dataPath == ".dob" } + assertContains(errorAge?.message ?: "", "Age") + println("RESULT: ${errorAge?.dataPath} ${errorAge?.message}") + } +} + +private fun ValidationBuilder.minAge(age: Int) = addConstraint( + errorMessage = "Age cannot be in range of 15..130 years", + "15..130" +) { + if (it == null) { + println("EMPTY!") + false + } else { + val date = Clock.System + .now() + .minus((age * 365).days) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + println("DATE: $date ~ $it ${it.compareTo(date)}") + (it.compareTo(date) in 0..1) + } +} + +data class SomeObject( + val userId: String = "", + val dob: LocalDate? = null, +) diff --git a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2FromTransportValidated.kt b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2FromTransportValidated.kt new file mode 100644 index 0000000..9f73d52 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2FromTransportValidated.kt @@ -0,0 +1,65 @@ +package ru.otus.otuskotlin.marketplace.api.v2.mappers + +import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.stubs.MkplStubs + +// Демонстрация форматной валидации в мапере +private sealed interface Result +private data class Ok(val value: T) : Result +private data class Err(val errors: List) : Result { + constructor(error: E) : this(listOf(error)) +} + +private fun Result.getOrExec(default: T, block: (Err) -> Unit = {}): T = when (this) { + is Ok -> this.value + is Err -> { + block(this) + default + } +} + +@Suppress("unused") +private fun Result.getOrNull(block: (Err) -> Unit = {}): T? = when (this) { + is Ok -> this.value + is Err -> { + block(this) + null + } +} + +private fun String?.transportToStubCaseValidated(): Result = when (this) { + "success" -> Ok(MkplStubs.SUCCESS) + "notFound" -> Ok(MkplStubs.NOT_FOUND) + "badId" -> Ok(MkplStubs.BAD_ID) + "badTitle" -> Ok(MkplStubs.BAD_TITLE) + "badDescription" -> Ok(MkplStubs.BAD_DESCRIPTION) + "badVisibility" -> Ok(MkplStubs.BAD_VISIBILITY) + "cannotDelete" -> Ok(MkplStubs.CANNOT_DELETE) + "badSearchString" -> Ok(MkplStubs.BAD_SEARCH_STRING) + null -> Ok(MkplStubs.NONE) + else -> Err( + MkplError( + code = "wrong-stub-case", + group = "mapper-validation", + field = "debug.stub", + message = "Unsupported value for case \"$this\"" + ) + ) +} + +@Suppress("unused") +fun MkplContext.fromTransportValidated(request: AdCreateRequest) { + command = MkplCommand.CREATE + // Вся магия здесь! + stubCase = request + .debug + ?.stub + ?.value + .transportToStubCaseValidated() + .getOrExec(MkplStubs.NONE) { err: Err -> + errors.addAll(err.errors) + state = MkplState.FAILING + } +} diff --git a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonTest/kotlin/mappers/MapperValidatedTest.kt b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonTest/kotlin/mappers/MapperValidatedTest.kt new file mode 100644 index 0000000..768ceb6 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonTest/kotlin/mappers/MapperValidatedTest.kt @@ -0,0 +1,25 @@ +package ru.otus.otuskotlin.marketplace.api.v2.mappers + +import ru.otus.otuskotlin.marketplace.api.v2.models.AdCreateRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDebug +import ru.otus.otuskotlin.marketplace.api.v2.models.AdRequestDebugStubs +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.stubs.MkplStubs +import kotlin.test.Test +import kotlin.test.assertEquals + +class MapperValidatedTest { + @Test + fun fromTransportValidated() { + val req = AdCreateRequest( + debug = AdDebug( + stub = AdRequestDebugStubs.SUCCESS, + ), + ) + + val context = MkplContext() + context.fromTransportValidated(req) + + assertEquals(MkplStubs.SUCCESS, context.stubCase) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/MkplAdProcessor.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/MkplAdProcessor.kt index 47d9958..b73cfb9 100644 --- a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/MkplAdProcessor.kt +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/MkplAdProcessor.kt @@ -4,10 +4,14 @@ import ru.otus.otuskotlin.marketplace.biz.general.initStatus import ru.otus.otuskotlin.marketplace.biz.general.operation import ru.otus.otuskotlin.marketplace.biz.general.stubs import ru.otus.otuskotlin.marketplace.biz.stubs.* +import ru.otus.otuskotlin.marketplace.biz.validation.* import ru.otus.otuskotlin.marketplace.common.MkplContext import ru.otus.otuskotlin.marketplace.common.MkplCorSettings +import ru.otus.otuskotlin.marketplace.common.models.MkplAdId +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock import ru.otus.otuskotlin.marketplace.common.models.MkplCommand import ru.otus.otuskotlin.marketplace.cor.rootChain +import ru.otus.otuskotlin.marketplace.cor.worker class MkplAdProcessor( private val corSettings: MkplCorSettings = MkplCorSettings.NONE @@ -25,6 +29,18 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() } + worker("Очистка id") { adValidating.id = MkplAdId.NONE } + worker("Очистка заголовка") { adValidating.title = adValidating.title.trim() } + worker("Очистка описания") { adValidating.description = adValidating.description.trim() } + validateTitleNotEmpty("Проверка, что заголовок не пуст") + validateTitleHasContent("Проверка символов") + validateDescriptionNotEmpty("Проверка, что описание не пусто") + validateDescriptionHasContent("Проверка символов") + + finishAdValidation("Завершение проверок") + } } operation("Получить объявление", MkplCommand.READ) { stubs("Обработка стабов") { @@ -33,6 +49,14 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() } + worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) } + validateIdNotEmpty("Проверка на непустой id") + validateIdProperFormat("Проверка формата id") + + finishAdValidation("Успешное завершение процедуры валидации") + } } operation("Изменить объявление", MkplCommand.UPDATE) { stubs("Обработка стабов") { @@ -43,6 +67,23 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() } + worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) } + worker("Очистка lock") { adValidating.lock = MkplAdLock(adValidating.lock.asString().trim()) } + worker("Очистка заголовка") { adValidating.title = adValidating.title.trim() } + worker("Очистка описания") { adValidating.description = adValidating.description.trim() } + validateIdNotEmpty("Проверка на непустой id") + validateIdProperFormat("Проверка формата id") + validateLockNotEmpty("Проверка на непустой lock") + validateLockProperFormat("Проверка формата lock") + validateTitleNotEmpty("Проверка на непустой заголовок") + validateTitleHasContent("Проверка на наличие содержания в заголовке") + validateDescriptionNotEmpty("Проверка на непустое описание") + validateDescriptionHasContent("Проверка на наличие содержания в описании") + + finishAdValidation("Успешное завершение процедуры валидации") + } } operation("Удалить объявление", MkplCommand.DELETE) { stubs("Обработка стабов") { @@ -51,6 +92,18 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adValidating") { + adValidating = adRequest.deepCopy() + } + worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) } + worker("Очистка lock") { adValidating.lock = MkplAdLock(adValidating.lock.asString().trim()) } + validateIdNotEmpty("Проверка на непустой id") + validateIdProperFormat("Проверка формата id") + validateLockNotEmpty("Проверка на непустой lock") + validateLockProperFormat("Проверка формата lock") + finishAdValidation("Успешное завершение процедуры валидации") + } } operation("Поиск объявлений", MkplCommand.SEARCH) { stubs("Обработка стабов") { @@ -59,6 +112,12 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adFilterValidating") { adFilterValidating = adFilterRequest.deepCopy() } + validateSearchStringLength("Валидация длины строки поиска в фильтре") + + finishAdFilterValidation("Успешное завершение процедуры валидации") + } } operation("Поиск подходящих предложений для объявления", MkplCommand.OFFERS) { stubs("Обработка стабов") { @@ -67,6 +126,15 @@ class MkplAdProcessor( stubDbError("Имитация ошибки работы с БД") stubNoCase("Ошибка: запрошенный стаб недопустим") } + validation { + worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() } + worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) } + validateIdNotEmpty("Проверка на непустой id") + validateIdProperFormat("Проверка формата id") + + finishAdValidation("Успешное завершение процедуры валидации") + } } }.build() } + diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/FinishValidation.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/FinishValidation.kt new file mode 100644 index 0000000..08feabd --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/FinishValidation.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker + +fun ICorChainDsl.finishAdValidation(title: String) = worker { + this.title = title + on { state == MkplState.RUNNING } + handle { + adValidated = adValidating + } +} + +fun ICorChainDsl.finishAdFilterValidation(title: String) = worker { + this.title = title + on { state == MkplState.RUNNING } + handle { + adFilterValidated = adFilterValidating + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionHasContent.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionHasContent.kt new file mode 100644 index 0000000..3f3402e --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionHasContent.kt @@ -0,0 +1,23 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker + +// пример обработки ошибки в рамках бизнес-цепочки +fun ICorChainDsl.validateDescriptionHasContent(title: String) = worker { + this.title = title + val regExp = Regex("\\p{L}") + on { adValidating.description.isNotEmpty() && !adValidating.description.contains(regExp) } + handle { + fail( + errorValidation( + field = "description", + violationCode = "noContent", + description = "field must contain letters" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionNotEmpty.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionNotEmpty.kt new file mode 100644 index 0000000..2ffac6f --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateDescriptionNotEmpty.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail + +fun ICorChainDsl.validateDescriptionNotEmpty(title: String) = worker { + this.title = title + on { adValidating.description.isEmpty() } + handle { + fail( + errorValidation( + field = "description", + violationCode = "empty", + description = "field must not be empty" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdNotEmpty.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdNotEmpty.kt new file mode 100644 index 0000000..9b2d505 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdNotEmpty.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail + +fun ICorChainDsl.validateIdNotEmpty(title: String) = worker { + this.title = title + on { adValidating.id.asString().isEmpty() } + handle { + fail( + errorValidation( + field = "id", + violationCode = "empty", + description = "field must not be empty" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdProperFormat.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdProperFormat.kt new file mode 100644 index 0000000..ecb48a1 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateIdProperFormat.kt @@ -0,0 +1,28 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail +import ru.otus.otuskotlin.marketplace.common.models.MkplAdId + +fun ICorChainDsl.validateIdProperFormat(title: String) = worker { + this.title = title + + // Может быть вынесен в MkplAdId для реализации различных форматов + val regExp = Regex("^[0-9a-zA-Z#:-]+$") + on { adValidating.id != MkplAdId.NONE && ! adValidating.id.asString().matches(regExp) } + handle { + val encodedId = adValidating.id.asString() + .replace("<", "<") + .replace(">", ">") + fail( + errorValidation( + field = "id", + violationCode = "badFormat", + description = "value $encodedId must contain only letters and numbers" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockNotEmpty.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockNotEmpty.kt new file mode 100644 index 0000000..6dee777 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockNotEmpty.kt @@ -0,0 +1,21 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker + +fun ICorChainDsl.validateLockNotEmpty(title: String) = worker { + this.title = title + on { adValidating.lock.asString().isEmpty() } + handle { + fail( + errorValidation( + field = "lock", + violationCode = "empty", + description = "field must not be empty" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockProperFormat.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockProperFormat.kt new file mode 100644 index 0000000..7252b66 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateLockProperFormat.kt @@ -0,0 +1,26 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.helpers.fail +import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker + +fun ICorChainDsl.validateLockProperFormat(title: String) = worker { + this.title = title + + // Может быть вынесен в MkplAdId для реализации различных форматов + val regExp = Regex("^[0-9a-zA-Z-]+$") + on { adValidating.lock != MkplAdLock.NONE && !adValidating.lock.asString().matches(regExp) } + handle { + val encodedId = adValidating.lock.asString() + fail( + errorValidation( + field = "lock", + violationCode = "badFormat", + description = "value $encodedId must contain only" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateSearchStringLength.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateSearchStringLength.kt new file mode 100644 index 0000000..cbaccab --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateSearchStringLength.kt @@ -0,0 +1,49 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.helpers.fail +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.chain +import ru.otus.otuskotlin.marketplace.cor.worker + +fun ICorChainDsl.validateSearchStringLength(title: String) = chain { + this.title = title + this.description = """ + Валидация длины строки поиска в поисковых фильтрах. Допустимые значения: + - null - не выполняем поиск по строке + - 3-100 - допустимая длина + - больше 100 - слишком длинная строка + """.trimIndent() + on { state == MkplState.RUNNING } + worker("Обрезка пустых символов") { adFilterValidating.searchString = adFilterValidating.searchString.trim() } + worker { + this.title = "Проверка кейса длины на 0-2 символа" + this.description = this.title + on { state == MkplState.RUNNING && adFilterValidating.searchString.length in (1..2) } + handle { + fail( + errorValidation( + field = "searchString", + violationCode = "tooShort", + description = "Search string must contain at least 3 symbols" + ) + ) + } + } + worker { + this.title = "Проверка кейса длины на более 100 символов" + this.description = this.title + on { state == MkplState.RUNNING && adFilterValidating.searchString.length > 100 } + handle { + fail( + errorValidation( + field = "searchString", + violationCode = "tooLong", + description = "Search string must be no more than 100 symbols long" + ) + ) + } + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleHasContent.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleHasContent.kt new file mode 100644 index 0000000..0f6c02c --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleHasContent.kt @@ -0,0 +1,26 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail + +fun ICorChainDsl.validateTitleHasContent(title: String) = worker { + this.title = title + this.description = """ + Проверяем, что у нас есть какие-то слова в заголовке. + Отказываем в публикации заголовков, в которых только бессмысленные символы типа %^&^$^%#^))&^*&%^^& + """.trimIndent() + val regExp = Regex("\\p{L}") + on { adValidating.title.isNotEmpty() && ! adValidating.title.contains(regExp) } + handle { + fail( + errorValidation( + field = "title", + violationCode = "noContent", + description = "field must contain letters" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleNotEmpty.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleNotEmpty.kt new file mode 100644 index 0000000..75df4ac --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/ValidateTitleNotEmpty.kt @@ -0,0 +1,22 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.helpers.fail + +// смотрим пример COR DSL валидации +fun ICorChainDsl.validateTitleNotEmpty(title: String) = worker { + this.title = title + on { adValidating.title.isEmpty() } + handle { + fail( + errorValidation( + field = "title", + violationCode = "empty", + description = "field must not be empty" + ) + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/Validation.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/Validation.kt new file mode 100644 index 0000000..65ed1d3 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/validation/Validation.kt @@ -0,0 +1,13 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl +import ru.otus.otuskotlin.marketplace.cor.chain + +fun ICorChainDsl.validation(block: ICorChainDsl.() -> Unit) = chain { + block() + title = "Валидация" + + on { state == MkplState.RUNNING } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BaseBizValidationTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BaseBizValidationTest.kt new file mode 100644 index 0000000..46fb23e --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BaseBizValidationTest.kt @@ -0,0 +1,11 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplCorSettings +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand + +abstract class BaseBizValidationTest { + protected abstract val command: MkplCommand + private val settings by lazy { MkplCorSettings() } + protected val processor by lazy { MkplAdProcessor(settings) } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationCreateTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationCreateTest.kt new file mode 100644 index 0000000..ba85652 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationCreateTest.kt @@ -0,0 +1,19 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import kotlin.test.Test + +// смотрим пример теста валидации, собранного из тестовых функций-оберток +class BizValidationCreateTest: BaseBizValidationTest() { + override val command: MkplCommand = MkplCommand.CREATE + + @Test fun correctTitle() = validationTitleCorrect(command, processor) + @Test fun trimTitle() = validationTitleTrim(command, processor) + @Test fun emptyTitle() = validationTitleEmpty(command, processor) + @Test fun badSymbolsTitle() = validationTitleSymbols(command, processor) + + @Test fun correctDescription() = validationDescriptionCorrect(command, processor) + @Test fun trimDescription() = validationDescriptionTrim(command, processor) + @Test fun emptyDescription() = validationDescriptionEmpty(command, processor) + @Test fun badSymbolsDescription() = validationDescriptionSymbols(command, processor) +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationDeleteTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationDeleteTest.kt new file mode 100644 index 0000000..f31f6cc --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationDeleteTest.kt @@ -0,0 +1,23 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import validation.validationLockCorrect +import validation.validationLockEmpty +import validation.validationLockFormat +import validation.validationLockTrim +import kotlin.test.Test + +class BizValidationDeleteTest: BaseBizValidationTest() { + override val command = MkplCommand.DELETE + + @Test fun correctId() = validationIdCorrect(command, processor) + @Test fun trimId() = validationIdTrim(command, processor) + @Test fun emptyId() = validationIdEmpty(command, processor) + @Test fun badFormatId() = validationIdFormat(command, processor) + + @Test fun correctLock() = validationLockCorrect(command, processor) + @Test fun trimLock() = validationLockTrim(command, processor) + @Test fun emptyLock() = validationLockEmpty(command, processor) + @Test fun badFormatLock() = validationLockFormat(command, processor) + +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationOffersTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationOffersTest.kt new file mode 100644 index 0000000..5d3293c --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationOffersTest.kt @@ -0,0 +1,14 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import kotlin.test.Test + +class BizValidationOffersTest: BaseBizValidationTest() { + override val command = MkplCommand.OFFERS + + @Test fun correctId() = validationIdCorrect(command, processor) + @Test fun trimId() = validationIdTrim(command, processor) + @Test fun emptyId() = validationIdEmpty(command, processor) + @Test fun badFormatId() = validationIdFormat(command, processor) + +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationReadTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationReadTest.kt new file mode 100644 index 0000000..e9a325b --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationReadTest.kt @@ -0,0 +1,14 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import kotlin.test.Test + +class BizValidationReadTest: BaseBizValidationTest() { + override val command = MkplCommand.READ + + @Test fun correctId() = validationIdCorrect(command, processor) + @Test fun trimId() = validationIdTrim(command, processor) + @Test fun emptyId() = validationIdEmpty(command, processor) + @Test fun badFormatId() = validationIdFormat(command, processor) + +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationSearchTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationSearchTest.kt new file mode 100644 index 0000000..e27ca9b --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationSearchTest.kt @@ -0,0 +1,28 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplAdFilter +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.common.models.MkplWorkMode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class BizValidationSearchTest: BaseBizValidationTest() { + override val command = MkplCommand.SEARCH + + @Test + fun correctEmpty() = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adFilterRequest = MkplAdFilter() + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationUpdateTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationUpdateTest.kt new file mode 100644 index 0000000..ae1e29e --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/BizValidationUpdateTest.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import ru.otus.otuskotlin.marketplace.common.models.MkplCommand +import validation.validationLockCorrect +import validation.validationLockEmpty +import validation.validationLockFormat +import validation.validationLockTrim +import kotlin.test.Test + +class BizValidationUpdateTest: BaseBizValidationTest() { + override val command = MkplCommand.UPDATE + + @Test fun correctTitle() = validationTitleCorrect(command, processor) + @Test fun trimTitle() = validationTitleTrim(command, processor) + @Test fun emptyTitle() = validationTitleEmpty(command, processor) + @Test fun badSymbolsTitle() = validationTitleSymbols(command, processor) + + @Test fun correctDescription() = validationDescriptionCorrect(command, processor) + @Test fun trimDescription() = validationDescriptionTrim(command, processor) + @Test fun emptyDescription() = validationDescriptionEmpty(command, processor) + @Test fun badSymbolsDescription() = validationDescriptionSymbols(command, processor) + + @Test fun correctId() = validationIdCorrect(command, processor) + @Test fun trimId() = validationIdTrim(command, processor) + @Test fun emptyId() = validationIdEmpty(command, processor) + @Test fun badFormatId() = validationIdFormat(command, processor) + + @Test fun correctLock() = validationLockCorrect(command, processor) + @Test fun trimLock() = validationLockTrim(command, processor) + @Test fun emptyLock() = validationLockEmpty(command, processor) + @Test fun badFormatLock() = validationLockFormat(command, processor) + +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateSearchStringLengthTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateSearchStringLengthTest.kt new file mode 100644 index 0000000..c651425 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateSearchStringLengthTest.kt @@ -0,0 +1,59 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplAdFilter +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.cor.rootChain +import kotlin.test.Test +import kotlin.test.assertEquals + +class ValidateSearchStringLengthTest { + @Test + fun emptyString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = "")) + chain.exec(ctx) + assertEquals(MkplState.RUNNING, ctx.state) + assertEquals(0, ctx.errors.size) + } + + @Test + fun blankString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = " ")) + chain.exec(ctx) + assertEquals(MkplState.RUNNING, ctx.state) + assertEquals(0, ctx.errors.size) + } + + @Test + fun shortString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = "12")) + chain.exec(ctx) + assertEquals(MkplState.FAILING, ctx.state) + assertEquals(1, ctx.errors.size) + assertEquals("validation-searchString-tooShort", ctx.errors.first().code) + } + + @Test + fun normalString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = "123")) + chain.exec(ctx) + assertEquals(MkplState.RUNNING, ctx.state) + assertEquals(0, ctx.errors.size) + } + + @Test + fun longString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = "12".repeat(51))) + chain.exec(ctx) + assertEquals(MkplState.FAILING, ctx.state) + assertEquals(1, ctx.errors.size) + assertEquals("validation-searchString-tooLong", ctx.errors.first().code) + } + + companion object { + val chain = rootChain { + validateSearchStringLength("") + }.build() + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateTitleHasContentTest.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateTitleHasContentTest.kt new file mode 100644 index 0000000..51f4e0d --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidateTitleHasContentTest.kt @@ -0,0 +1,43 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.models.MkplAdFilter +import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.cor.rootChain +import kotlin.test.Test +import kotlin.test.assertEquals + +class ValidateTitleHasContentTest { + @Test + fun emptyString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adValidating = MkplAd(title = "")) + chain.exec(ctx) + assertEquals(MkplState.RUNNING, ctx.state) + assertEquals(0, ctx.errors.size) + } + + @Test + fun noContent() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adValidating = MkplAd(title = "12!@#$%^&*()_+-=")) + chain.exec(ctx) + assertEquals(MkplState.FAILING, ctx.state) + assertEquals(1, ctx.errors.size) + assertEquals("validation-title-noContent", ctx.errors.first().code) + } + + @Test + fun normalString() = runTest { + val ctx = MkplContext(state = MkplState.RUNNING, adFilterValidating = MkplAdFilter(searchString = "Ж")) + chain.exec(ctx) + assertEquals(MkplState.RUNNING, ctx.state) + assertEquals(0, ctx.errors.size) + } + + companion object { + val chain = rootChain { + validateTitleHasContent("") + }.build() + } +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadDescription.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadDescription.kt new file mode 100644 index 0000000..06b9153 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadDescription.kt @@ -0,0 +1,96 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +private val stub = MkplAdStub.get() + +fun validationDescriptionCorrect(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) + assertEquals("abc", ctx.adValidated.description) +} + +fun validationDescriptionTrim(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "abc", + description = " \n\tabc \n\t", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) + assertEquals("abc", ctx.adValidated.description) +} + +fun validationDescriptionEmpty(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "abc", + description = "", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("description", error?.field) + assertContains(error?.message ?: "", "description") +} + +fun validationDescriptionSymbols(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "abc", + description = "!@#$%^&*(),.{}", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("description", error?.field) + assertContains(error?.message ?: "", "description") +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadId.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadId.kt new file mode 100644 index 0000000..0ae60eb --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadId.kt @@ -0,0 +1,91 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.* +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +fun validationIdCorrect(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123-234-abc-ABC"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) +} + +fun validationIdTrim(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId(" \n\t 123-234-abc-ABC \n\t "), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) +} + +fun validationIdEmpty(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId(""), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("id", error?.field) + assertContains(error?.message ?: "", "id") +} + +fun validationIdFormat(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("!@#\$%^&*(),.{}"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("id", error?.field) + assertContains(error?.message ?: "", "id") +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadLock.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadLock.kt new file mode 100644 index 0000000..8619199 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadLock.kt @@ -0,0 +1,91 @@ +package validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.* +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +fun validationLockCorrect(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123-234-abc-ABC"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) +} + +fun validationLockTrim(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123-234-abc-ABC"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock(" \n\t 123-234-abc-ABC \n\t "), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) +} + +fun validationLockEmpty(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123-234-abc-ABC"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock(""), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("lock", error?.field) + assertContains(error?.message ?: "", "id") +} + +fun validationLockFormat(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123-234-abc-ABC"), + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("!@#\$%^&*(),.{}"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("lock", error?.field) + assertContains(error?.message ?: "", "id") +} diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadTitle.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadTitle.kt new file mode 100644 index 0000000..3bd52c5 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonTest/kotlin/validation/ValidationBadTitle.kt @@ -0,0 +1,96 @@ +package ru.otus.otuskotlin.marketplace.biz.validation + +import kotlinx.coroutines.test.runTest +import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor +import ru.otus.otuskotlin.marketplace.common.MkplContext +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +private val stub = MkplAdStub.get() + +fun validationTitleCorrect(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "abc", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) + assertEquals("abc", ctx.adValidated.title) +} + +fun validationTitleTrim(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = " \n\t abc \t\n ", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(0, ctx.errors.size) + assertNotEquals(MkplState.FAILING, ctx.state) + assertEquals("abc", ctx.adValidated.title) +} + +fun validationTitleEmpty(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = stub.id, + title = "", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("title", error?.field) + assertContains(error?.message ?: "", "title") +} + +fun validationTitleSymbols(command: MkplCommand, processor: MkplAdProcessor) = runTest { + val ctx = MkplContext( + command = command, + state = MkplState.NONE, + workMode = MkplWorkMode.TEST, + adRequest = MkplAd( + id = MkplAdId("123"), + title = "!@#$%^&*(),.{}", + description = "abc", + adType = MkplDealSide.DEMAND, + visibility = MkplVisibility.VISIBLE_PUBLIC, + lock = MkplAdLock("123-234-abc-ABC"), + ), + ) + processor.exec(ctx) + assertEquals(1, ctx.errors.size) + assertEquals(MkplState.FAILING, ctx.state) + val error = ctx.errors.firstOrNull() + assertEquals("title", error?.field) + assertContains(error?.message ?: "", "title") +} diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/MkplContext.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/MkplContext.kt index 7ab6829..786f222 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/MkplContext.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/MkplContext.kt @@ -20,7 +20,12 @@ data class MkplContext( var adRequest: MkplAd = MkplAd(), var adFilterRequest: MkplAdFilter = MkplAdFilter(), + var adValidating: MkplAd = MkplAd(), + var adFilterValidating: MkplAdFilter = MkplAdFilter(), + + var adValidated: MkplAd = MkplAd(), + var adFilterValidated: MkplAdFilter = MkplAdFilter(), + var adResponse: MkplAd = MkplAd(), var adsResponse: MutableList = mutableListOf(), - - ) +) diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/helpers/MkplErrorsHelpers.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/helpers/MkplErrorsHelpers.kt index f0e1e84..18878bd 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/helpers/MkplErrorsHelpers.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/helpers/MkplErrorsHelpers.kt @@ -3,6 +3,7 @@ package ru.otus.otuskotlin.marketplace.common.helpers import ru.otus.otuskotlin.marketplace.common.MkplContext import ru.otus.otuskotlin.marketplace.common.models.MkplError import ru.otus.otuskotlin.marketplace.common.models.MkplState +import ru.otus.otuskotlin.marketplace.logging.common.LogLevel fun Throwable.asMkplError( code: String = "unknown", @@ -22,3 +23,20 @@ inline fun MkplContext.fail(error: MkplError) { addError(error) state = MkplState.FAILING } + +inline fun errorValidation( + field: String, + /** + * Код, характеризующий ошибку. Не должен включать имя поля или указание на валидацию. + * Например: empty, badSymbols, tooLong, etc + */ + violationCode: String, + description: String, + level: LogLevel = LogLevel.ERROR, +) = MkplError( + code = "validation-$field-$violationCode", + field = field, + group = "validation", + message = "Validation error for field $field: $description", + level = level, +) diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAd.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAd.kt index 995fe45..dfd5e36 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAd.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAd.kt @@ -11,10 +11,13 @@ data class MkplAd( var lock: MkplAdLock = MkplAdLock.NONE, val permissionsClient: MutableSet = mutableSetOf() ) { + fun deepCopy(): MkplAd = copy( + permissionsClient = permissionsClient.toMutableSet(), + ) + fun isEmpty() = this == NONE companion object { private val NONE = MkplAd() } - } diff --git a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAdFilter.kt b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAdFilter.kt index b7352b4..ccb155b 100644 --- a/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAdFilter.kt +++ b/ok-marketplace-be/ok-marketplace-common/src/commonMain/kotlin/models/MkplAdFilter.kt @@ -4,4 +4,12 @@ data class MkplAdFilter( var searchString: String = "", var ownerId: MkplUserId = MkplUserId.NONE, var dealSide: MkplDealSide = MkplDealSide.NONE, -) +) { + fun deepCopy(): MkplAdFilter = copy() + + fun isEmpty() = this == NONE + + companion object { + private val NONE = MkplAdFilter() + } +}