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

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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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)

View File

@ -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)

View File

@ -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()
}

View File

@ -16,7 +16,7 @@ class RequestV1SerializationTest {
title = "New chat",
type = ChatCreateRequestAllOfChat.Type.GROUP,
mode = ChatCreateRequestAllOfChat.Mode.PERSONAL,
participants = emptyList(),
participants = emptySet(),
metadata = """
{
"isOwner": true,

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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
}
}
}

View File

@ -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"
)
)
}
}

View File

@ -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
}
}
}

View File

@ -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}"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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
}
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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
}
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}
}

View File

@ -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"
)
)
}
}

View File

@ -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"
)
)
}
}

View File

@ -0,0 +1 @@
package ru.otus.messenger.biz

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) }
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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(),
)

View File

@ -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
}

View File

@ -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,
)

View File

@ -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 = ""
)
)
)
}
}

View File

@ -18,6 +18,8 @@ data class MessengerChat(
) {
fun isEmpty() = this == NONE
fun deepCopy(): MessengerChat = copy()
companion object {
private val NONE = MessengerChat()
}

View File

@ -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,
}

View File

@ -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 }
}

View File

@ -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)
}

View File

@ -0,0 +1,4 @@
package ru.otus.messenger.cor
@DslMarker
annotation class CorDslMarker

View File

@ -0,0 +1,10 @@
package ru.otus.messenger.cor
/**
* Блок кода, который обрабатывает контекст. Имеет имя и описание
*/
interface ICorExec<T> {
val title: String
val description: String
suspend fun exec(context: T)
}

View File

@ -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
)
}

View File

@ -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)
})
}

View File

@ -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
}
}

View File

@ -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
)
}

View File

@ -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>)
}

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

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")