Merge pull request #7 from jwattik/m6-biz

module 6 lesson 1
This commit is contained in:
Александр Веденев
2025-04-07 12:48:34 +07:00
committed by GitHub
78 changed files with 2341 additions and 42 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.multiplatform) apply false
}
group = "ru.otus.lessons"
group = "ru.otus"
version = "0.0.1"
subprojects {
@@ -6,7 +6,7 @@ import ru.otus.messenger.api.v1.mappers.exceptions.UnknownRequestClass
import ru.otus.messenger.api.v1.models.*
import ru.otus.messenger.common.models.*
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
fun MessengerContext.fromTransport(request: IRequest) = when (request) {
is ChatCreateRequest -> fromTransport(request)
@@ -31,13 +31,13 @@ private fun Debug?.transportToWorkMode(): WorkMode = when (this?.mode) {
null -> WorkMode.PROD
}
private fun Debug?.transportToStubCase(): Stubs = when (this?.stub) {
DebugStubs.SUCCESS -> Stubs.SUCCESS
DebugStubs.NOT_FOUND -> Stubs.NOT_FOUND
DebugStubs.VALUE_ERROR -> Stubs.VALUE_ERROR
DebugStubs.MISSING_DATA -> Stubs.MISSING_DATA
DebugStubs.CANNOT_DELETE -> Stubs.CANNOT_DELETE
null -> Stubs.NONE
private fun Debug?.transportToStubCase(): MessengerStubs = when (this?.stub) {
DebugStubs.SUCCESS -> MessengerStubs.SUCCESS
DebugStubs.NOT_FOUND -> MessengerStubs.NOT_FOUND
DebugStubs.VALUE_ERROR -> MessengerStubs.VALUE_ERROR
DebugStubs.MISSING_DATA -> MessengerStubs.MISSING_DATA
DebugStubs.CANNOT_DELETE -> MessengerStubs.CANNOT_DELETE
null -> MessengerStubs.NONE
}
fun MessengerContext.fromTransport(request: ChatCreateRequest) {
@@ -20,7 +20,7 @@ import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.RequestId
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
class MapperCreateTest {
@Test
@@ -42,7 +42,7 @@ class MapperCreateTest {
val context = MessengerContext()
context.fromTransport(req)
assertEquals(Stubs.SUCCESS, context.stubCase)
assertEquals(MessengerStubs.SUCCESS, context.stubCase)
assertEquals(WorkMode.STUB, context.workMode)
assertEquals("title", context.chatRequest.title)
assertEquals(ChatType.PRIVATE, context.chatRequest.type)
@@ -17,7 +17,7 @@ import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.RequestId
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
class MapperDeleteTest {
@Test
@@ -33,7 +33,7 @@ class MapperDeleteTest {
val context = MessengerContext()
context.fromTransport(req)
assertEquals(Stubs.SUCCESS, context.stubCase)
assertEquals(MessengerStubs.SUCCESS, context.stubCase)
assertEquals(WorkMode.STUB, context.workMode)
assertEquals("", context.chatRequest.title)
assertEquals("chat-id", context.chatRequest.id.asString())
@@ -19,7 +19,7 @@ import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.RequestId
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
class MapperReadTest {
@Test
@@ -35,7 +35,7 @@ class MapperReadTest {
val context = MessengerContext()
context.fromTransport(req)
assertEquals(Stubs.SUCCESS, context.stubCase)
assertEquals(MessengerStubs.SUCCESS, context.stubCase)
assertEquals(WorkMode.STUB, context.workMode)
assertEquals("", context.chatRequest.title)
assertEquals("chat-id", context.chatRequest.id.asString())
@@ -20,7 +20,7 @@ import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.RequestId
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
class MapperSearchTest {
@Test
@@ -41,7 +41,7 @@ class MapperSearchTest {
val context = MessengerContext()
context.fromTransport(req)
assertEquals(Stubs.SUCCESS, context.stubCase)
assertEquals(MessengerStubs.SUCCESS, context.stubCase)
assertEquals(WorkMode.STUB, context.workMode)
assertEquals("title", context.chatRequest.title)
assertEquals(ChatType.PRIVATE, context.chatRequest.type)
@@ -20,7 +20,7 @@ import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.RequestId
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
class MapperUpdateTest {
@Test
@@ -47,7 +47,7 @@ class MapperUpdateTest {
val context = MessengerContext()
context.fromTransport(req)
assertEquals(Stubs.SUCCESS, context.stubCase)
assertEquals(MessengerStubs.SUCCESS, context.stubCase)
assertEquals(WorkMode.STUB, context.workMode)
assertEquals("chat-id", context.chatRequest.id.asString())
assertEquals("title", context.chatRequest.title)
@@ -1,13 +1,19 @@
package ru.otus.messenger.api.v1
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.text.SimpleDateFormat
import ru.otus.messenger.api.v1.models.IRequest
import ru.otus.messenger.api.v1.models.IResponse
val apiV1Mapper: JsonMapper = JsonMapper.builder().run {
addModule(KotlinModule.Builder().build())
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
defaultDateFormat(SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"))
serializationInclusion(Include.NON_NULL)
enable(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL)
build()
}
@@ -16,7 +16,7 @@ class RequestV1SerializationTest {
title = "New chat",
type = ChatCreateRequestAllOfChat.Type.GROUP,
mode = ChatCreateRequestAllOfChat.Mode.PERSONAL,
participants = emptyList(),
participants = emptySet(),
metadata = """
{
"isOwner": true,
@@ -16,7 +16,7 @@ class ResponseV1SerializationTest {
title = "Test chat title",
type = Chat.Type.CHANNEL,
mode = Chat.Mode.WORK,
participants = emptyList(),
participants = emptySet(),
createdAt = Instant.now().toString(),
isArchived = false,
metadata = null
@@ -1,2 +1,133 @@
package ru.otus.messenger.app.stub
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.jackson.*
import io.ktor.server.testing.*
import java.util.UUID
import ru.otus.messenger.api.v1.models.ChatCreateRequest
import ru.otus.messenger.api.v1.models.ChatCreateRequestAllOfChat
import ru.otus.messenger.api.v1.models.ChatCreateResponse
import ru.otus.messenger.api.v1.models.ChatDeleteRequest
import ru.otus.messenger.api.v1.models.ChatDeleteResponse
import ru.otus.messenger.api.v1.models.ChatReadRequest
import ru.otus.messenger.api.v1.models.ChatReadResponse
import ru.otus.messenger.api.v1.models.ChatSearchRequest
import ru.otus.messenger.api.v1.models.ChatSearchRequestAllOfCriteria
import ru.otus.messenger.api.v1.models.ChatSearchResponse
import ru.otus.messenger.api.v1.models.Debug
import ru.otus.messenger.api.v1.models.DebugMode
import ru.otus.messenger.api.v1.models.DebugStubs
import ru.otus.messenger.api.v1.models.IRequest
import ru.otus.messenger.api.v1.models.ResponseResult
import ru.otus.messenger.app.common.MessengerAppSettingsData
import ru.otus.messenger.app.module
import ru.otus.messenger.common.MessengerCorSettings
import kotlin.test.Test
import kotlin.test.assertEquals
import ru.otus.messenger.stubs.MessengerChatStubSample
class V1StubApiTest {
private val chatId = UUID.randomUUID().toString()
private val participants = (MessengerChatStubSample.participants.map { it.asString() } + MessengerChatStubSample.chatOwnerId).toSet()
@Test
fun create() = v1TestApplication(
func = "create",
request = ChatCreateRequest(
chat = ChatCreateRequestAllOfChat(
title = "New chat",
description = "New chat description",
),
debug = Debug(
mode = DebugMode.STUB,
stub = DebugStubs.SUCCESS
)
),
) { response ->
val responseObj = response.body<ChatCreateResponse>()
assertEquals(200, response.status.value)
assertEquals(MessengerChatStubSample.chatOwnerId, responseObj.chat?.ownerId)
assertEquals("New chat", responseObj.chat?.title)
assertEquals(participants, responseObj.chat?.participants)
}
@Test
fun read() = v1TestApplication(
func = "read",
request = ChatReadRequest(
chatId = MessengerChatStubSample.chatId,
debug = Debug(
mode = DebugMode.STUB,
stub = DebugStubs.SUCCESS
)
),
) { response ->
val responseObj = response.body<ChatReadResponse>()
assertEquals(200, response.status.value)
assertEquals(MessengerChatStubSample.chatId, responseObj.chat?.id)
}
@Test
fun delete() = v1TestApplication(
func = "delete",
request = ChatDeleteRequest(
chatId = chatId,
debug = Debug(
mode = DebugMode.STUB,
stub = DebugStubs.SUCCESS
)
),
) { response ->
val responseObj = response.body<ChatDeleteResponse>()
assertEquals(200, response.status.value)
assertEquals(ResponseResult.SUCCESS, responseObj.result)
}
@Test
fun search() = v1TestApplication(
func = "search",
request = ChatSearchRequest(
criteria = ChatSearchRequestAllOfCriteria(
title = "Chat search title",
type = ChatSearchRequestAllOfCriteria.Type.GROUP,
),
debug = Debug(
mode = DebugMode.STUB,
stub = DebugStubs.SUCCESS
)
),
) { response ->
val responseObj = response.body<ChatSearchResponse>()
assertEquals(200, response.status.value)
assertEquals(2, responseObj.chats?.size)
assertEquals("Chat search title", responseObj.chats?.first()?.title)
}
private fun v1TestApplication(
func: String,
request: IRequest,
function: suspend (HttpResponse) -> Unit,
): Unit = testApplication {
application { module(MessengerAppSettingsData(corSettings = MessengerCorSettings())) }
val client = createClient {
install(ContentNegotiation) {
jackson {
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
enable(SerializationFeature.INDENT_OUTPUT)
writerWithDefaultPrettyPrinter()
}
}
}
val response = client.post("/v1/chat/$func") {
contentType(ContentType.Application.Json)
setBody(request)
}
function(response)
}
}
@@ -4,8 +4,13 @@ plugins {
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.coroutines)
implementation(libs.kotlin.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(project(":ok-messenger-common"))
implementation(project(":ok-messenger-stubs"))
implementation("ru.otus.messenger.libs:ok-messenger-lib-cor")
testImplementation(kotlin("test-junit"))
testImplementation(libs.kotlin.coroutines.test)
}
@@ -1,17 +1,145 @@
package ru.otus.messenger.biz
import ru.otus.messenger.biz.general.initStatus
import ru.otus.messenger.biz.general.operation
import ru.otus.messenger.biz.general.stubs
import ru.otus.messenger.biz.general.validation
import ru.otus.messenger.biz.stubs.stubCreateSuccess
import ru.otus.messenger.biz.stubs.stubReadSuccess
import ru.otus.messenger.biz.stubs.stubUpdateSuccess
import ru.otus.messenger.biz.stubs.stubDbError
import ru.otus.messenger.biz.stubs.stubDeleteSuccess
import ru.otus.messenger.biz.stubs.stubNoCase
import ru.otus.messenger.biz.stubs.stubSearchSuccess
import ru.otus.messenger.biz.stubs.stubValidationBadDescription
import ru.otus.messenger.biz.stubs.stubValidationBadId
import ru.otus.messenger.biz.stubs.stubValidationBadTitle
import ru.otus.messenger.biz.validation.finishChatFilterValidation
import ru.otus.messenger.biz.validation.finishChatValidation
import ru.otus.messenger.biz.validation.validateDescriptionHasContent
import ru.otus.messenger.biz.validation.validateDescriptionNotEmpty
import ru.otus.messenger.biz.validation.validateIdNotEmpty
import ru.otus.messenger.biz.validation.validateIdProperFormat
import ru.otus.messenger.biz.validation.validateSearchStringLength
import ru.otus.messenger.biz.validation.validateTitleHasContent
import ru.otus.messenger.biz.validation.validateTitleNotEmpty
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.stubs.MessengerChatStub
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.cor.dsl.rootChain
import ru.otus.messenger.cor.dsl.worker
@Suppress("unused", "RedundantSuspendModifier")
class MessengerProcessor(val corSettings: MessengerCorSettings) {
suspend fun exec(ctx: MessengerContext) {
ctx.chatResponse = MessengerChatStub.get()
ctx.chatsResponse = MessengerChatStub.prepareSearchList("New chat", ChatType.GROUP, ChatMode.PERSONAL).toMutableList()
ctx.state = ChatState.RUNNING
}
class MessengerProcessor(
private val corSettings: MessengerCorSettings = MessengerCorSettings.NONE
) {
suspend fun exec(ctx: MessengerContext) = businessChain.exec(
ctx.also { it.corSettings = corSettings }
)
private val businessChain = rootChain<MessengerContext> {
initStatus("Инициализация статуса")
operation("Создание чата", ChatCommand.CREATE) {
stubs("Обработка стабов") {
stubCreateSuccess("Имитация успешной обработки", corSettings)
stubValidationBadTitle("Имитация ошибки валидации заголовка")
stubValidationBadDescription("Имитация ошибки валидации описания")
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в chatValidating") { chatValidating = chatRequest.deepCopy() }
worker("Очистка id") { chatValidating.id = ChatId.NONE }
worker("Очистка заголовка") { chatValidating.title = chatValidating.title.trim() }
worker("Очистка описания") { chatValidating.description = chatValidating.description.trim() }
validateTitleNotEmpty("Проверка, что заголовок не пуст")
validateTitleHasContent("Проверка символов")
validateDescriptionNotEmpty("Проверка, что описание не пусто")
validateDescriptionHasContent("Проверка символов")
finishChatValidation("Завершение проверок")
}
}
operation("Получить чат", ChatCommand.READ) {
stubs("Обработка стабов") {
stubReadSuccess("Имитация успешной обработки", corSettings)
stubValidationBadId("Имитация ошибки валидации id")
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в chatValidating") { chatValidating = chatRequest.deepCopy() }
worker("Очистка id") { chatValidating.id = ChatId(chatValidating.id.asString().trim()) }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")
finishChatValidation("Успешное завершение процедуры валидации")
}
}
operation("Изменить чат", ChatCommand.UPDATE) {
stubs("Обработка стабов") {
stubUpdateSuccess("Имитация успешной обработки", corSettings)
stubValidationBadId("Имитация ошибки валидации id")
stubValidationBadTitle("Имитация ошибки валидации заголовка")
stubValidationBadDescription("Имитация ошибки валидации описания")
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в chatValidating") { chatValidating = chatRequest.deepCopy() }
worker("Очистка id") { chatValidating.id = ChatId(chatValidating.id.asString().trim()) }
worker("Очистка заголовка") { chatValidating.title = chatValidating.title.trim() }
worker("Очистка описания") { chatValidating.description = chatValidating.description.trim() }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")
validateTitleNotEmpty("Проверка на непустой заголовок")
validateTitleHasContent("Проверка на наличие содержания в заголовке")
validateDescriptionNotEmpty("Проверка на непустое описание")
validateDescriptionHasContent("Проверка на наличие содержания в описании")
finishChatValidation("Успешное завершение процедуры валидации")
}
}
operation("Удалить чат", ChatCommand.DELETE) {
stubs("Обработка стабов") {
stubDeleteSuccess("Имитация успешной обработки", corSettings)
stubValidationBadId("Имитация ошибки валидации id")
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в chatValidating") {
chatValidating = chatRequest.deepCopy()
}
worker("Очистка id") { chatValidating.id = ChatId(chatValidating.id.asString().trim()) }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")
finishChatValidation("Успешное завершение процедуры валидации")
}
}
operation("Поиск чата", ChatCommand.SEARCH) {
stubs("Обработка стабов") {
stubSearchSuccess("Имитация успешной обработки", corSettings)
stubValidationBadId("Имитация ошибки валидации id")
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в chatFilterValidating") { chatFilterValidating = chatFilterRequest.deepCopy() }
validateSearchStringLength("Валидация длины строки поиска в фильтре")
finishChatFilterValidation("Успешное завершение процедуры валидации")
}
}
}.build()
}
@@ -0,0 +1,15 @@
package ru.otus.messenger.biz.general
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.initStatus(title: String) = worker {
this.title = title
this.description = """
Этот обработчик устанавливает стартовый статус обработки. Запускается только в случае не заданного статуса.
""".trimIndent()
on { state == ChatState.NONE }
handle { state = ChatState.RUNNING }
}
@@ -0,0 +1,17 @@
package ru.otus.messenger.biz.general
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.chain
fun ICorChainDsl<MessengerContext>.operation(
title: String,
command: ChatCommand,
block: ICorChainDsl<MessengerContext>.() -> Unit
) = chain {
block()
this.title = title
on { this.command == command && state == ChatState.RUNNING }
}
@@ -0,0 +1,13 @@
package ru.otus.messenger.biz.general
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.chain
fun ICorChainDsl<MessengerContext>.stubs(title: String = "Обработка стабов", block: ICorChainDsl<MessengerContext>.() -> Unit) = chain {
block()
this.title = title
on { workMode == WorkMode.STUB && state == ChatState.RUNNING }
}
@@ -0,0 +1,12 @@
package ru.otus.messenger.biz.general
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.chain
fun ICorChainDsl<MessengerContext>.validation(block: ICorChainDsl<MessengerContext>.() -> Unit) = chain {
block()
title = "Валидация"
on { state == ChatState.RUNNING }
}
@@ -0,0 +1,33 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
import ru.otus.messenger.logging.common.LogLevel
import ru.otus.messenger.stubs.MessengerChatStub
fun ICorChainDsl<MessengerContext>.stubCreateSuccess(title: String, corSettings: MessengerCorSettings) = worker {
this.title = title
this.description = """
Кейс успеха для создания чата
""".trimIndent()
on { stubCase == MessengerStubs.SUCCESS && state == ChatState.RUNNING }
val logger = corSettings.loggerProvider.logger("stubCreateSuccess")
handle {
logger.doWithLogging(id = this.requestId.asString(), LogLevel.DEBUG) {
state = ChatState.FINISHING
val stub = MessengerChatStub.prepareResult {
chatRequest.title.takeIf { it.isNotBlank() }?.also { this.title = it }
chatRequest.description.takeIf { it.isNotBlank() }?.also { this.description = it }
chatRequest.type.takeIf { it != ChatType.NONE }?.also { this.type = it }
chatRequest.mode.takeIf { it != ChatMode.NONE }?.also { this.mode = it }
}
chatResponse = stub
}
}
}
@@ -0,0 +1,26 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubDbError(title: String) = worker {
this.title = title
this.description = """
Кейс ошибки базы данных
""".trimIndent()
on { stubCase == MessengerStubs.DB_ERROR && state == ChatState.RUNNING }
handle {
fail(
ChatError(
group = "internal",
code = "internal-db",
message = "Internal error"
)
)
}
}
@@ -0,0 +1,28 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
import ru.otus.messenger.logging.common.LogLevel
import ru.otus.messenger.stubs.MessengerChatStub
fun ICorChainDsl<MessengerContext>.stubDeleteSuccess(title: String, corSettings: MessengerCorSettings) = worker {
this.title = title
this.description = """
Кейс успеха для удаления объявления
""".trimIndent()
on { stubCase == MessengerStubs.SUCCESS && state == ChatState.RUNNING }
val logger = corSettings.loggerProvider.logger("stubDeleteSuccess")
handle {
logger.doWithLogging(id = this.requestId.asString(), LogLevel.DEBUG) {
state = ChatState.FINISHING
val stub = MessengerChatStub.prepareResult {
chatRequest.title.takeIf { it.isNotBlank() }?.also { this.title = it }
}
chatResponse = stub
}
}
}
@@ -0,0 +1,26 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubNoCase(title: String) = worker {
this.title = title
this.description = """
Валидируем ситуацию, когда запрошен кейс, который не поддерживается в стабах
""".trimIndent()
on { state == ChatState.RUNNING }
handle {
fail(
ChatError(
code = "validation",
field = "stub",
group = "validation",
message = "Wrong stub case is requested: ${stubCase.name}"
)
)
}
}
@@ -0,0 +1,28 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubNotFound(title: String) = worker {
this.title = title
this.description = """
Entity of interest were not found
""".trimIndent()
on { stubCase == MessengerStubs.NOT_FOUND && state == ChatState.RUNNING }
handle {
fail(
ChatError(
group = "internal",
code = "internal-db",
message = "Entity not found error"
)
)
}
}
@@ -0,0 +1,28 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
import ru.otus.messenger.logging.common.LogLevel
import ru.otus.messenger.stubs.MessengerChatStub
fun ICorChainDsl<MessengerContext>.stubReadSuccess(title: String, corSettings: MessengerCorSettings) = worker {
this.title = title
this.description = """
Кейс успеха для чтения чата
""".trimIndent()
on { stubCase == MessengerStubs.SUCCESS && state == ChatState.RUNNING }
val logger = corSettings.loggerProvider.logger("stubReadSuccess")
handle {
logger.doWithLogging(id = this.requestId.asString(), LogLevel.DEBUG) {
state = ChatState.FINISHING
val stub = MessengerChatStub.prepareResult {
chatRequest.title.takeIf { it.isNotBlank() }?.also { this.title = it }
}
chatResponse = stub
}
}
}
@@ -0,0 +1,32 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
import ru.otus.messenger.logging.common.LogLevel
import ru.otus.messenger.stubs.MessengerChatStub
fun ICorChainDsl<MessengerContext>.stubSearchSuccess(title: String, corSettings: MessengerCorSettings) = worker {
this.title = title
this.description = """
Кейс успеха для поиска объявлений
""".trimIndent()
on { stubCase == MessengerStubs.SUCCESS && state == ChatState.RUNNING }
val logger = corSettings.loggerProvider.logger("stubSearchSuccess")
handle {
logger.doWithLogging(id = this.requestId.asString(), LogLevel.DEBUG) {
state = ChatState.FINISHING
chatsResponse.addAll(
MessengerChatStub.prepareSearchList(
(chatFilterRequest.searchFields.first() as StringSearchField).stringValue,
ChatType.GROUP
)
)
}
}
}
@@ -0,0 +1,35 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
import ru.otus.messenger.logging.common.LogLevel
import ru.otus.messenger.stubs.MessengerChatStub
fun ICorChainDsl<MessengerContext>.stubUpdateSuccess(title: String, corSettings: MessengerCorSettings) = worker {
this.title = title
this.description = """
Кейс успеха для изменения объявления
""".trimIndent()
on { stubCase == MessengerStubs.SUCCESS && state == ChatState.RUNNING }
val logger = corSettings.loggerProvider.logger("stubUpdateSuccess")
handle {
logger.doWithLogging(id = this.requestId.asString(), LogLevel.DEBUG) {
state = ChatState.FINISHING
val stub = MessengerChatStub.prepareResult {
chatRequest.id.takeIf { it != ChatId.NONE }?.also { this.id = it }
chatRequest.title.takeIf { it.isNotBlank() }?.also { this.title = it }
chatRequest.description.takeIf { it.isNotBlank() }?.also { this.description = it }
chatRequest.type.takeIf { it != ChatType.NONE }?.also { this.type = it }
chatRequest.mode.takeIf { it != ChatMode.NONE }?.also { this.mode = it }
}
chatResponse = stub
}
}
}
@@ -0,0 +1,27 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubValidationBadDescription(title: String) = worker {
this.title = title
this.description = """
Кейс ошибки валидации для описания чата
""".trimIndent()
on { stubCase == MessengerStubs.BAD_DESCRIPTION && state == ChatState.RUNNING }
handle {
fail(
ChatError(
group = "validation",
code = "validation-description",
field = "description",
message = "Wrong description field"
)
)
}
}
@@ -0,0 +1,27 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubValidationBadId(title: String) = worker {
this.title = title
this.description = """
Кейс ошибки валидации для идентификатора объявления
""".trimIndent()
on { stubCase == MessengerStubs.BAD_ID && state == ChatState.RUNNING }
handle {
fail(
ChatError(
group = "validation",
code = "validation-id",
field = "id",
message = "Wrong id field"
)
)
}
}
@@ -0,0 +1,27 @@
package ru.otus.messenger.biz.stubs
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.stubValidationBadTitle(title: String) = worker {
this.title = title
this.description = """
Кейс ошибки валидации для заголовка чата
""".trimIndent()
on { stubCase == MessengerStubs.BAD_TITLE && state == ChatState.RUNNING }
handle {
fail(
ChatError(
group = "validation",
code = "validation-title",
field = "title",
message = "Wrong title field"
)
)
}
}
@@ -0,0 +1,23 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.finishChatValidation(title: String) = worker {
this.title = title
on { state == ChatState.RUNNING }
handle {
chatValidated = chatValidating
}
}
fun ICorChainDsl<MessengerContext>.finishChatFilterValidation(title: String) = worker {
this.title = title
on { state == ChatState.RUNNING }
handle {
chatFilterValidated = chatFilterValidating
}
}
@@ -0,0 +1,22 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateDescriptionHasContent(title: String) = worker {
this.title = title
val regExp = Regex("\\p{L}")
on { chatValidating.description.isNotEmpty() && !chatValidating.description.contains(regExp) }
handle {
fail(
errorValidation(
field = "description",
violationCode = "noContent",
description = "field must contain letters"
)
)
}
}
@@ -0,0 +1,21 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateDescriptionNotEmpty(title: String) = worker {
this.title = title
on { chatValidating.description.isEmpty() }
handle {
fail(
errorValidation(
field = "description",
violationCode = "empty",
description = "field must not be empty"
)
)
}
}
@@ -0,0 +1,21 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateIdNotEmpty(title: String) = worker {
this.title = title
on { chatValidating.id.asString().isEmpty() }
handle {
fail(
errorValidation(
field = "id",
violationCode = "empty",
description = "field must not be empty"
)
)
}
}
@@ -0,0 +1,28 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateIdProperFormat(title: String) = worker {
this.title = title
// Может быть вынесен в ChatId для реализации различных форматов
val regExp = Regex("^[0-9a-zA-Z#:-]+$")
on { chatValidating.id != ChatId.NONE && ! chatValidating.id.asString().matches(regExp) }
handle {
val encodedId = chatValidating.id.asString()
.replace("<", "&lt;")
.replace(">", "&gt;")
fail(
errorValidation(
field = "id",
violationCode = "badFormat",
description = "value $encodedId must contain only letters and numbers"
)
)
}
}
@@ -0,0 +1,53 @@
package ru.otus.messenger.biz.validation
import kotlin.collections.first
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.chain
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateSearchStringLength(title: String) = chain {
this.title = title
this.description = """
Валидация длины строки поиска в поисковых фильтрах. Допустимые значения:
- null - не выполняем поиск по строке
- 3-100 - допустимая длина
- больше 100 - слишком длинная строка
""".trimIndent()
on { state == ChatState.RUNNING }
worker("Обрезка пустых символов") {
(chatFilterValidating.searchFields.first() as StringSearchField).stringValue =
(chatFilterValidating.searchFields.first() as StringSearchField).stringValue.trim() }
worker {
this.title = "Проверка кейса длины на 0-2 символа"
this.description = this.title
on { state == ChatState.RUNNING && (chatFilterValidating.searchFields.first() as StringSearchField).stringValue.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 == ChatState.RUNNING && (chatFilterValidating.searchFields.first() as StringSearchField).stringValue.length > 100 }
handle {
fail(
errorValidation(
field = "searchString",
violationCode = "tooLong",
description = "Search string must be no more than 100 symbols long"
)
)
}
}
}
@@ -0,0 +1,26 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateTitleHasContent(title: String) = worker {
this.title = title
this.description = """
Проверяем, что у нас есть какие-то слова в заголовке.
Отказываем в публикации заголовков, в которых только бессмысленные символы типа %^&^$^%#^))&^*&%^^&
""".trimIndent()
val regExp = Regex("\\p{L}")
on { chatValidating.title.isNotEmpty() && ! chatValidating.title.contains(regExp) }
handle {
fail(
errorValidation(
field = "title",
violationCode = "noContent",
description = "field must contain letters"
)
)
}
}
@@ -0,0 +1,21 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorValidation
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.validateTitleNotEmpty(title: String) = worker {
this.title = title
on { chatValidating.title.isEmpty() }
handle {
fail(
errorValidation(
field = "title",
violationCode = "empty",
description = "field must not be empty"
)
)
}
}
@@ -0,0 +1 @@
package ru.otus.messenger.biz
@@ -0,0 +1,94 @@
package ru.otus.messenger.biz.stub
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.stubs.MessengerChatStub
class MessengerCreateStubTest {
private val processor = MessengerProcessor()
private val timestamp = Instant.parse("2025-01-31T06:30:00Z")
private val content = buildJsonObject {
put("sampleId", "uuid4")
put("testParam", "test")
}
@Test
fun create() = runTest {
val context = MessengerContext(
command = ChatCommand.CREATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.SUCCESS,
)
processor.exec(context)
assertEquals(MessengerChatStub.get().title, context.chatResponse.title)
assertEquals(timestamp, context.chatResponse.createdAt)
assertEquals(content.toString(), context.chatResponse.metadata.asString())
}
@Test
fun badTitle() = runTest {
val context = MessengerContext(
command = ChatCommand.CREATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_TITLE,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("title", context.errors.firstOrNull()?.field)
assertEquals("validation", context.errors.firstOrNull()?.group)
}
@Test
fun badDescription() = runTest {
val context = MessengerContext(
command = ChatCommand.CREATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_DESCRIPTION,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("description", context.errors.firstOrNull()?.field)
assertEquals("validation", context.errors.firstOrNull()?.group)
}
@Test
fun databaseError() = runTest {
val context = MessengerContext(
command = ChatCommand.CREATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.DB_ERROR,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("internal", context.errors.firstOrNull()?.group)
}
@Test
fun badNoCase() = runTest {
val context = MessengerContext(
command = ChatCommand.CREATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.NOT_FOUND,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("stub", context.errors.firstOrNull()?.field)
assertEquals("validation", context.errors.firstOrNull()?.group)
}
}
@@ -0,0 +1,75 @@
package ru.otus.messenger.biz.stub
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.stubs.MessengerChatStub
class MessengerDeleteStubTest {
private val processor = MessengerProcessor()
@Test
fun delete() = runTest {
val context = MessengerContext(
command = ChatCommand.DELETE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.SUCCESS,
)
processor.exec(context)
val stub = MessengerChatStub.get()
assertEquals(stub.id, context.chatResponse.id)
assertEquals(stub.title, context.chatResponse.title)
assertEquals(stub.type, context.chatResponse.type)
assertEquals(stub.mode, context.chatResponse.mode)
}
@Test
fun badNoCase() = runTest {
val context = MessengerContext(
command = ChatCommand.DELETE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.NOT_FOUND,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("stub", context.errors.firstOrNull()?.field)
assertEquals("validation", context.errors.firstOrNull()?.group)
}
@Test
fun badId() = runTest {
val context = MessengerContext(
command = ChatCommand.DELETE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_ID
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("id", context.errors.firstOrNull()?.field)
assertEquals("validation", context.errors.firstOrNull()?.group)
}
@Test
fun databaseError() = runTest {
val context = MessengerContext(
command = ChatCommand.DELETE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.DB_ERROR,
)
processor.exec(context)
assertEquals(MessengerChat(), context.chatResponse)
assertEquals("internal", context.errors.firstOrNull()?.group)
}
}
@@ -0,0 +1,76 @@
package ru.otus.messenger.biz.stub
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.stubs.MessengerChatStub
class MessengerReadStubTest {
private val processor = MessengerProcessor()
@Test
fun read() = runTest {
val ctx = MessengerContext(
command = ChatCommand.READ,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.SUCCESS,
)
processor.exec(ctx)
with (MessengerChatStub.get()) {
assertEquals(id, ctx.chatResponse.id)
assertEquals(title, ctx.chatResponse.title)
assertEquals(description, ctx.chatResponse.description)
assertEquals(type, ctx.chatResponse.type)
assertEquals(mode, ctx.chatResponse.mode)
}
}
@Test
fun badId() = runTest {
val ctx = MessengerContext(
command = ChatCommand.READ,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_ID,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("id", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
@Test
fun databaseError() = runTest {
val ctx = MessengerContext(
command = ChatCommand.READ,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.DB_ERROR,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("internal", ctx.errors.firstOrNull()?.group)
}
@Test
fun badNoCase() = runTest {
val ctx = MessengerContext(
command = ChatCommand.READ,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_TITLE,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("stub", ctx.errors.firstOrNull()?.field)
}
}
@@ -0,0 +1,91 @@
package ru.otus.messenger.biz.stub
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.stubs.MessengerChatStub
class MessengerSearchStubTest {
private val processor = MessengerProcessor()
val filter = ChatSearchFilter(
searchFields = listOf(
StringSearchField(fieldName = "title", stringValue = "New chat" ),
StringSearchField(fieldName = "description", stringValue = "New chat description" )
)
)
@Test
fun read() = runTest {
val ctx = MessengerContext(
command = ChatCommand.SEARCH,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.SUCCESS,
chatFilterRequest = filter,
)
processor.exec(ctx)
assertTrue(ctx.chatsResponse.size > 1)
val first = ctx.chatsResponse.firstOrNull() ?: fail("Empty response list")
assertTrue(first.title.contains((filter.searchFields[0] as StringSearchField).stringValue))
assertTrue(first.description.contains((filter.searchFields[1] as StringSearchField).stringValue))
with(MessengerChatStub.get()) {
assertEquals(type, first.type)
assertEquals(mode, first.mode)
}
}
@Test
fun badId() = runTest {
val ctx = MessengerContext(
command = ChatCommand.SEARCH,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_ID,
chatFilterRequest = filter,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("id", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
@Test
fun databaseError() = runTest {
val ctx = MessengerContext(
command = ChatCommand.SEARCH,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.DB_ERROR,
chatFilterRequest = filter,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("internal", ctx.errors.firstOrNull()?.group)
}
@Test
fun badNoCase() = runTest {
val ctx = MessengerContext(
command = ChatCommand.SEARCH,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_TITLE,
chatFilterRequest = filter,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("stub", ctx.errors.firstOrNull()?.field)
}
}
@@ -0,0 +1,117 @@
package ru.otus.messenger.biz.stub
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.stubs.MessengerChatStubSample
class MessengerUpdateStubTest {
private val processor = MessengerProcessor()
val id = ChatId(MessengerChatStubSample.chatId)
val title = "New chat"
val description = "New chat description"
val type = ChatType.GROUP
val mode = ChatMode.PERSONAL
@Test
fun create() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.SUCCESS
)
processor.exec(ctx)
assertEquals(id, ctx.chatResponse.id)
assertEquals(title, ctx.chatResponse.title)
assertEquals(description, ctx.chatResponse.description)
assertEquals(type, ctx.chatResponse.type)
assertEquals(mode, ctx.chatResponse.mode)
}
@Test
fun badId() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_ID,
chatRequest = MessengerChat(),
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("id", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
@Test
fun badTitle() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_TITLE,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("title", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
@Test
fun badDescription() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_DESCRIPTION,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("description", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
@Test
fun databaseError() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.DB_ERROR,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("internal", ctx.errors.firstOrNull()?.group)
}
@Test
fun badNoCase() = runTest {
val ctx = MessengerContext(
command = ChatCommand.UPDATE,
state = ChatState.NONE,
workMode = WorkMode.STUB,
stubCase = MessengerStubs.BAD_SEARCH_STRING,
)
processor.exec(ctx)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals("stub", ctx.errors.firstOrNull()?.field)
assertEquals("validation", ctx.errors.firstOrNull()?.group)
}
}
@@ -0,0 +1,11 @@
package ru.otus.messenger.biz.validation
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatCommand
abstract class BaseBizValidationTest {
protected abstract val command: ChatCommand
private val settings by lazy { MessengerCorSettings() }
protected val processor by lazy { MessengerProcessor(settings) }
}
@@ -0,0 +1,18 @@
package ru.otus.messenger.biz.validation
import kotlin.test.Test
import ru.otus.messenger.common.models.ChatCommand
class BizValidationCreateTest : BaseBizValidationTest() {
override val command: ChatCommand = ChatCommand.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)
}
@@ -0,0 +1,13 @@
package ru.otus.messenger.biz.validation
import org.junit.Test
import ru.otus.messenger.common.models.ChatCommand
class BizValidationDeleteTest : BaseBizValidationTest() {
override val command: ChatCommand = ChatCommand.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)
}
@@ -0,0 +1,13 @@
package ru.otus.messenger.biz.validation
import org.junit.Test
import ru.otus.messenger.common.models.ChatCommand
class BizValidationReadTest : BaseBizValidationTest() {
override val command: ChatCommand = ChatCommand.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)
}
@@ -0,0 +1,36 @@
package ru.otus.messenger.biz.validation
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.WorkMode
class BizValidationSearchTest : BaseBizValidationTest() {
override val command: ChatCommand = ChatCommand.SEARCH
@Test
fun correctEmpty() = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatFilterRequest = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "",
stringValue = ""
)
),
)
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
}
}
@@ -0,0 +1,23 @@
package ru.otus.messenger.biz.validation
import org.junit.Test
import ru.otus.messenger.common.models.ChatCommand
class BizValidationUpdateTest : BaseBizValidationTest() {
override val command: ChatCommand = ChatCommand.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)
}
@@ -0,0 +1,110 @@
package ru.otus.messenger.biz.validation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.cor.dsl.rootChain
class ValidateSearchStringLengthTest {
@Test
fun emptyString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = "",
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.RUNNING, ctx.state)
assertEquals(0, ctx.errors.size)
}
@Test
fun blankString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = " ",
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.RUNNING, ctx.state)
assertEquals(0, ctx.errors.size)
}
@Test
fun shortString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = "12",
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.FAILING, ctx.state)
assertEquals(1, ctx.errors.size)
assertEquals("validation-searchString-tooShort", ctx.errors.first().code)
}
@Test
fun normalString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = "123",
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.RUNNING, ctx.state)
assertEquals(0, ctx.errors.size)
}
@Test
fun longString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = "12".repeat(51),
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.FAILING, ctx.state)
assertEquals(1, ctx.errors.size)
assertEquals("validation-searchString-tooLong", ctx.errors.first().code)
}
companion object {
val chain = rootChain {
validateSearchStringLength("")
}.build()
}
}
@@ -0,0 +1,55 @@
package ru.otus.messenger.biz.validation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatSearchFilter.StringSearchField
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.cor.dsl.rootChain
class ValidateTitleHasContentTest {
@Test
fun emptyString() = runTest {
val ctx = MessengerContext(state = ChatState.RUNNING, chatValidating = MessengerChat(title = ""))
chain.exec(ctx)
assertEquals(ChatState.RUNNING, ctx.state)
assertEquals(0, ctx.errors.size)
}
@Test
fun noContent() = runTest {
val ctx =
MessengerContext(state = ChatState.RUNNING, chatValidating = MessengerChat(title = "12!@#$%^&*()_+-="))
chain.exec(ctx)
assertEquals(ChatState.FAILING, ctx.state)
assertEquals(1, ctx.errors.size)
assertEquals("validation-title-noContent", ctx.errors.first().code)
}
@Test
fun normalString() = runTest {
val ctx = MessengerContext(
state = ChatState.RUNNING,
chatFilterValidating = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "title",
stringValue = "Ж"
)
)
)
)
chain.exec(ctx)
assertEquals(ChatState.RUNNING, ctx.state)
assertEquals(0, ctx.errors.size)
}
companion object {
val chain = rootChain {
validateTitleHasContent("")
}.build()
}
}
@@ -0,0 +1,75 @@
package ru.otus.messenger.biz.validation
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.stubs.MessengerChatStub
fun validationDescriptionCorrect(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.get(),
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
assertContains(ctx.chatValidated.description, "description")
}
fun validationDescriptionTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
description = " \n\tabc \n\t"
},
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
assertEquals("abc", ctx.chatValidated.description)
}
fun validationDescriptionEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
description = ""
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("description", error?.field)
assertContains(error?.message ?: "", "description")
}
fun validationDescriptionSymbols(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
description = "!@#$%^&*(),.{}"
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("description", error?.field)
assertContains(error?.message ?: "", "description")
}
@@ -0,0 +1,73 @@
package ru.otus.messenger.biz.validation
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.stubs.MessengerChatStub
fun validationIdCorrect(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.get(),
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
}
fun validationIdTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
id = ChatId(" \n\t ${id.asString()} \n\t ")
},
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
}
fun validationIdEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
id = ChatId("")
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("id", error?.field)
assertContains(error?.message ?: "", "id")
}
fun validationIdFormat(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
id = ChatId("!@#\$%^&*(),.{}")
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("id", error?.field)
assertContains(error?.message ?: "", "id")
}
@@ -0,0 +1,76 @@
package ru.otus.messenger.biz.validation
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatCommand
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.stubs.MessengerChatStub
fun validationTitleCorrect(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
title = "abc"
},
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
assertEquals("abc", ctx.chatValidated.title)
}
fun validationTitleTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
title = " \n\t abc \t\n "
},
)
processor.exec(ctx)
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
assertEquals("abc", ctx.chatValidated.title)
}
fun validationTitleEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
title = ""
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("title", error?.field)
assertContains(error?.message ?: "", "title")
}
fun validationTitleSymbols(command: ChatCommand, processor: MessengerProcessor) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChatStub.prepareResult {
title = "!@#$%^&*(),.{}"
},
)
processor.exec(ctx)
assertEquals(1, ctx.errors.size)
assertEquals(ChatState.FAILING, ctx.state)
val error = ctx.errors.firstOrNull()
assertEquals("title", error?.field)
assertContains(error?.message ?: "", "title")
}
@@ -2,7 +2,7 @@ package ru.otus.messenger.common
import kotlinx.datetime.Instant
import ru.otus.messenger.common.models.*
import ru.otus.messenger.common.stubs.Stubs
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.common.ws.IMessengerWsSession
data class MessengerContext(
@@ -10,8 +10,9 @@ data class MessengerContext(
var state: ChatState = ChatState.NONE,
val errors: MutableList<ChatError> = mutableListOf(),
var corSettings: MessengerCorSettings = MessengerCorSettings(),
var workMode: WorkMode = WorkMode.PROD,
var stubCase: Stubs = Stubs.NONE,
var stubCase: MessengerStubs = MessengerStubs.NONE,
var wsSession: IMessengerWsSession = IMessengerWsSession.NONE,
var requestId: RequestId = RequestId.NONE,
@@ -19,6 +20,12 @@ data class MessengerContext(
var chatRequest: MessengerChat = MessengerChat(),
var chatFilterRequest: ChatSearchFilter = ChatSearchFilter.NONE,
var chatValidating: MessengerChat = MessengerChat(),
var chatFilterValidating: ChatSearchFilter = ChatSearchFilter.NONE,
var chatValidated: MessengerChat = MessengerChat(),
var chatFilterValidated: ChatSearchFilter = ChatSearchFilter.NONE,
var chatResponse: MessengerChat = MessengerChat(),
var chatsResponse: MutableList<MessengerChat> = mutableListOf(),
)
@@ -0,0 +1,12 @@
package ru.otus.messenger.common.helpers
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatState
fun MessengerContext.addError(vararg error: ChatError) = errors.addAll(error)
fun MessengerContext.fail(error: ChatError) {
addError(error)
state = ChatState.FAILING
}
@@ -1,6 +1,7 @@
package ru.otus.messenger.common.helpers
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.logging.common.LogLevel
fun Throwable.asMessengerError(
code: String = "unknown",
@@ -12,4 +13,17 @@ fun Throwable.asMessengerError(
field = "",
message = message,
exception = this,
)
fun errorValidation(
field: String,
violationCode: String,
description: String,
level: LogLevel = LogLevel.ERROR,
) = ChatError(
code = "validation-$field-$violationCode",
field = field,
group = "validation",
message = "Validation error for field $field: $description",
level = level,
)
@@ -6,9 +6,14 @@ data class ChatSearchFilter(
var type: ChatType = ChatType.NONE,
var mode: ChatMode = ChatMode.NONE,
) {
fun deepCopy(): ChatSearchFilter = copy(
searchFields = searchFields.toMutableList().toList()
)
interface SearchField {
val fieldName: String
val action: SearchAction
fun deepCopy(fieldName: String? = null): SearchField
}
enum class SearchAction {
@@ -21,10 +26,24 @@ data class ChatSearchFilter(
data class StringSearchField(
override val fieldName: String,
override val action: SearchAction = SearchAction.CONTAINS,
val stringValue: String,
) : SearchField
var stringValue: String,
) : SearchField {
override fun deepCopy(fieldName: String?): SearchField {
fieldName?.let {
return this.copy(fieldName = it)
}
return this.copy()
}
}
companion object {
val NONE = ChatSearchFilter()
val NONE = ChatSearchFilter(
searchFields = listOf(
StringSearchField(
fieldName = "",
stringValue = ""
)
)
)
}
}
@@ -18,6 +18,8 @@ data class MessengerChat(
) {
fun isEmpty() = this == NONE
fun deepCopy(): MessengerChat = copy()
companion object {
private val NONE = MessengerChat()
}
@@ -1,11 +1,16 @@
package ru.otus.messenger.common.stubs
enum class Stubs {
enum class MessengerStubs {
NONE,
SUCCESS,
NOT_FOUND,
BAD_ID,
BAD_TITLE,
BAD_DESCRIPTION,
BAD_ARCHIVED,
CANNOT_DELETE,
MISSING_DATA,
VALUE_ERROR,
BAD_SEARCH_STRING,
DB_ERROR,
}
@@ -1,6 +1,5 @@
package ru.otus.messenger.stubs
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.stubs.MessengerChatStubSample.CHAT_SAMPLE_1
@@ -14,9 +13,8 @@ object MessengerChatStub {
fun prepareSearchList(
chatTitle: String,
chatType: ChatType,
chatMode: ChatMode,
) = listOf(
CHAT_SAMPLE_1,
CHAT_SAMPLE_2
).filter { it.title == chatTitle && it.type == chatType && it.mode == chatMode }
).filter { it.title == chatTitle && it.type == chatType }
}
@@ -0,0 +1,11 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.coroutines)
testImplementation(kotlin("test-junit"))
testImplementation(libs.kotlin.coroutines.test)
}
@@ -0,0 +1,4 @@
package ru.otus.messenger.cor
@DslMarker
annotation class CorDslMarker
@@ -0,0 +1,10 @@
package ru.otus.messenger.cor
/**
* Блок кода, который обрабатывает контекст. Имеет имя и описание
*/
interface ICorExec<T> {
val title: String
val description: String
suspend fun exec(context: T)
}
@@ -0,0 +1,22 @@
package ru.otus.messenger.cor.dsl
import ru.otus.messenger.cor.CorDslMarker
import ru.otus.messenger.cor.ICorExec
import ru.otus.messenger.cor.handlers.CorChain
@CorDslMarker
class CorChainDsl<T>(
) : CorExecDsl<T>(), ICorChainDsl<T> {
private val workers: MutableList<ICorExecDsl<T>> = mutableListOf()
override fun add(worker: ICorExecDsl<T>) {
workers.add(worker)
}
override fun build(): ICorExec<T> = CorChain(
title = title,
description = description,
execs = workers.map { it.build() },
blockOn = blockOn,
blockExcept = blockExcept
)
}
@@ -0,0 +1,51 @@
package ru.otus.messenger.cor.dsl
/**
* Точка входа в dsl построения цепочек.
* Элементы исполняются последовательно.
*
* Пример:
* ```
* rootChain<SomeContext> {
* worker {
* }
* chain {
* worker(...) {
* }
* worker(...) {
* }
* }
* }
* ```
*/
fun <T> rootChain(function: ICorChainDsl<T>.() -> Unit): ICorChainDsl<T> = CorChainDsl<T>().apply(function)
/**
* Создает цепочку, элементы которой исполняются последовательно.
*/
fun <T> ICorChainDsl<T>.chain(function: ICorChainDsl<T>.() -> Unit) {
add(CorChainDsl<T>().apply(function))
}
/**
* Создает рабочего
*/
fun <T> ICorChainDsl<T>.worker(function: ICorWorkerDsl<T>.() -> Unit) {
add(CorWorkerDsl<T>().apply(function))
}
/**
* Создает рабочего с on и except по умолчанию
*/
fun <T> ICorChainDsl<T>.worker(
title: String,
description: String = "",
blockHandle: T.() -> Unit
) {
add(CorWorkerDsl<T>().also {
it.title = title
it.description = description
it.handle(blockHandle)
})
}
@@ -0,0 +1,17 @@
package ru.otus.messenger.cor.dsl
abstract class CorExecDsl<T> : ICorExecDsl<T> {
protected var blockOn: suspend T.() -> Boolean = { true }
protected var blockExcept: suspend T.(e: Throwable) -> Unit = { e: Throwable -> throw e }
override var title: String = ""
override var description: String = ""
override fun on(function: suspend T.() -> Boolean) {
blockOn = function
}
override fun except(function: suspend T.(e: Throwable) -> Unit) {
blockExcept = function
}
}
@@ -0,0 +1,21 @@
package ru.otus.messenger.cor.dsl
import ru.otus.messenger.cor.CorDslMarker
import ru.otus.messenger.cor.ICorExec
import ru.otus.messenger.cor.handlers.CorWorker
@CorDslMarker
class CorWorkerDsl<T> : CorExecDsl<T>(), ICorWorkerDsl<T> {
private var blockHandle: suspend T.() -> Unit = {}
override fun handle(function: suspend T.() -> Unit) {
blockHandle = function
}
override fun build(): ICorExec<T> = CorWorker(
title = title,
description = description,
blockOn = blockOn,
blockHandle = blockHandle,
blockExcept = blockExcept
)
}
@@ -0,0 +1,11 @@
package ru.otus.messenger.cor.dsl
import ru.otus.messenger.cor.CorDslMarker
/**
* Билдер (dsl) для цепочек (chain)
*/
@CorDslMarker
interface ICorChainDsl<T> : ICorExecDsl<T> {
fun add(worker: ICorExecDsl<T>)
}
@@ -0,0 +1,17 @@
package ru.otus.messenger.cor.dsl
import ru.otus.messenger.cor.CorDslMarker
import ru.otus.messenger.cor.ICorExec
/**
* Базовый билдер (dsl)
*/
@CorDslMarker
interface ICorExecDsl<T> {
var title: String
var description: String
fun on(function: suspend T.() -> Boolean)
fun except(function: suspend T.(e: Throwable) -> Unit)
fun build(): ICorExec<T>
}
@@ -0,0 +1,11 @@
package ru.otus.messenger.cor.dsl
import ru.otus.messenger.cor.CorDslMarker
/**
* Билдер (dsl) для рабочих (worker)
*/
@CorDslMarker
interface ICorWorkerDsl<T> : ICorExecDsl<T> {
fun handle(function: suspend T.() -> Unit)
}
@@ -0,0 +1,25 @@
package ru.otus.messenger.cor.handlers
import ru.otus.messenger.cor.ICorExec
abstract class AbstractCorExec<T>(
override val title: String,
override val description: String = "",
private val blockOn: suspend T.() -> Boolean = { true },
private val blockExcept: suspend T.(Throwable) -> Unit = {},
): ICorExec<T> {
protected abstract suspend fun handle(context: T)
private suspend fun on(context: T): Boolean = context.blockOn()
private suspend fun except(context: T, e: Throwable) = context.blockExcept(e)
override suspend fun exec(context: T) {
if (on(context)) {
try {
handle(context)
} catch (e: Throwable) {
except(context, e)
}
}
}
}
@@ -0,0 +1,20 @@
package ru.otus.messenger.cor.handlers
import ru.otus.messenger.cor.ICorExec
/**
* Реализация цепочки (chain), которая исполняет свои вложенные цепочки и рабочие
*/
class CorChain<T>(
private val execs: List<ICorExec<T>>,
title: String,
description: String = "",
blockOn: suspend T.() -> Boolean = { true },
blockExcept: suspend T.(Throwable) -> Unit = {},
) : AbstractCorExec<T>(title, description, blockOn, blockExcept) {
override suspend fun handle(context: T) {
execs.forEach {
it.exec(context)
}
}
}
@@ -0,0 +1,11 @@
package ru.otus.messenger.cor.handlers
class CorWorker<T>(
title: String,
description: String = "",
blockOn: suspend T.() -> Boolean = { true },
private val blockHandle: suspend T.() -> Unit = {},
blockExcept: suspend T.(Throwable) -> Unit = {},
) : AbstractCorExec<T>(title, description, blockOn, blockExcept) {
override suspend fun handle(context: T) = blockHandle(context)
}
@@ -0,0 +1,27 @@
package ru.otus.messenger.cor
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.cor.handlers.CorChain
import ru.otus.messenger.cor.handlers.CorWorker
class CorChainTest {
@Test
fun `chain should execute workers`() = runTest {
val createWorker = { title: String ->
CorWorker<TestContext>(
title = title,
blockOn = { status == TestContext.CorStatus.NONE },
blockHandle = { history += "$title; " }
)
}
val chain = CorChain<TestContext>(
execs = listOf(createWorker("w1"), createWorker("w2")),
title = "chain",
)
val ctx = TestContext()
chain.exec(ctx)
assertEquals("w1; w2; ", ctx.history)
}
}
@@ -0,0 +1,109 @@
package ru.otus.messenger.cor
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.ICorExecDsl
import ru.otus.messenger.cor.dsl.chain
import ru.otus.messenger.cor.dsl.rootChain
import ru.otus.messenger.cor.dsl.worker
class CorDslTest {
private suspend fun execute(dsl: ICorExecDsl<TestContext>): TestContext {
val ctx = TestContext()
dsl.build().exec(ctx)
return ctx
}
@Test
fun `handle should execute`() = runTest {
val chain = rootChain<TestContext> {
worker {
handle { history += "w1; " }
}
}
val ctx = execute(chain)
assertEquals("w1; ", ctx.history)
}
@Test
fun `on should check condition`() = runTest {
val chain = rootChain<TestContext> {
worker {
on { status == TestContext.CorStatus.ERROR }
handle { history += "w1; " }
}
worker {
on { status == TestContext.CorStatus.NONE }
handle {
history += "w2; "
status = TestContext.CorStatus.FAILING
}
}
worker {
on { status == TestContext.CorStatus.FAILING }
handle { history += "w3; " }
}
}
assertEquals("w2; w3; ", execute(chain).history)
}
@Test
fun `except should execute when exception`() = runTest {
val chain = rootChain<TestContext> {
worker {
handle { throw RuntimeException("some error") }
except { history += it.message }
}
}
assertEquals("some error", execute(chain).history)
}
@Test
fun `should throw when exception and no except`() = runTest {
val chain = rootChain<TestContext> {
worker("throw") { throw RuntimeException("some error") }
}
assertFails {
execute(chain)
}
}
@Test
fun `complex chain example`() = runTest {
val chain = rootChain<TestContext> {
worker {
title = "Инициализация статуса"
description = "При старте обработки цепочки, статус еще не установлен. Проверяем его"
on { status == TestContext.CorStatus.NONE }
handle { status = TestContext.CorStatus.RUNNING }
except { status = TestContext.CorStatus.ERROR }
}
chain {
on { status == TestContext.CorStatus.RUNNING }
worker(
title = "Лямбда обработчик",
description = "Пример использования обработчика в виде лямбды"
) {
some += 4
}
}
printResult()
}.build()
val ctx = TestContext()
chain.exec(ctx)
println("Complete: $ctx")
}
private fun ICorChainDsl<TestContext>.printResult() = worker(title = "Print example") {
println("some = $some")
}
}
@@ -0,0 +1,44 @@
package ru.otus.messenger.cor
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import ru.otus.messenger.cor.handlers.CorWorker
class CorWorkerTest {
@Test
fun `worker should execute handle`() = runTest {
val worker = CorWorker<TestContext>(
title = "w1",
blockHandle = { history += "w1; " }
)
val ctx = TestContext()
worker.exec(ctx)
assertEquals("w1; ", ctx.history)
}
@Test
fun `worker should not execute when off`() = runTest {
val worker = CorWorker<TestContext>(
title = "w1",
blockOn = { status == TestContext.CorStatus.ERROR },
blockHandle = { history += "w1; " }
)
val ctx = TestContext()
worker.exec(ctx)
assertEquals("", ctx.history)
}
@Test
fun `worker should handle exception`() = runTest {
val worker = CorWorker<TestContext>(
title = "w1",
blockHandle = { throw RuntimeException("some error") },
blockExcept = { e -> history += e.message }
)
val ctx = TestContext()
worker.exec(ctx)
assertEquals("some error", ctx.history)
}
}
@@ -0,0 +1,14 @@
package ru.otus.messenger.cor
data class TestContext(
var status: CorStatus = CorStatus.NONE,
var some: Int = 0,
var history: String = "",
) {
enum class CorStatus {
NONE,
RUNNING,
FAILING,
ERROR
}
}
+2 -1
View File
@@ -23,4 +23,5 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
include(":ok-messenger-lib-logging")
include(":ok-messenger-lib-logging")
include(":ok-messenger-lib-cor")