module 7 lesson 1

This commit is contained in:
Александр Веденёв
2025-04-18 02:05:36 +07:00
parent a1ffa0c893
commit 97575168ea
99 changed files with 2712 additions and 30 deletions

View File

@ -36,6 +36,11 @@ muschko = "9.4.0"
jvm-compiler = "17"
jvm-language = "21"
uuid = "0.8.4"
db-cache = "0.13.0"
clickhouse-client = "0.8.1"
mockk = "1.13.10"
[plugins]
jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@ -108,4 +113,12 @@ kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
logger-fluentd = { module = "org.fluentd:fluent-logger", version.ref = "fluentd" }
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-binaryCompatibilityValidator = "org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2"
plugin-binaryCompatibilityValidator = "org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2"
# DB repo
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
db-cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "db-cache" }
# DB
clickhouse-client = { module = "com.clickhouse:client-v2", version.ref = "clickhouse-client"}
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

View File

@ -3,7 +3,7 @@ headers = curl/curl.h
# You also need to specify linking parameters for different platforms
compilerOpts.linux_x64 = -I/usr/include -I/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk/usr/include/
linkerOpts.osx = -L/opt/homebrew/Cellar/curl/8.11.1/lib -L/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk/usr/include/ -lcurl
linkerOpts.osx = -L/opt/homebrew/Cellar/curl/8.13.3/lib -L/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk/usr/include/ -lcurl
linkerOpts.linux_x64 = -L/Library/Developer/CommandLineTools/SDKs/MacOSX15.sdk/usr/include/ -lcurl
---

View File

@ -14,13 +14,13 @@ fun MessengerContext.toLog(logId: String) = CommonLogModel(
)
private fun MessengerContext.toChatLog(): ChatLogModel? {
val emptyReport = MessengerChat()
val emptyChat = MessengerChat()
return ChatLogModel(
requestId = requestId.takeIf { it != RequestId.NONE }?.asString(),
requestChat = chatRequest.takeIf { it != emptyReport }?.toLog(),
requestChat = chatRequest.takeIf { it != emptyChat }?.toLog(),
requestSearch = chatFilterRequest.takeIf { it != ChatSearchFilter.NONE }?.toLog(),
responseChat = chatResponse.takeIf { it != emptyReport }?.toLog(),
responseChats = chatsResponse.takeIf { it.isNotEmpty() }?.filter { it != emptyReport }?.map { it.toLog() },
responseChat = chatResponse.takeIf { it != emptyChat }?.toLog(),
responseChats = chatsResponse.takeIf { it.isNotEmpty() }?.filter { it != emptyChat }?.map { it.toLog() },
).takeIf { it != ChatLogModel() }
}

View File

@ -0,0 +1,38 @@
package ru.otus.messenger.api.v1.mappers
import kotlin.collections.map
import kotlin.collections.toSet
import ru.otus.messenger.api.v1.models.ChatCreateRequestAllOfChat
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMetadata
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
fun MessengerChat.toTransportCreate() = ChatCreateRequestAllOfChat(
title = title.takeIf { it.isNotBlank() },
description = description.takeIf { it.isNotBlank() },
type = type.takeIf { it != ChatType.NONE }?.toTransportChat(),
mode = mode.takeIf { it != ChatMode.NONE }?.toTransportChat(),
ownerId = ownerId.takeIf { it != ChatOwnerId.NONE }?.asString(),
participants = participants.map { it.asString() }.toSet().takeIf { it.isNotEmpty() },
metadata = metadata.takeIf { it != ChatMetadata.NONE }?.asString()
)
fun MessengerChat.toTransportRead() = id.takeIf { it != ChatId.NONE }?.asString()
fun MessengerChat.toTransportDelete() = id.takeIf { it != ChatId.NONE }?.asString()
private fun ChatType.toTransportChat() = when (this) {
ChatType.PRIVATE -> ChatCreateRequestAllOfChat.Type.PRIVATE
ChatType.GROUP -> ChatCreateRequestAllOfChat.Type.GROUP
ChatType.CHANNEL -> ChatCreateRequestAllOfChat.Type.CHANNEL
ChatType.NONE -> null
}
private fun ChatMode.toTransportChat() = when (this) {
ChatMode.PERSONAL -> ChatCreateRequestAllOfChat.Mode.PERSONAL
ChatMode.WORK -> ChatCreateRequestAllOfChat.Mode.WORK
ChatMode.NONE -> null
}

View File

@ -22,6 +22,8 @@ jib {
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.cors)
@ -45,6 +47,11 @@ dependencies {
// stubs
implementation(project(":ok-messenger-stubs"))
// database
implementation(project(":ok-messenger-repo-stubs"))
implementation(project(":ok-messenger-repo-inmemory"))
implementation(project(":ok-messenger-repo-clickhouse"))
// logging
implementation(project(":ok-messenger-api-log-v1"))
implementation("ru.otus.messenger.libs:ok-messenger-lib-logging")
@ -52,4 +59,6 @@ dependencies {
testImplementation(kotlin("test-junit"))
testImplementation(libs.ktor.server.test)
testImplementation(libs.ktor.client.negotiation)
testImplementation(libs.mockk)
testImplementation(project(":ok-messenger-repo-common"))
}

View File

@ -27,6 +27,7 @@ fun Application.module(
anyHost()
}
configureHTTP()
configureMonitoring()
configureSerialization()
configureRouting(appSettings)

View File

@ -0,0 +1,21 @@
package ru.otus.messenger.app.configs
import io.ktor.server.config.ApplicationConfig
data class ClickHouseConfig(
val host: String = "localhost",
val port: Int = 8443,
val user: String = "default",
val password: String = "",
) {
constructor(config: ApplicationConfig): this(
host = config.propertyOrNull("$PATH.host")?.getString() ?: "localhost",
port = config.propertyOrNull("$PATH.port")?.getString()?.toIntOrNull() ?: 8443,
user = config.propertyOrNull("$PATH.user")?.getString() ?: "default",
password = config.property("$PATH.password").getString(),
)
companion object {
const val PATH = "${ConfigPaths.REPOSITORY}.db"
}
}

View File

@ -0,0 +1,6 @@
package ru.otus.messenger.app.configs
object ConfigPaths {
const val ROOT = "messenger"
const val REPOSITORY = "$ROOT.repository"
}

View File

@ -0,0 +1,47 @@
package ru.otus.messenger.app.plugins
import io.ktor.server.application.*
import ru.otus.messenger.app.configs.ClickHouseConfig
import ru.otus.messenger.app.configs.ConfigPaths
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import ru.otus.messenger.repo.clickhouse.ChatRepoClickHouse
import ru.otus.messenger.repo.clickhouse.DbProperties
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.repo.inmemory.ChatRepoInMemory
fun Application.getDatabaseConf(type: DbType): IRepoChat{
val dbSettingPath = "${ConfigPaths.REPOSITORY}.${type.confName}"
val dbSetting = environment.config.propertyOrNull(dbSettingPath)?.getString()?.lowercase()
return when (dbSetting) {
"in-memory", "inmemory", "memory", "mem" -> initInMemory()
"db", "clickhouse" -> initClickHouse()
else -> throw IllegalArgumentException(
"$dbSettingPath must be set in application.yml to one of: 'inmemory', 'clickhouse'"
)
}
}
enum class DbType(val confName: String) {
PROD("prod"),
TEST("test")
}
fun Application.initInMemory(): IRepoChat {
val ttlSetting = environment.config.propertyOrNull("db.prod")?.getString()?.let {
Duration.parse(it)
}
return ChatRepoInMemory(ttl = ttlSetting ?: 10.minutes)
}
fun Application.initClickHouse(): IRepoChat {
val config = ClickHouseConfig(environment.config)
return ChatRepoClickHouse(
properties = DbProperties(
host = config.host,
port = config.port,
user = config.user,
password = config.password,
)
)
}

View File

@ -1,14 +1,20 @@
package ru.otus.messenger.app.plugins
import io.ktor.server.application.*
import ru.otus.messenger.app.base.KtorWsSessionRepo
import ru.otus.messenger.app.common.MessengerAppSettings
import ru.otus.messenger.app.common.MessengerAppSettingsData
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.repo.stub.ChatRepoStub
fun Application.initAppSettings(): MessengerAppSettings {
val corSettings = MessengerCorSettings(
loggerProvider = getLoggerProviderConf(),
wsSessions = KtorWsSessionRepo(),
repoTest = getDatabaseConf(DbType.TEST),
repoProd = getDatabaseConf(DbType.PROD),
repoStub = ChatRepoStub(),
)
return MessengerAppSettingsData(
appUrls = environment.config.propertyOrNull("ktor.urls")?.getList() ?: emptyList(),

View File

@ -7,4 +7,15 @@ ktor:
- resources
application:
modules:
- ru.otus.messenger.app.ApplicationKt.module
- ru.otus.messenger.app.ApplicationKt.module
logger: logback
messenger:
repository:
test: "inmemory"
prod: "$DB_TYPE_PROD:inmemory"
db:
host: "$DB_HOST:localhost"
port: "$DB_PORT:8443"
user: "$DB_USER:default"
password: "$DB_PASS:pass"

View File

@ -5,13 +5,15 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.testApplication
import kotlin.test.assertEquals
import org.junit.Test
import ru.otus.messenger.app.common.MessengerAppSettingsData
import ru.otus.messenger.common.MessengerCorSettings
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
module()
module(MessengerAppSettingsData(corSettings = MessengerCorSettings()))
}
client.get("/").apply {
assertEquals(HttpStatusCode.Companion.OK, status)

View File

@ -45,7 +45,7 @@ class ControllerTest {
}
}
private suspend fun TestApplicationCall.createReport(appSettings: MessengerAppSettings) {
private suspend fun TestApplicationCall.createChat(appSettings: MessengerAppSettings) {
val response = appSettings.controllerHelper(
{ fromTransport(receive<ChatCreateRequest>()) },
{ toTransportChat() },
@ -57,7 +57,7 @@ class ControllerTest {
@Test
fun ktorHelperTest() = runTest {
val testApp = TestApplicationCall(request).apply { createReport(appSettings) }
val testApp = TestApplicationCall(request).apply { createChat(appSettings) }
val response = testApp.response as ChatCreateResponse
assertEquals(ResponseResult.SUCCESS, response.result)
}

View File

@ -0,0 +1,126 @@
package ru.otus.messenger.app.repo
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
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 ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.stubs.MessengerChatStub
import ru.otus.messenger.app.module
import ru.otus.messenger.app.common.MessengerAppSettings
import ru.otus.messenger.api.v1.models.ChatCreateResponse
import ru.otus.messenger.api.v1.models.DebugMode
import ru.otus.messenger.api.v1.mappers.toTransportCreate
import ru.otus.messenger.api.v1.mappers.toTransportDelete
import ru.otus.messenger.api.v1.mappers.toTransportRead
import ru.otus.messenger.api.v1.models.*
abstract class V1ChatRepoBaseTest {
abstract val workMode: DebugMode
abstract val appSettingsCreate: MessengerAppSettings
abstract val appSettingsRead: MessengerAppSettings
abstract val appSettingsDelete: MessengerAppSettings
abstract val appSettingsSearch: MessengerAppSettings
abstract val appSettingsResume: MessengerAppSettings
protected val uuidOld = "10000000-0000-0000-0000-000000000001"
protected val uuidNew = "10000000-0000-0000-0000-000000000002"
protected val initChat = MessengerChatStub.prepareResult {
id = ChatId(uuidOld)
}
@Test
fun create() {
val chat = initChat.toTransportCreate()
v1TestApplication(
settings = appSettingsCreate,
endpoint = "create",
request = ChatCreateRequest(
chat = chat,
debug = Debug(mode = workMode),
),
) { response ->
val responseObj = response.body<ChatCreateResponse>()
assertEquals(200, response.status.value)
assertEquals(uuidNew, responseObj.chat?.id)
assertEquals(chat.ownerId, responseObj.chat?.ownerId)
assertEquals(chat.title, responseObj.chat?.title)
assertEquals(chat.description, responseObj.chat?.description)
}
}
@Test
fun read() {
val chat = initChat.toTransportRead()
v1TestApplication(
settings = appSettingsRead,
endpoint = "read",
request = ChatReadRequest(
chatId = chat,
debug = Debug(mode = workMode),
),
) { response ->
val responseObj = response.body<IResponse>() as ChatReadResponse
assertEquals(200, response.status.value)
assertEquals(uuidOld, responseObj.chat?.id)
}
}
@Test
fun delete() {
val chat = initChat.toTransportDelete()
v1TestApplication(
settings = appSettingsDelete,
endpoint = "delete",
request = ChatDeleteRequest(
chatId = chat,
debug = Debug(mode = workMode),
),
) { response ->
val responseObj = response.body<ChatDeleteResponse>()
assertEquals(200, response.status.value)
assertEquals(ResponseResult.SUCCESS, responseObj.result)
}
}
@Test
fun search() = v1TestApplication(
settings = appSettingsSearch,
endpoint = "search",
request = ChatSearchRequest(
criteria = ChatSearchRequestAllOfCriteria(),
debug = Debug(mode = workMode),
),
) { response ->
val responseObj = response.body<ChatSearchResponse>()
assertEquals(200, response.status.value)
assertNotEquals(0, responseObj.chats?.size)
assertEquals(uuidOld, responseObj.chats?.first()?.id)
}
private inline fun <reified T: IRequest> v1TestApplication(
settings: MessengerAppSettings,
endpoint: String,
request: T,
crossinline function: suspend (HttpResponse) -> Unit,
): Unit = testApplication {
application { module(appSettings = settings) }
val client = createClient {
install(ContentNegotiation) {
jackson()
}
}
val response = client.post("/v1/chat/$endpoint") {
contentType(ContentType.Application.Json)
header("X-Trace-Id", "12345")
setBody(request)
}
function(response)
}
}

View File

@ -0,0 +1,78 @@
package ru.otus.messenger.app.repo
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import ru.otus.messenger.api.v1.models.DebugMode
import ru.otus.messenger.app.common.MessengerAppSettings
import ru.otus.messenger.app.common.MessengerAppSettingsData
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.repo.clickhouse.ChatRepoClickHouse
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.stubs.MessengerChatStub
class V1ChatRepoClickHouseTest : V1ChatRepoBaseTest() {
override val workMode: DebugMode = DebugMode.TEST
private fun appSettings(repo: IRepoChat) = MessengerAppSettingsData(
corSettings = MessengerCorSettings(
repoTest = repo
)
)
private val mockkRepo = mockk<ChatRepoClickHouse> {
every { save(any()) } returnsArgument(0)
coEvery { createChat(any()) } returns DbChatResponseOk(
MessengerChatStub.prepareResult {
id = ChatId(uuidNew)
}
)
coEvery { readChat(any()) } returns DbChatResponseOk(
MessengerChatStub.prepareResult {
id = ChatId(uuidOld)
}
)
coEvery { deleteChat(any()) } returns DbChatResponseOk(
MessengerChatStub.prepareResult {
id = ChatId(uuidOld)
}
)
coEvery { searchChat(any()) } returns DbChatsResponseOk(
listOf(
MessengerChatStub.prepareResult { id = ChatId(uuidOld) }
)
)
}
override val appSettingsCreate: MessengerAppSettings = appSettings(
repo = ChatRepoInitialized(
mockkRepo
)
)
override val appSettingsRead: MessengerAppSettings = appSettings(
repo = ChatRepoInitialized(
mockkRepo,
initObjects = listOf(initChat),
)
)
override val appSettingsDelete: MessengerAppSettings = appSettings(
repo = ChatRepoInitialized(
mockkRepo,
initObjects = listOf(initChat),
)
)
override val appSettingsSearch: MessengerAppSettings = appSettings(
repo = ChatRepoInitialized(
mockkRepo,
initObjects = listOf(initChat),
)
)
override val appSettingsResume: MessengerAppSettings = appSettings(
repo = ChatRepoInitialized(
mockkRepo,
initObjects = listOf(initChat),
)
)
}

View File

@ -0,0 +1,47 @@
package ru.otus.messenger.app.repo
import ru.otus.messenger.api.v1.models.DebugMode
import ru.otus.messenger.app.common.MessengerAppSettingsData
import ru.otus.messenger.common.MessengerCorSettings
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.inmemory.ChatRepoInMemory
class V1ChatRepoInmemoryTest : V1ChatRepoBaseTest() {
override val workMode: DebugMode = DebugMode.TEST
private fun appSettings(repo: IRepoChat) = MessengerAppSettingsData(
corSettings = MessengerCorSettings(
repoTest = repo
)
)
override val appSettingsCreate: MessengerAppSettingsData = appSettings(
repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew })
)
)
override val appSettingsRead: MessengerAppSettingsData = appSettings(
repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew }),
initObjects = listOf(initChat),
)
)
override val appSettingsDelete: MessengerAppSettingsData = appSettings(
repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew }),
initObjects = listOf(initChat),
)
)
override val appSettingsSearch: MessengerAppSettingsData = appSettings(
repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew }),
initObjects = listOf(initChat),
)
)
override val appSettingsResume: MessengerAppSettingsData = appSettings(
repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew }),
initObjects = listOf(initChat),
)
)
}

View File

@ -13,4 +13,6 @@ dependencies {
testImplementation(kotlin("test-junit"))
testImplementation(libs.kotlin.coroutines.test)
testImplementation(project(":ok-messenger-repo-tests"))
testImplementation(project(":ok-messenger-repo-inmemory"))
}

View File

@ -1,9 +1,20 @@
package ru.otus.messenger.biz
import ru.otus.messenger.biz.general.initRepo
import ru.otus.messenger.biz.general.initStatus
import ru.otus.messenger.biz.general.operation
import ru.otus.messenger.biz.general.prepareResult
import ru.otus.messenger.biz.general.stubs
import ru.otus.messenger.biz.general.validation
import ru.otus.messenger.biz.repo.checkLock
import ru.otus.messenger.biz.repo.repoCreate
import ru.otus.messenger.biz.repo.repoDelete
import ru.otus.messenger.biz.repo.repoPrepareCreate
import ru.otus.messenger.biz.repo.repoPrepareDelete
import ru.otus.messenger.biz.repo.repoPrepareUpdate
import ru.otus.messenger.biz.repo.repoRead
import ru.otus.messenger.biz.repo.repoSearch
import ru.otus.messenger.biz.repo.repoUpdate
import ru.otus.messenger.biz.stubs.stubCreateSuccess
import ru.otus.messenger.biz.stubs.stubReadSuccess
import ru.otus.messenger.biz.stubs.stubUpdateSuccess
@ -27,6 +38,8 @@ import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.cor.dsl.chain
import ru.otus.messenger.cor.dsl.rootChain
import ru.otus.messenger.cor.dsl.worker
@ -39,6 +52,7 @@ class MessengerProcessor(
private val businessChain = rootChain<MessengerContext> {
initStatus("Инициализация статуса")
initRepo("Инициализация репозитория")
operation("Создание чата", ChatCommand.CREATE) {
stubs("Обработка стабов") {
@ -61,6 +75,13 @@ class MessengerProcessor(
finishChatValidation("Завершение проверок")
}
chain {
title = "Логика сохранения"
repoPrepareCreate("Подготовка объекта для сохранения")
repoCreate("Создание чата в БД")
}
prepareResult("Подготовка ответа")
}
operation("Получить чат", ChatCommand.READ) {
@ -79,6 +100,17 @@ class MessengerProcessor(
finishChatValidation("Успешное завершение процедуры валидации")
}
chain {
title = "Логика чтения"
repoRead("Чтение чата из БД")
worker {
title = "Подготовка ответа для Read"
on { state == ChatState.RUNNING }
handle { chatRepoDone = chatRepoRead }
}
}
prepareResult("Подготовка ответа")
}
operation("Изменить чат", ChatCommand.UPDATE) {
@ -105,6 +137,15 @@ class MessengerProcessor(
finishChatValidation("Успешное завершение процедуры валидации")
}
chain {
title = "Логика сохранения"
repoRead("Чтение чата из БД")
checkLock("Проверяем консистентность по оптимистичной блокировке")
repoPrepareUpdate("Подготовка объекта для обновления")
repoUpdate("Обновление чата в БД")
}
prepareResult("Подготовка ответа")
}
operation("Удалить чат", ChatCommand.DELETE) {
@ -124,6 +165,15 @@ class MessengerProcessor(
validateIdProperFormat("Проверка формата id")
finishChatValidation("Успешное завершение процедуры валидации")
}
chain {
title = "Логика удаления"
repoRead("Чтение чата из БД")
checkLock("Проверяем консистентность по оптимистичной блокировке")
repoPrepareDelete("Подготовка объекта для удаления")
repoDelete("Удаление чата из БД")
}
prepareResult("Подготовка ответа")
}
operation("Поиск чата", ChatCommand.SEARCH) {
@ -140,6 +190,9 @@ class MessengerProcessor(
finishChatFilterValidation("Успешное завершение процедуры валидации")
}
repoSearch("Поиск чата в БД по фильтру")
prepareResult("Подготовка ответа")
}
}.build()
}

View File

@ -0,0 +1,7 @@
package ru.otus.messenger.biz.exception
import ru.otus.messenger.common.models.WorkMode
class DbNotConfiguredException(val workMode: WorkMode): Exception(
"Database is not configured properly for work mode $workMode"
)

View File

@ -0,0 +1,28 @@
package ru.otus.messenger.biz.general
import ru.otus.messenger.biz.exception.DbNotConfiguredException
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.errorSystem
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.WorkMode
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.initRepo(title: String) = worker {
this.title = title
description = "Estimate main working repo depending on work mode".trimIndent()
handle {
chatRepo = when (workMode) {
WorkMode.TEST -> corSettings.repoTest
WorkMode.STUB -> corSettings.repoStub
else -> corSettings.repoProd
}
if (workMode != WorkMode.STUB && chatRepo == IRepoChat.NONE) fail(
errorSystem(
violationCode = "dbNotConfigured",
e = DbNotConfiguredException(workMode)
)
)
}
}

View File

@ -0,0 +1,21 @@
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.worker
fun ICorChainDsl<MessengerContext>.prepareResult(title: String) = worker {
this.title = title
description = "Подготовка данных для ответа клиенту на запрос"
on { workMode != WorkMode.STUB }
handle {
chatResponse = chatRepoDone
chatsResponse = chatsRepoDone
state = when (val st = state) {
ChatState.RUNNING -> ChatState.FINISHING
else -> st
}
}
}

View File

@ -0,0 +1,25 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseErrWithData
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.repoCreate(title: String) = worker {
this.title = title
description = "Добавление объявления в БД"
on { state == ChatState.RUNNING }
handle {
val request = DbChatRequest(chatRepoPrepare)
when(val result = chatRepo.createChat(request)) {
is DbChatResponseOk -> chatRepoDone = result.data
is DbChatResponseErr -> fail(result.errors)
is DbChatResponseErrWithData -> fail(result.errors)
}
}
}

View File

@ -0,0 +1,31 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseErrWithData
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.repoDelete(title: String) = worker {
this.title = title
description = "Удаление объявления из БД по ID"
on { state == ChatState.RUNNING }
handle {
val request = DbChatIdRequest(chatRepoPrepare)
when(val result = chatRepo.deleteChat(request)) {
is DbChatResponseOk -> chatRepoDone = result.data
is DbChatResponseErr -> {
fail(result.errors)
chatRepoDone = chatRepoRead
}
is DbChatResponseErrWithData -> {
fail(result.errors)
chatRepoDone = result.data
}
}
}
}

View File

@ -0,0 +1,15 @@
package ru.otus.messenger.biz.repo
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>.repoPrepareCreate(title: String) = worker {
this.title = title
description = "Подготовка объекта к сохранению в базе данных"
on { state == ChatState.RUNNING }
handle {
chatRepoPrepare = chatValidated.deepCopy()
}
}

View File

@ -0,0 +1,15 @@
package ru.otus.messenger.biz.repo
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>.repoPrepareDelete(title: String) = worker {
this.title = title
description = "Готовим данные к удалению из БД".trimIndent()
on { state == ChatState.RUNNING }
handle {
chatRepoPrepare = chatValidated.deepCopy()
}
}

View File

@ -0,0 +1,21 @@
package ru.otus.messenger.biz.repo
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>.repoPrepareUpdate(title: String) = worker {
this.title = title
description = "Готовим данные к сохранению в БД: совмещаем данные, прочитанные из БД, " +
"и данные, полученные от пользователя"
on { state == ChatState.RUNNING }
handle {
chatRepoPrepare = chatRepoRead.deepCopy().apply {
this.title = chatValidated.title
description = chatValidated.description
type = chatValidated.type
mode = chatValidated.mode
}
}
}

View File

@ -0,0 +1,28 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseErrWithData
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.repoRead(title: String) = worker {
this.title = title
description = "Chat reading from DB"
on { state == ChatState.RUNNING }
handle {
val request = DbChatIdRequest(chatValidated)
when(val result = chatRepo.readChat(request)) {
is DbChatResponseOk -> chatRepoRead = result.data
is DbChatResponseErr -> fail(result.errors)
is DbChatResponseErrWithData -> {
fail(result.errors)
chatRepoRead = result.data
}
}
}
}

View File

@ -0,0 +1,28 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatsResponseErr
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.repoSearch(title: String) = worker {
this.title = title
description = "Search for chats in DB using filters"
on { state == ChatState.RUNNING }
handle {
val request = DbChatFilterRequest(
ownerId = chatFilterValidated.ownerId,
chatType = chatFilterValidated.type,
chatMode = chatFilterValidated.mode,
searchFields = chatFilterValidated.searchFields,
)
when (val result = chatRepo.searchChat(request)) {
is DbChatsResponseOk -> chatsRepoDone = result.data.toMutableList()
is DbChatsResponseErr -> fail(result.errors)
}
}
}

View File

@ -0,0 +1,27 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseErrWithData
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.repoUpdate(title: String) = worker {
this.title = title
on { state == ChatState.RUNNING }
handle {
val request = DbChatRequest(chatRepoPrepare)
when(val result = chatRepo.updateChat(request)) {
is DbChatResponseOk -> chatRepoDone = result.data
is DbChatResponseErr -> fail(result.errors)
is DbChatResponseErrWithData -> {
fail(result.errors)
chatRepoDone = result.data
}
}
}
}

View File

@ -0,0 +1,20 @@
package ru.otus.messenger.biz.repo
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.models.ChatState
import ru.otus.messenger.common.repo.errorRepoConcurrency
import ru.otus.messenger.common.helpers.fail
import ru.otus.messenger.cor.dsl.ICorChainDsl
import ru.otus.messenger.cor.dsl.worker
fun ICorChainDsl<MessengerContext>.checkLock(title: String) = worker {
this.title = title
description = """
Проверка оптимистичной блокировки. Если не равна сохраненной в БД, значит данные запроса устарели
и необходимо их обновить вручную
""".trimIndent()
on { state == ChatState.RUNNING && chatValidated.id != chatRepoRead.id }
handle {
fail(errorRepoConcurrency(chatRepoRead).errors)
}
}

View File

@ -0,0 +1,66 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.ChatOwnerId
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.repo.DbChatResponseOk
import ru.otus.messenger.repo.tests.ChatRepositoryMock
class BizRepoCreateTest {
private val userId = ChatOwnerId("321")
private val command = ChatCommand.CREATE
private val uuid = "10000000-0000-0000-0000-000000000001"
private val repo = ChatRepositoryMock(
invokeCreateChat = {
DbChatResponseOk(
data = MessengerChat(
id = ChatId(uuid),
title = it.chat.title,
description = it.chat.description,
ownerId = userId,
type = it.chat.type,
mode = it.chat.mode,
)
)
}
)
private val settings = MessengerCorSettings(
repoTest = repo
)
private val processor = MessengerProcessor(settings)
@Test
fun repoCreateSuccessTest() = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChat(
title = "abc",
description = "abc",
type = ChatType.GROUP,
mode = ChatMode.WORK,
),
)
processor.exec(ctx)
assertEquals(ChatState.FINISHING, ctx.state)
assertNotEquals(ChatId.NONE, ctx.chatResponse.id)
assertEquals("abc", ctx.chatResponse.title)
assertEquals("abc", ctx.chatResponse.description)
assertEquals(ChatType.GROUP, ctx.chatResponse.type)
assertEquals(ChatMode.WORK, ctx.chatResponse.mode)
}
}

View File

@ -0,0 +1,78 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.ChatOwnerId
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.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.repo.tests.ChatRepositoryMock
class BizRepoDeleteTest {
private val userId = ChatOwnerId("321")
private val command = ChatCommand.DELETE
private val initAd = MessengerChat(
id = ChatId("123"),
title = "abc",
description = "abc",
ownerId = userId,
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
private val repo = ChatRepositoryMock(
invokeReadChat = {
DbChatResponseOk(
data = initAd,
)
},
invokeDeleteChat = {
if (it.id == initAd.id)
DbChatResponseOk(
data = initAd
)
else DbChatResponseErr()
}
)
private val settings by lazy {
MessengerCorSettings(
repoTest = repo
)
}
private val processor = MessengerProcessor(settings)
@Test
fun repoDeleteSuccessTest() = runTest {
val chatToUpdate = MessengerChat(
id = ChatId("123"),
)
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = chatToUpdate,
)
processor.exec(ctx)
assertEquals(ChatState.FINISHING, ctx.state)
assertTrue { ctx.errors.isEmpty() }
assertEquals(initAd.id, ctx.chatResponse.id)
assertEquals(initAd.title, ctx.chatResponse.title)
assertEquals(initAd.description, ctx.chatResponse.description)
assertEquals(initAd.type, ctx.chatResponse.type)
assertEquals(initAd.mode, ctx.chatResponse.mode)
}
@Test
fun repoDeleteNotFoundTest() = repoNotFoundTest(command)
}

View File

@ -0,0 +1,63 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.ChatOwnerId
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.repo.DbChatResponseOk
import ru.otus.messenger.repo.tests.ChatRepositoryMock
class BizRepoReadTest {
private val userId = ChatOwnerId("321")
private val command = ChatCommand.READ
private val initAd = MessengerChat(
id = ChatId("123"),
title = "abc",
description = "abc",
ownerId = userId,
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
private val repo = ChatRepositoryMock(
invokeReadChat = {
DbChatResponseOk(
data = initAd,
)
}
)
private val settings = MessengerCorSettings(repoTest = repo)
private val processor = MessengerProcessor(settings)
@Test
fun repoReadSuccessTest() = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChat(
id = ChatId("123"),
),
)
processor.exec(ctx)
assertEquals(ChatState.FINISHING, ctx.state)
assertEquals(initAd.id, ctx.chatResponse.id)
assertEquals(initAd.title, ctx.chatResponse.title)
assertEquals(initAd.description, ctx.chatResponse.description)
assertEquals(initAd.type, ctx.chatResponse.type)
assertEquals(initAd.mode, ctx.chatResponse.mode)
}
@Test
fun repoReadNotFoundTest() = repoNotFoundTest(command)
}

View File

@ -0,0 +1,64 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.ChatOwnerId
import ru.otus.messenger.common.models.ChatSearchFilter
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.repo.DbChatsResponseOk
import ru.otus.messenger.repo.tests.ChatRepositoryMock
class BizRepoSearchTest {
private val userId = ChatOwnerId("321")
private val command = ChatCommand.SEARCH
private val initAd = MessengerChat(
id = ChatId("123"),
title = "abc",
description = "abc",
ownerId = userId,
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
private val repo = ChatRepositoryMock(
invokeSearchChat = {
DbChatsResponseOk(
data = listOf(initAd),
)
}
)
private val settings = MessengerCorSettings(repoTest = repo)
private val processor = MessengerProcessor(settings)
@Test
fun repoSearchSuccessTest() = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatFilterRequest = ChatSearchFilter(
searchFields = listOf(
ChatSearchFilter.StringSearchField(
fieldName = "ownerId",
stringValue = userId.asString()
)
),
type = ChatType.GROUP,
mode = ChatMode.WORK,
),
)
processor.exec(ctx)
assertEquals(ChatState.FINISHING, ctx.state)
assertEquals(1, ctx.chatsResponse.size)
}
}

View File

@ -0,0 +1,79 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.ChatOwnerId
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.repo.DbChatResponseOk
import ru.otus.messenger.repo.tests.ChatRepositoryMock
class BizRepoUpdateTest {
private val userId = ChatOwnerId("321")
private val command = ChatCommand.UPDATE
private val initAd = MessengerChat(
id = ChatId("123"),
title = "abc",
description = "abc",
ownerId = userId,
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
private val repo = ChatRepositoryMock(
invokeReadChat = {
DbChatResponseOk(
data = initAd,
)
},
invokeUpdateChat = {
DbChatResponseOk(
data = MessengerChat(
id = ChatId("123"),
title = "xyz",
description = "xyz",
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
)
}
)
private val settings = MessengerCorSettings(repoTest = repo)
private val processor = MessengerProcessor(settings)
@Test
fun repoUpdateSuccessTest() = runTest {
val chatToUpdate = MessengerChat(
id = ChatId("123"),
title = "xyz",
description = "xyz",
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = chatToUpdate,
)
processor.exec(ctx)
assertEquals(ChatState.FINISHING, ctx.state)
assertEquals(chatToUpdate.id, ctx.chatResponse.id)
assertEquals(chatToUpdate.title, ctx.chatResponse.title)
assertEquals(chatToUpdate.description, ctx.chatResponse.description)
assertEquals(chatToUpdate.type, ctx.chatResponse.type)
assertEquals(chatToUpdate.mode, ctx.chatResponse.mode)
}
@Test
fun repoUpdateNotFoundTest() = repoNotFoundTest(command)
}

View File

@ -0,0 +1,57 @@
package ru.otus.messenger.biz.repo
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.biz.MessengerProcessor
import ru.otus.messenger.common.MessengerContext
import ru.otus.messenger.common.MessengerCorSettings
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.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.errorNotFound
import ru.otus.messenger.repo.tests.ChatRepositoryMock
private val initAd = MessengerChat(
id = ChatId("123"),
title = "abc",
description = "abc",
type = ChatType.GROUP,
mode = ChatMode.WORK,
)
private val repo = ChatRepositoryMock(
invokeReadChat = {
if (it.id == initAd.id) {
DbChatResponseOk(
data = initAd,
)
} else errorNotFound(it.id)
}
)
private val settings = MessengerCorSettings(repoTest = repo)
private val processor = MessengerProcessor(settings)
fun repoNotFoundTest(command: ChatCommand) = runTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
workMode = WorkMode.TEST,
chatRequest = MessengerChat(
id = ChatId("12345"),
title = "xyz",
description = "xyz",
type = ChatType.GROUP,
mode = ChatMode.WORK,
),
)
processor.exec(ctx)
assertEquals(ChatState.FAILING, ctx.state)
assertEquals(MessengerChat(), ctx.chatResponse)
assertEquals(1, ctx.errors.size)
assertNotNull(ctx.errors.find { it.code == "repo-not-found" }, "Errors must contain not-found")
}

View File

@ -3,9 +3,19 @@ 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
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.inmemory.ChatRepoInMemory
import ru.otus.messenger.stubs.MessengerChatStub
abstract class BaseBizValidationTest {
protected abstract val command: ChatCommand
private val settings by lazy { MessengerCorSettings() }
private val repo = ChatRepoInitialized(
repo = ChatRepoInMemory(),
initObjects = listOf(
MessengerChatStub.get(),
),
)
private val settings by lazy { MessengerCorSettings(repoTest = repo) }
protected val processor by lazy { MessengerProcessor(settings) }
}

View File

@ -0,0 +1,13 @@
package ru.otus.messenger.biz.validation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
fun runBizTest(block: suspend () -> Unit) = runTest {
withContext(Dispatchers.Default.limitedParallelism(1)) {
block()
}
}

View File

@ -3,7 +3,6 @@ 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
@ -11,7 +10,7 @@ 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 {
fun validationDescriptionCorrect(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -25,7 +24,7 @@ fun validationDescriptionCorrect(command: ChatCommand, processor: MessengerProce
assertContains(ctx.chatValidated.description, "description")
}
fun validationDescriptionTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationDescriptionTrim(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -40,7 +39,7 @@ fun validationDescriptionTrim(command: ChatCommand, processor: MessengerProcesso
assertEquals("abc", ctx.chatValidated.description)
}
fun validationDescriptionEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationDescriptionEmpty(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -57,7 +56,7 @@ fun validationDescriptionEmpty(command: ChatCommand, processor: MessengerProcess
assertContains(error?.message ?: "", "description")
}
fun validationDescriptionSymbols(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationDescriptionSymbols(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,

View File

@ -3,7 +3,6 @@ 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
@ -12,7 +11,7 @@ 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 {
fun validationIdCorrect(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -24,7 +23,7 @@ fun validationIdCorrect(command: ChatCommand, processor: MessengerProcessor) = r
assertNotEquals(ChatState.FAILING, ctx.state)
}
fun validationIdTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationIdTrim(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -38,7 +37,7 @@ fun validationIdTrim(command: ChatCommand, processor: MessengerProcessor) = runT
assertNotEquals(ChatState.FAILING, ctx.state)
}
fun validationIdEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationIdEmpty(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -55,7 +54,7 @@ fun validationIdEmpty(command: ChatCommand, processor: MessengerProcessor) = run
assertContains(error?.message ?: "", "id")
}
fun validationIdFormat(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationIdFormat(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,

View File

@ -3,7 +3,6 @@ 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
@ -11,7 +10,7 @@ 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 {
fun validationTitleCorrect(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -21,12 +20,13 @@ fun validationTitleCorrect(command: ChatCommand, processor: MessengerProcessor)
},
)
processor.exec(ctx)
println(ctx.errors.joinToString("\n"))
assertEquals(0, ctx.errors.size)
assertNotEquals(ChatState.FAILING, ctx.state)
assertEquals("abc", ctx.chatValidated.title)
}
fun validationTitleTrim(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationTitleTrim(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -41,7 +41,7 @@ fun validationTitleTrim(command: ChatCommand, processor: MessengerProcessor) = r
assertEquals("abc", ctx.chatValidated.title)
}
fun validationTitleEmpty(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationTitleEmpty(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,
@ -58,7 +58,7 @@ fun validationTitleEmpty(command: ChatCommand, processor: MessengerProcessor) =
assertContains(error?.message ?: "", "title")
}
fun validationTitleSymbols(command: ChatCommand, processor: MessengerProcessor) = runTest {
fun validationTitleSymbols(command: ChatCommand, processor: MessengerProcessor) = runBizTest {
val ctx = MessengerContext(
command = command,
state = ChatState.NONE,

View File

@ -2,9 +2,6 @@ plugins {
id("build-jvm")
}
group = rootProject.group
version = rootProject.version
sourceSets {
main {
java.srcDir("src/commonMain/kotlin")
@ -13,8 +10,10 @@ sourceSets {
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.datetime)
api(libs.kotlin.datetime)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.coroutines)
api("ru.otus.messenger.libs:ok-messenger-lib-logging")
testImplementation(kotlin("test-junit"))

View File

@ -2,6 +2,7 @@ package ru.otus.messenger.common
import kotlinx.datetime.Instant
import ru.otus.messenger.common.models.*
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.common.stubs.MessengerStubs
import ru.otus.messenger.common.ws.IMessengerWsSession
@ -17,15 +18,27 @@ data class MessengerContext(
var requestId: RequestId = RequestId.NONE,
var timeStart: Instant = Instant.NONE,
// objects from request
var chatRequest: MessengerChat = MessengerChat(),
var chatFilterRequest: ChatSearchFilter = ChatSearchFilter.NONE,
// objects during validation process
var chatValidating: MessengerChat = MessengerChat(),
var chatFilterValidating: ChatSearchFilter = ChatSearchFilter.NONE,
// objects after validation
var chatValidated: MessengerChat = MessengerChat(),
var chatFilterValidated: ChatSearchFilter = ChatSearchFilter.NONE,
// objects during requests to DB
var chatRepo: IRepoChat = IRepoChat.NONE,
var chatRepoRead: MessengerChat = MessengerChat(), // object, read from repo
var chatRepoPrepare: MessengerChat = MessengerChat(), // prepare to save to DB
var chatRepoDone: MessengerChat = MessengerChat(), // result from DB
var chatsRepoDone: MutableList<MessengerChat> = mutableListOf(),
// objects to send to client
var chatResponse: MessengerChat = MessengerChat(),
var chatsResponse: MutableList<MessengerChat> = mutableListOf(),
)

View File

@ -1,11 +1,15 @@
package ru.otus.messenger.common
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.common.ws.IMessengerWsSessionRepo
import ru.otus.messenger.logging.common.LoggerProvider
data class MessengerCorSettings(
val loggerProvider: LoggerProvider = LoggerProvider(),
val wsSessions: IMessengerWsSessionRepo = IMessengerWsSessionRepo.NONE,
val repoStub: IRepoChat = IRepoChat.NONE,
val repoTest: IRepoChat = IRepoChat.NONE,
val repoProd: IRepoChat = IRepoChat.NONE,
) {
companion object {
val NONE = MessengerCorSettings()

View File

@ -0,0 +1,9 @@
package ru.otus.messenger.common.exceptions
import ru.otus.messenger.common.models.ChatId
open class RepoChatException(
@Suppress("unused")
val chatId: ChatId,
msg: String,
): RepoException(msg)

View File

@ -0,0 +1,8 @@
package ru.otus.messenger.common.exceptions
import ru.otus.messenger.common.models.ChatId
class RepoConcurrencyException(id: ChatId): RepoChatException(
id,
"Expected lock while actual lock in db"
)

View File

@ -0,0 +1,8 @@
package ru.otus.messenger.common.exceptions
import ru.otus.messenger.common.models.ChatId
class RepoEmptyLockException(id: ChatId): RepoChatException(
id,
"Lock is empty in DB"
)

View File

@ -0,0 +1,3 @@
package ru.otus.messenger.common.exceptions
open class RepoException(msg: String): Exception(msg)

View File

@ -5,8 +5,14 @@ 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.addErrors(error: Collection<ChatError>) = errors.addAll(error)
fun MessengerContext.fail(error: ChatError) {
addError(error)
state = ChatState.FAILING
}
fun MessengerContext.fail(errors: Collection<ChatError>) {
addErrors(errors)
state = ChatState.FAILING
}

View File

@ -26,4 +26,16 @@ fun errorValidation(
group = "validation",
message = "Validation error for field $field: $description",
level = level,
)
fun errorSystem(
violationCode: String,
level: LogLevel = LogLevel.ERROR,
e: Throwable,
) = ChatError(
code = "system-$violationCode",
group = "system",
message = "System error occurred. Our stuff has been informed, please retry later",
level = level,
exception = e,
)

View File

@ -0,0 +1,29 @@
package ru.otus.messenger.common.repo
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import ru.otus.messenger.common.helpers.errorSystem
abstract class ChatRepoBase : IRepoChat {
protected suspend fun tryChatMethod(timeout: Duration = 10.seconds, ctx: CoroutineContext = Dispatchers.IO, block: suspend () -> IDbChatResponse) = try {
withTimeout(timeout) {
withContext(ctx) {
block()
}
}
} catch (e: Throwable) {
DbChatResponseErr(errorSystem("methodException", e = e))
}
protected suspend fun tryChatsMethod(block: suspend () -> IDbChatsResponse) = try {
block()
} catch (e: Throwable) {
DbChatsResponseErr(errorSystem("methodException", e = e))
}
}

View File

@ -0,0 +1,13 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatType
data class DbChatFilterRequest(
val searchFields: List<ChatSearchFilter.SearchField> = emptyList(),
val ownerId: ChatOwnerId = ChatOwnerId.NONE,
val chatType: ChatType = ChatType.NONE,
val chatMode: ChatMode = ChatMode.NONE,
)

View File

@ -0,0 +1,10 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.MessengerChat
data class DbChatIdRequest(
val id: ChatId,
) {
constructor(chat: MessengerChat): this(chat.id)
}

View File

@ -0,0 +1,7 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.MessengerChat
data class DbChatRequest(
val chat: MessengerChat
)

View File

@ -0,0 +1,9 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.ChatError
data class DbChatResponseErr(
val errors: List<ChatError> = emptyList()
): IDbChatResponse {
constructor(err: ChatError): this(listOf(err))
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.MessengerChat
data class DbChatResponseErrWithData(
val data: MessengerChat,
val errors: List<ChatError> = emptyList()
): IDbChatResponse {
constructor(chat: MessengerChat, err: ChatError): this(chat, listOf(err))
}

View File

@ -0,0 +1,7 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.MessengerChat
data class DbChatResponseOk(
val data: MessengerChat
): IDbChatResponse

View File

@ -0,0 +1,10 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.ChatError
@Suppress("unused")
data class DbChatsResponseErr(
val errors: List<ChatError> = emptyList()
): IDbChatsResponse {
constructor(err: ChatError): this(listOf(err))
}

View File

@ -0,0 +1,7 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.MessengerChat
data class DbChatsResponseOk(
val data: List<MessengerChat>
): IDbChatsResponse

View File

@ -0,0 +1,60 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.exceptions.RepoConcurrencyException
import ru.otus.messenger.common.exceptions.RepoException
import ru.otus.messenger.common.helpers.errorSystem
import ru.otus.messenger.common.models.ChatError
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.MessengerChat
const val ERROR_GROUP_REPO = "repo"
fun errorNotFound(id: ChatId) = DbChatResponseErr(
ChatError(
code = "$ERROR_GROUP_REPO-not-found",
group = ERROR_GROUP_REPO,
field = "id",
message = "Object with ID: ${id.asString()} is not Found",
)
)
val errorEmptyId = DbChatResponseErr(
ChatError(
code = "$ERROR_GROUP_REPO-empty-id",
group = ERROR_GROUP_REPO,
field = "id",
message = "Id must not be null or blank"
)
)
fun errorRepoConcurrency(
oldChat: MessengerChat,
exception: Exception = RepoConcurrencyException(
id = oldChat.id
),
) = DbChatResponseErrWithData(
chat = oldChat,
err = ChatError(
code = "${ERROR_GROUP_REPO}-concurrency",
group = ERROR_GROUP_REPO,
field = "lock",
message = "The object with ID ${oldChat.id.asString()} has been changed concurrently by another user or process",
exception = exception,
)
)
fun errorEmptyLock(id: ChatId) = DbChatResponseErr(
ChatError(
code = "$ERROR_GROUP_REPO-lock-empty",
group = ERROR_GROUP_REPO,
field = "lock",
message = "Lock for Ad ${id.asString()} is empty that is not admitted"
)
)
fun errorDb(e: RepoException) = DbChatResponseErr(
errorSystem(
violationCode = "dbLockEmpty",
e = e
)
)

View File

@ -0,0 +1,5 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.MessengerChat
sealed interface IDbChatResponse: IDbResponse<MessengerChat>

View File

@ -0,0 +1,5 @@
package ru.otus.messenger.common.repo
import ru.otus.messenger.common.models.MessengerChat
sealed interface IDbChatsResponse: IDbResponse<List<MessengerChat>>

View File

@ -0,0 +1,3 @@
package ru.otus.messenger.common.repo
sealed interface IDbResponse<T>

View File

@ -0,0 +1,35 @@
package ru.otus.messenger.common.repo
interface IRepoChat {
suspend fun createChat(rq: DbChatRequest): IDbChatResponse
suspend fun readChat(rq: DbChatIdRequest): IDbChatResponse
suspend fun updateChat(rq: DbChatRequest): IDbChatResponse
suspend fun deleteChat(rq: DbChatIdRequest): IDbChatResponse
suspend fun searchChat(rq: DbChatFilterRequest): IDbChatsResponse
companion object {
val NONE = object : IRepoChat {
override suspend fun createChat(rq: DbChatRequest): IDbChatResponse {
throw NotImplementedError("Must not be used")
}
override suspend fun readChat(rq: DbChatIdRequest): IDbChatResponse {
throw NotImplementedError("Must not be used")
}
override suspend fun updateChat(rq: DbChatRequest): IDbChatResponse {
throw NotImplementedError("Must not be used")
}
override suspend fun deleteChat(rq: DbChatIdRequest): IDbChatResponse {
throw NotImplementedError("Must not be used")
}
override suspend fun searchChat(rq: DbChatFilterRequest): IDbChatsResponse {
throw NotImplementedError("Must not be used")
}
}
}
}

View File

@ -0,0 +1,19 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.coroutines)
implementation(libs.uuid)
implementation(libs.clickhouse.client)
implementation(project(":ok-messenger-common"))
api(project(":ok-messenger-repo-common"))
testImplementation(kotlin("test-junit"))
testImplementation(project(":ok-messenger-repo-tests"))
testImplementation(project(":ok-messenger-stubs"))
testImplementation(libs.mockk)
}

View File

@ -0,0 +1,255 @@
package ru.otus.messenger.repo.clickhouse
import com.benasher44.uuid.uuid4
import com.clickhouse.client.api.Client
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import kotlinx.datetime.toKotlinInstant
import ru.otus.messenger.common.NONE
import ru.otus.messenger.common.models.ChatArchiveFlag
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMetadata
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.ChatRepoBase
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IDbChatResponse
import ru.otus.messenger.common.repo.IDbChatsResponse
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.common.repo.errorEmptyId
import ru.otus.messenger.common.repo.errorNotFound
import ru.otus.messenger.repo.common.IRepoChatInitializable
class ChatRepoClickHouse(
properties: DbProperties,
private val randomUuid: () -> String = { uuid4().toString() }
) : ChatRepoBase(), IRepoChat, IRepoChatInitializable {
private val chatTable: String = "chat"
private val client: Client = Client.Builder()
.addEndpoint("https://${properties.host}:${properties.port}/")
.setUsername(properties.user)
.setPassword(properties.password)
.build().also {
it.register(ChatTableRecord::class.java, it.getTableSchema(chatTable))
}
override fun save(chats: Collection<MessengerChat>): Collection<MessengerChat> {
try {
client.insert(
chatTable,
chats.map { chat -> ChatTableRecord(chat) }
)
return chats
} catch (e: Exception) {
return listOf()
}
}
override suspend fun createChat(request: DbChatRequest): IDbChatResponse = tryChatMethod {
val key = randomUuid()
val chat = request.chat.copy(id = ChatId(key))
val record = ChatTableRecord(chat)
withContext(Dispatchers.IO) {
client.insert(chatTable, listOf(record))
}
DbChatResponseOk(chat)
}
override suspend fun readChat(request: DbChatIdRequest): IDbChatResponse = tryChatMethod {
val key = request.id.takeIf { it != ChatId.NONE }?.asString() ?: return@tryChatMethod errorEmptyId
val sql = "SELECT * FROM $chatTable WHERE chatId = $key"
// Default format is RowBinaryWithNamesAndTypesFormatReader so reader have all information about columns
withContext(Dispatchers.IO) {
client.query(sql)[3, TimeUnit.SECONDS].use { response ->
// Create a reader to access the data in a convenient way
val reader = client.newBinaryFormatReader(response)
if (reader.hasNext()) {
// Read the next record from stream and parse it
reader.next()
// get values
DbChatResponseOk(
ChatTableRecord(
chatId = reader.getString("chatId"),
title = reader.getString("title"),
description = reader.getString("description"),
type = reader.getString("type"),
mode = reader.getString("mode"),
ownerId = reader.getString("ownerId"),
participants = reader.getList("participants"),
createdAt = reader.getInstant("createdAt").toKotlinInstant(),
updatedAt = reader.getInstant("updatedAt").toKotlinInstant(),
isArchived = reader.getBoolean("isArchived"),
metadata = reader.getString("metadata"),
).toInternal()
)
} else {
errorNotFound(request.id)
}
}
}
}
override suspend fun updateChat(request: DbChatRequest): IDbChatResponse = tryChatMethod {
val chatId = request.chat.id.takeIf { it != ChatId.NONE } ?: return@tryChatMethod errorEmptyId
val chat = request.chat
val updates = mutableListOf<String>()
val params = mutableMapOf<String, Any>()
if (chat.title.isNotBlank()) {
updates += "title = :title"
params["title"] = chat.title
}
if (chat.description.isNotBlank()) {
updates += "description = :description"
params["description"] = chat.description
}
if (chat.type != ChatType.NONE) {
updates += "chat_type = :chatType"
params["chatType"] = chat.type.name
}
if (chat.mode != ChatMode.NONE) {
updates += "chat_mode = :chatMode"
params["chatMode"] = chat.mode.name
}
if (chat.ownerId != ChatOwnerId.NONE) {
updates += "owner_id = :ownerId"
params["ownerId"] = chat.ownerId.asString()
}
if (chat.createdAt != Instant.NONE) {
updates += "created_at = :createdAt"
params["createdAt"] = chat.createdAt
}
if (chat.updatedAt != Instant.NONE) {
updates += "updated_at = :updatedAt"
params["updatedAt"] = chat.updatedAt
}
if (chat.isArchived != ChatArchiveFlag.NONE) {
updates += "is_archived = :isArchived"
params["isArchived"] = chat.isArchived.asBoolean()
}
if (chat.metadata != ChatMetadata.NONE) {
updates += "metadata = :metadata"
params["metadata"] = chat.metadata.asString()
}
if (updates.isEmpty()) {
throw IllegalArgumentException("No fields to update")
}
params["chatId"] = chatId.asString()
val sql = """
UPDATE $chatTable
SET ${updates.joinToString(", ")}
WHERE id = :chatId
""".trimIndent()
withContext(Dispatchers.IO) {
client.query(sql)[3, TimeUnit.SECONDS]
}
readChat(DbChatIdRequest(chatId))
}
override suspend fun deleteChat(request: DbChatIdRequest): IDbChatResponse = tryChatMethod {
val chatId = request.id.takeIf { it != ChatId.NONE } ?: return@tryChatMethod errorEmptyId
val key = chatId.asString()
val result = readChat(request)
val sql = "DELETE FROM $chatTable WHERE chatId = $key"
withContext(Dispatchers.IO) {
client.query(sql)[3, TimeUnit.SECONDS]
}
result
}
override suspend fun searchChat(request: DbChatFilterRequest): IDbChatsResponse = tryChatsMethod {
val result: MutableList<ChatTableRecord> = mutableListOf()
val whereClauses = mutableListOf<String>()
val params = mutableMapOf<String, Any>()
if (request.ownerId != ChatOwnerId.NONE) {
whereClauses += "ownerId = :ownerId"
params["ownerId"] = request.ownerId.asString()
}
if (request.chatType != ChatType.NONE) {
whereClauses += "chatType = :chatType"
params["chatType"] = request.chatType.name
}
if (request.chatMode != ChatMode.NONE) {
whereClauses += "chatMode = :chatMode"
params["chatMode"] = request.chatMode.name
}
if (request.searchFields.isNotEmpty()) {
val searchConditions = request.searchFields.mapIndexed { index, field ->
field as ChatSearchFilter.StringSearchField
val paramName = "searchField$index"
params[paramName] = "%${field.stringValue}%"
"${field.fieldName} ILIKE :$paramName"
}
whereClauses += "(${searchConditions.joinToString(" OR ")})"
}
val wherePart = if (whereClauses.isNotEmpty()) {
"WHERE " + whereClauses.joinToString(" AND ")
} else {
""
}
val sql = "SELECT * FROM $chatTable $wherePart"
// Default format is RowBinaryWithNamesAndTypesFormatReader so reader have all information about columns
withContext(Dispatchers.IO) {
client.query(sql)[3, TimeUnit.SECONDS].use { response ->
// Create a reader to access the data in a convenient way
val reader = client.newBinaryFormatReader(response)
while (reader.hasNext()) {
// Read the next record from stream and parse it
reader.next()
// get values
result.add(
ChatTableRecord(
chatId = reader.getString("chatId"),
title = reader.getString("title"),
description = reader.getString("description"),
type = reader.getString("type"),
mode = reader.getString("mode"),
ownerId = reader.getString("ownerId"),
participants = reader.getList("participants"),
createdAt = reader.getInstant("createdAt").toKotlinInstant(),
updatedAt = reader.getInstant("updatedAt").toKotlinInstant(),
isArchived = reader.getBoolean("isArchived"),
metadata = reader.getString("metadata"),
)
)
}
}
}
DbChatsResponseOk(result.map { it.toInternal() })
}
}

View File

@ -0,0 +1,56 @@
package ru.otus.messenger.repo.clickhouse
import kotlinx.datetime.Instant
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import ru.otus.messenger.common.NONE
import ru.otus.messenger.common.models.ChatArchiveFlag
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMetadata
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.ChatUserId
import ru.otus.messenger.common.models.MessengerChat
class ChatTableRecord(
val chatId: String?,
val title: String?,
val description: String?,
val type: String?,
val mode: String?,
val ownerId: String?,
val participants: List<String>,
val createdAt: Instant?,
val updatedAt: Instant?,
val isArchived: Boolean?,
val metadata: String?,
) {
constructor(model: MessengerChat): this(
chatId = model.id.takeIf { it != ChatId.NONE }?.asString(),
title = model.title.takeIf { it.isNotBlank() },
description = model.description.takeIf { it.isNotBlank() },
type = model.type.takeIf { it != ChatType.NONE }?.name,
mode = model.mode.takeIf { it != ChatMode.NONE }?.name,
ownerId = model.ownerId.takeIf { it != ChatOwnerId.NONE }?.asString(),
participants = model.participants.map { it.asString() },
createdAt = model.createdAt.takeIf { it != Instant.NONE },
updatedAt = model.updatedAt.takeIf { it != Instant.NONE },
isArchived = model.isArchived.takeIf { it != ChatArchiveFlag.NONE }?.asBoolean(),
metadata = model.metadata.takeIf { it != ChatMetadata.NONE }?.asString()
)
fun toInternal() = MessengerChat(
id = chatId?.let { ChatId(it) } ?: ChatId.NONE,
title = title ?: "",
description = description ?: "",
type = type?.let { ChatType.valueOf(it) } ?: ChatType.NONE,
mode = mode?.let { ChatMode.valueOf(it) } ?: ChatMode.NONE,
ownerId = ownerId?.let { ChatOwnerId(it) } ?: ChatOwnerId.NONE,
participants = participants.map { ChatUserId(it) }.toMutableSet(),
createdAt = createdAt ?: Instant.NONE,
updatedAt = updatedAt ?: Instant.NONE,
isArchived = isArchived?.let { ChatArchiveFlag(it) } ?: ChatArchiveFlag.NONE,
metadata = metadata?.let { ChatMetadata(Json.parseToJsonElement(it).jsonObject) } ?: ChatMetadata.NONE,
)
}

View File

@ -0,0 +1,8 @@
package ru.otus.messenger.repo.clickhouse
data class DbProperties(
val host: String = "localhost",
val port: Int = 8443,
val user: String = "default",
val password: String = "",
)

View File

@ -0,0 +1,36 @@
package ru.otus.messenger.repo.clickhouse
import io.mockk.coEvery
import io.mockk.mockk
import kotlin.test.Test
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.stubs.MessengerChatStub
class ChatRepoClickHouseTest {
private val repo = mockk<ChatRepoClickHouse>()
private val stub = MessengerChatStub.get()
@Test
fun testCreate() {
coEvery { repo.createChat(DbChatRequest(stub)) } returns DbChatResponseOk(stub)
}
@Test
fun testRead() {
coEvery { repo.readChat(DbChatIdRequest(stub.id)) } returns DbChatResponseOk(stub)
}
@Test
fun testDelete() {
coEvery { repo.deleteChat(DbChatIdRequest(stub.id)) } returns DbChatResponseOk(stub)
}
@Test
fun testSearch() {
coEvery { repo.searchChat(DbChatFilterRequest()) } returns DbChatsResponseOk(listOf(stub))
}
}

View File

@ -0,0 +1,11 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.coroutines)
implementation(project(":ok-messenger-common"))
testImplementation(kotlin("test-junit"))
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.repo.common
import ru.otus.messenger.common.models.MessengerChat
class ChatRepoInitialized(
private val repo: IRepoChatInitializable,
initObjects: Collection<MessengerChat> = emptyList(),
) : IRepoChatInitializable by repo {
@Suppress("unused")
val initializedObjects: List<MessengerChat> = save(initObjects).toList()
}

View File

@ -0,0 +1,8 @@
package ru.otus.messenger.repo.common
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.IRepoChat
interface IRepoChatInitializable : IRepoChat {
fun save(chats: Collection<MessengerChat>) : Collection<MessengerChat>
}

View File

@ -0,0 +1,8 @@
# Модуль `ok-messenger-repo-in-memory`
Модуль реализует интерфейс репозитария в виде кеша в памяти.
Используемые зависимости:
- **io.github.reactivecircus.cache4k:cache4k** - мультиплатформенная библиотека для кеширования [Документация](https://github.com/ReactiveCircus/cache4k)
- **com.benasher44:uuid** - реализация UUID для KMP [Документация](https://github.com/benasher44/uuid)

View File

@ -0,0 +1,17 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.coroutines)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.datetime)
implementation(libs.db.cache4k)
implementation(libs.uuid)
implementation(project(":ok-messenger-common"))
api(project(":ok-messenger-repo-common"))
testImplementation(kotlin("test-junit"))
testImplementation(project(":ok-messenger-repo-tests"))
}

View File

@ -0,0 +1,56 @@
package ru.otus.messenger.repo.inmemory
import kotlinx.datetime.Instant
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import ru.otus.messenger.common.NONE
import ru.otus.messenger.common.models.ChatArchiveFlag
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMetadata
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.ChatUserId
import ru.otus.messenger.common.models.MessengerChat
data class ChatEntity(
val chatId: String? = null,
val title: String? = null,
val description: String? = null,
val type: String? = null,
val mode: String? = null,
val ownerId: String? = null,
val participants: List<String> = listOf(),
val createdAt: Instant? = null,
val updatedAt: Instant? = null,
val isArchived: Boolean? = null,
val metadata: String? = null,
) {
constructor(model: MessengerChat): this(
chatId = model.id.takeIf { it != ChatId.NONE }?.asString(),
title = model.title.takeIf { it.isNotBlank() },
description = model.description.takeIf { it.isNotBlank() },
type = model.type.takeIf { it != ChatType.NONE }?.name,
mode = model.mode.takeIf { it != ChatMode.NONE }?.name,
ownerId = model.ownerId.takeIf { it != ChatOwnerId.NONE }?.asString(),
participants = model.participants.map { it.asString() },
createdAt = model.createdAt.takeIf { it != Instant.NONE },
updatedAt = model.updatedAt.takeIf { it != Instant.NONE },
isArchived = model.isArchived.takeIf { it != ChatArchiveFlag.NONE }?.asBoolean(),
metadata = model.metadata.takeIf { it != ChatMetadata.NONE }?.asString()
)
fun toInternal() = MessengerChat(
id = chatId?.let { ChatId(it) } ?: ChatId.NONE,
title = title ?: "",
description = description ?: "",
type = type?.let { ChatType.valueOf(it) } ?: ChatType.NONE,
mode = mode?.let { ChatMode.valueOf(it) } ?: ChatMode.NONE,
ownerId = ownerId?.let { ChatOwnerId(it) } ?: ChatOwnerId.NONE,
participants = participants.map { ChatUserId(it) }.toMutableSet(),
createdAt = createdAt ?: Instant.NONE,
updatedAt = updatedAt ?: Instant.NONE,
isArchived = isArchived?.let { ChatArchiveFlag(it) } ?: ChatArchiveFlag.NONE,
metadata = metadata?.let { ChatMetadata(Json.parseToJsonElement(it).jsonObject) } ?: ChatMetadata.NONE,
)
}

View File

@ -0,0 +1,158 @@
package ru.otus.messenger.repo.inmemory
import com.benasher44.uuid.uuid4
import io.github.reactivecircus.cache4k.Cache
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.otus.messenger.common.exceptions.RepoEmptyLockException
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.ChatRepoBase
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IDbChatResponse
import ru.otus.messenger.common.repo.IDbChatsResponse
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.common.repo.errorDb
import ru.otus.messenger.common.repo.errorEmptyId
import ru.otus.messenger.common.repo.errorNotFound
import ru.otus.messenger.common.repo.errorRepoConcurrency
import ru.otus.messenger.repo.common.IRepoChatInitializable
class ChatRepoInMemory(
ttl: Duration = 2.minutes,
val randomUuid: () -> String = { uuid4().toString() },
) : ChatRepoBase(), IRepoChat, IRepoChatInitializable {
private val mutex: Mutex = Mutex()
private val cache = Cache.Builder<String, ChatEntity>()
.expireAfterWrite(ttl)
.build()
override fun save(chats: Collection<MessengerChat>) = chats.map { chat ->
val entity = ChatEntity(chat)
require(entity.chatId != null)
cache.put(entity.chatId, entity)
chat
}
override suspend fun createChat(request: DbChatRequest): IDbChatResponse = tryChatMethod {
val key = randomUuid()
val chat = request.chat.copy(id = ChatId(key))
val entity = ChatEntity(chat)
mutex.withLock {
cache.put(key, entity)
}
DbChatResponseOk(chat)
}
override suspend fun readChat(request: DbChatIdRequest): IDbChatResponse = tryChatMethod {
val key = request.id.takeIf { it != ChatId.NONE }?.asString() ?: return@tryChatMethod errorEmptyId
mutex.withLock {
cache.get(key)
?.let {
DbChatResponseOk(it.toInternal())
} ?: errorNotFound(request.id)
}
}
override suspend fun updateChat(request: DbChatRequest): IDbChatResponse = tryChatMethod {
val requestChat = request.chat
val chatId = requestChat.id.takeIf { it != ChatId.NONE } ?: return@tryChatMethod errorEmptyId
val key = chatId.asString()
mutex.withLock {
val oldChat = cache.get(key)?.toInternal()
when {
oldChat == null -> errorNotFound(chatId)
oldChat.id == ChatId.NONE -> errorDb(RepoEmptyLockException(chatId))
oldChat.id != chatId -> errorRepoConcurrency(oldChat)
else -> {
val newAd = requestChat.copy(id = ChatId(randomUuid()))
val entity = ChatEntity(newAd)
cache.put(key, entity)
DbChatResponseOk(newAd)
}
}
}
}
override suspend fun deleteChat(request: DbChatIdRequest): IDbChatResponse = tryChatMethod {
val chatId = request.id.takeIf { it != ChatId.NONE } ?: return@tryChatMethod errorEmptyId
val key = chatId.asString()
mutex.withLock {
val oldChat = cache.get(key)?.toInternal()
when {
oldChat == null -> errorNotFound(chatId)
oldChat.id == ChatId.NONE -> errorDb(RepoEmptyLockException(chatId))
oldChat.id != chatId -> errorRepoConcurrency(oldChat)
else -> {
cache.invalidate(key)
DbChatResponseOk(oldChat)
}
}
}
}
override suspend fun searchChat(request: DbChatFilterRequest): IDbChatsResponse = tryChatsMethod {
val result: List<MessengerChat> = cache.asMap().asSequence()
.filter { entry ->
request.chatType.takeIf { it != ChatType.NONE }?.let {
it.name == entry.value.type
} ?: true
}
.filter { entry ->
request.chatMode.takeIf { it != ChatMode.NONE }?.let {
it.name == entry.value.mode
} ?: true
}
.filter { entry ->
request.ownerId.takeIf { it != ChatOwnerId.NONE }?.let {
it.asString() == entry.value.ownerId
} ?: true
}
.filter { entry ->
request.searchFields.takeIf { it.isNotEmpty() }?.let { searchFields ->
searchFields.any { searchField ->
when (searchField) {
is ChatSearchFilter.StringSearchField -> {
when (searchField.fieldName.lowercase()) {
"chatid" -> assertAction(searchField.stringValue, entry.value.chatId!!, searchField.action)
"title" -> assertAction(searchField.stringValue, entry.value.title!!, searchField.action)
"ownerid" -> assertAction(searchField.stringValue, entry.value.ownerId!!, searchField.action)
"description" -> assertAction(searchField.stringValue, entry.value.description!!, searchField.action)
"isarchived" -> assertAction(searchField.stringValue, entry.value.isArchived.toString(), searchField.action)
"metadata" -> assertAction(searchField.stringValue, entry.value.metadata!!, searchField.action)
else -> false
}
}
else -> false
}
}
} ?: true
}
.map { it.value.toInternal() }
.toList()
DbChatsResponseOk(result)
}
fun assertAction(expected: String, actual: String, action: ChatSearchFilter.SearchAction) =
when (action) {
ChatSearchFilter.SearchAction.EQUALS -> expected.toString() == actual
ChatSearchFilter.SearchAction.CONTAINS -> expected.toString().contains(actual, ignoreCase = true)
ChatSearchFilter.SearchAction.MORE -> expected > actual
ChatSearchFilter.SearchAction.LESS -> expected < actual
else -> false
}
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.repo.inmemory
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.tests.RepoChatCreateTest
class ChatRepoInMemoryCreateTest : RepoChatCreateTest() {
override val repo = ChatRepoInitialized(
ChatRepoInMemory(randomUuid = { uuidNew.asString() }),
initObjects = initObjects,
)
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.repo.inmemory
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.tests.RepoChatDeleteTest
class ChatRepoInMemoryDeleteTest : RepoChatDeleteTest() {
override val repo = ChatRepoInitialized(
ChatRepoInMemory(),
initObjects = initObjects,
)
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.repo.inmemory
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.tests.RepoChatReadTest
class ChatRepoInMemoryReadTest : RepoChatReadTest() {
override val repo = ChatRepoInitialized(
ChatRepoInMemory(),
initObjects = initObjects,
)
}

View File

@ -0,0 +1,11 @@
package ru.otus.messenger.repo.inmemory
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.tests.RepoChatSearchTest
class ChatRepoInMemorySearchTest : RepoChatSearchTest() {
override val repo = ChatRepoInitialized(
ChatRepoInMemory(),
initObjects = initObjects,
)
}

View File

@ -0,0 +1,12 @@
package ru.otus.messenger.repo.inmemory
import ru.otus.messenger.repo.common.ChatRepoInitialized
import ru.otus.messenger.repo.tests.RepoChatDeleteTest
import ru.otus.messenger.repo.tests.RepoChatUpdateTest
class ChatRepoInMemoryUpdateTest : RepoChatUpdateTest() {
override val repo = ChatRepoInitialized(
ChatRepoInMemory(),
initObjects = RepoChatDeleteTest.Companion.initObjects,
)
}

View File

@ -0,0 +1,3 @@
# Модуль `ok-messenger-repo-stubs`
Модуль реализует интерфейс репозитария в виде стабов.

View File

@ -0,0 +1,13 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlin.coroutines)
implementation(project(":ok-messenger-common"))
implementation(project(":ok-messenger-stubs"))
testImplementation(kotlin("test-junit"))
testImplementation(project(":ok-messenger-repo-tests"))
}

View File

@ -0,0 +1,47 @@
package ru.otus.messenger.repo.stub
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IDbChatResponse
import ru.otus.messenger.common.repo.IDbChatsResponse
import ru.otus.messenger.common.repo.IRepoChat
import ru.otus.messenger.stubs.MessengerChatStub
class ChatRepoStub : IRepoChat {
override suspend fun createChat(request: DbChatRequest): IDbChatResponse {
return DbChatResponseOk(
data = MessengerChatStub.get(),
)
}
override suspend fun readChat(request: DbChatIdRequest): IDbChatResponse {
return DbChatResponseOk(
data = MessengerChatStub.get(),
)
}
override suspend fun updateChat(request: DbChatRequest): IDbChatResponse {
return DbChatResponseOk(
data = MessengerChatStub.get(),
)
}
override suspend fun deleteChat(request: DbChatIdRequest): IDbChatResponse {
return DbChatResponseOk(
data = MessengerChatStub.get(),
)
}
override suspend fun searchChat(request: DbChatFilterRequest): IDbChatsResponse {
return DbChatsResponseOk(
data = MessengerChatStub.prepareSearchList(
chatTitle = "",
chatType = ChatType.NONE,
),
)
}
}

View File

@ -0,0 +1,16 @@
plugins {
id("build-jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.datetime)
api(libs.kotlin.coroutines)
api(libs.kotlin.coroutines.test)
implementation(project(":ok-messenger-common"))
implementation(project(":ok-messenger-repo-common"))
implementation(kotlin("test-junit"))
testImplementation(project(":ok-messenger-stubs"))
}

View File

@ -0,0 +1,21 @@
package ru.otus.messenger.repo.tests
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
abstract class BaseInitChats(private val operation: String): IInitObjects<MessengerChat> {
fun createInitTestModel(
suffix: String,
chatOwnerId: ChatOwnerId = ChatOwnerId("TestOwnerId"),
chatType: ChatType = ChatType.GROUP,
chatMode: ChatMode = ChatMode.WORK,
) = MessengerChat(
id = ChatId("chat-repo-$operation-$suffix"),
ownerId = chatOwnerId,
type = chatType,
mode = chatMode,
)
}

View File

@ -0,0 +1,44 @@
package ru.otus.messenger.repo.tests
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IDbChatResponse
import ru.otus.messenger.common.repo.IDbChatsResponse
import ru.otus.messenger.common.repo.IRepoChat
class ChatRepositoryMock(
private val invokeCreateChat: (DbChatRequest) -> IDbChatResponse = { DEFAULT_CHAT_SUCCESS_EMPTY_MOCK },
private val invokeReadChat: (DbChatIdRequest) -> IDbChatResponse = { DEFAULT_CHAT_SUCCESS_EMPTY_MOCK },
private val invokeUpdateChat: (DbChatRequest) -> IDbChatResponse = { DEFAULT_CHAT_SUCCESS_EMPTY_MOCK },
private val invokeDeleteChat: (DbChatIdRequest) -> IDbChatResponse = { DEFAULT_CHAT_SUCCESS_EMPTY_MOCK },
private val invokeSearchChat: (DbChatFilterRequest) -> IDbChatsResponse = { DEFAULT_CHATS_SUCCESS_EMPTY_MOCK },
): IRepoChat {
override suspend fun createChat(request: DbChatRequest): IDbChatResponse {
return invokeCreateChat(request)
}
override suspend fun readChat(request: DbChatIdRequest): IDbChatResponse {
return invokeReadChat(request)
}
override suspend fun updateChat(request: DbChatRequest): IDbChatResponse {
return invokeUpdateChat(request)
}
override suspend fun deleteChat(request: DbChatIdRequest): IDbChatResponse {
return invokeDeleteChat(request)
}
override suspend fun searchChat(request: DbChatFilterRequest): IDbChatsResponse {
return invokeSearchChat(request)
}
companion object {
val DEFAULT_CHAT_SUCCESS_EMPTY_MOCK = DbChatResponseOk(MessengerChat())
val DEFAULT_CHATS_SUCCESS_EMPTY_MOCK = DbChatsResponseOk(emptyList())
}
}

View File

@ -0,0 +1,5 @@
package ru.otus.messenger.repo.tests
internal interface IInitObjects<T> {
val initObjects: List<T>
}

View File

@ -0,0 +1,61 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotEquals
import kotlinx.datetime.Instant
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import ru.otus.messenger.common.models.ChatArchiveFlag
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.ChatMetadata
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.repo.common.IRepoChatInitializable
abstract class RepoChatCreateTest {
abstract val repo: IRepoChatInitializable
protected open val uuidNew = ChatId("10000000-0000-0000-0000-000000000001")
private val createObj = MessengerChat(
id = ChatId("Test"),
title = "",
description = "",
type = ChatType.GROUP,
mode = ChatMode.PERSONAL,
ownerId = ChatOwnerId("Test123"),
participants = mutableSetOf(),
createdAt = Instant.fromEpochMilliseconds(123456),
updatedAt = Instant.fromEpochMilliseconds(123456),
isArchived = ChatArchiveFlag.NONE,
metadata = ChatMetadata(
buildJsonObject {
put("sampleId", "test")
put("case", "create object")
put("info", "Why should I repeat this initialization one more time???")
}
)
)
@Test
fun createSuccess() = runRepoTest {
val result = repo.createChat(DbChatRequest(createObj))
val expected = createObj
assertIs<DbChatResponseOk>(result)
assertEquals(uuidNew, result.data.id)
assertEquals(expected.ownerId, result.data.ownerId)
assertEquals(expected.type, result.data.type)
assertEquals(expected.mode, result.data.mode)
assertEquals(expected.createdAt, result.data.createdAt)
assertNotEquals(ChatId.NONE, result.data.id)
}
companion object : BaseInitChats("create") {
override val initObjects: List<MessengerChat> = emptyList()
}
}

View File

@ -0,0 +1,42 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.IRepoChat
abstract class RepoChatDeleteTest {
abstract val repo: IRepoChat
protected open val deleteSuccess = initObjects[0]
protected open val notFoundId = ChatId("repo-delete-notFound")
@Test
fun deleteSuccess() = runRepoTest {
val result = repo.deleteChat(DbChatIdRequest(deleteSuccess.id))
assertIs<DbChatResponseOk>(result)
assertEquals(deleteSuccess.ownerId, result.data.ownerId)
assertEquals(deleteSuccess.type, result.data.type)
}
@Test
fun deleteNotFound() = runRepoTest {
val result = repo.deleteChat(DbChatIdRequest(notFoundId))
assertIs<DbChatResponseErr>(result)
val error = result.errors.find { it.code == "repo-not-found" }
assertNotNull(error)
}
companion object : BaseInitChats("delete") {
override val initObjects: List<MessengerChat> = listOf(
createInitTestModel("delete"),
)
}
}

View File

@ -0,0 +1,41 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertEquals
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.IRepoChat
abstract class RepoChatReadTest {
abstract val repo: IRepoChat
protected open val readSuccess = initObjects[0]
@Test
fun readSuccess() = runRepoTest {
val result = repo.readChat(DbChatIdRequest(readSuccess.id))
assertIs<DbChatResponseOk>(result)
assertEquals(readSuccess, result.data)
}
@Test
fun readNotFound() = runRepoTest {
val result = repo.readChat(DbChatIdRequest(notFoundId))
assertIs<DbChatResponseErr>(result)
val error = result.errors.find { it.code == "repo-not-found" }
assertEquals("id", error?.field)
}
companion object : BaseInitChats("read") {
override val initObjects: List<MessengerChat> = listOf(
createInitTestModel("read")
)
val notFoundId = ChatId("repo-read-notFound")
}
}

View File

@ -0,0 +1,97 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import ru.otus.messenger.common.models.ChatMode
import ru.otus.messenger.common.models.ChatOwnerId
import ru.otus.messenger.common.models.ChatSearchFilter
import ru.otus.messenger.common.models.ChatType
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.common.repo.IRepoChat
abstract class RepoChatSearchTest {
abstract val repo: IRepoChat
protected open val initializedObjects: List<MessengerChat> = initObjects
@Test
fun searchByOwnerId() = runRepoTest {
val result = repo.searchChat(
DbChatFilterRequest(ownerId = ChatOwnerId("TestOwnerId"))
)
assertIs<DbChatsResponseOk>(result)
val expected = listOf(initializedObjects[1], initializedObjects[3]).sortedBy { it.id.asString() }
assertEquals(expected, result.data.sortedBy { it.id.asString() })
}
@Test
fun searchByTypeAndMode() = runRepoTest {
val result = repo.searchChat(
DbChatFilterRequest(
chatType = ChatType.GROUP,
chatMode = ChatMode.WORK,
)
)
assertIs<DbChatsResponseOk>(result)
val expected = listOf(initializedObjects[0]).sortedBy { it.id.asString() }
assertEquals(expected, result.data.sortedBy { it.id.asString() })
}
@Test
fun searchContent() = runRepoTest {
val result = repo.searchChat(
DbChatFilterRequest(
searchFields = listOf(
ChatSearchFilter.StringSearchField(
fieldName = "ownerId",
action = ChatSearchFilter.SearchAction.EQUALS,
stringValue = "Hmm",
)
)
)
)
assertIs<DbChatsResponseOk>(result)
val expected = listOf(initializedObjects[0], initializedObjects[4]).sortedBy { it.id.asString() }
assertEquals(expected, result.data.sortedBy { it.id.asString() })
}
companion object: BaseInitChats("search") {
private val searchOwnerId = ChatOwnerId("TestOwnerId")
private val searchType = ChatType.CHANNEL
private val searchMode = ChatMode.PERSONAL
override val initObjects: List<MessengerChat> = listOf(
createInitTestModel(
"test1",
chatOwnerId = ChatOwnerId("Hmm"),
chatType = ChatType.GROUP,
chatMode = ChatMode.WORK,
),
createInitTestModel(
"test2",
chatOwnerId = searchOwnerId,
chatType = searchType,
chatMode = searchMode,
),
createInitTestModel(
"test3",
chatOwnerId = ChatOwnerId("Eee!"),
chatType = searchType,
chatMode = ChatMode.WORK,
),
createInitTestModel(
"test4",
chatOwnerId = searchOwnerId,
chatType = ChatType.PRIVATE
),
createInitTestModel(
"test5",
chatOwnerId = ChatOwnerId("Hmm"),
chatType = ChatType.PRIVATE,
chatMode = ChatMode.WORK,
),
)
}
}

View File

@ -0,0 +1,90 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import ru.otus.messenger.common.models.ChatId
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseErr
import ru.otus.messenger.common.repo.DbChatResponseErrWithData
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.IRepoChat
abstract class RepoChatUpdateTest {
abstract val repo: IRepoChat
protected open val updateSucc = initObjects[0]
protected open val updateConc = initObjects[1]
protected val updateIdNotFound = ChatId("chat-repo-update-not-found")
protected val chatIdBad = ChatId("20000000-0000-0000-0000-000000000009")
protected open val initializedObjects: List<MessengerChat> = initObjects
private val reqUpdateSucc by lazy {
MessengerChat(
id = updateSucc.id,
title = "update object",
description = "update object description",
ownerId = updateSucc.ownerId,
type = updateSucc.type,
mode = updateSucc.mode,
)
}
private val reqUpdateNotFound = MessengerChat(
id = updateIdNotFound,
title = "update object not found",
description = "update object not found description",
ownerId = updateConc.ownerId,
type = updateConc.type,
mode = updateConc.mode,
)
private val reqUpdateConc by lazy {
MessengerChat(
id = chatIdBad,
title = "update object not found",
description = "update object not found description",
ownerId = updateConc.ownerId,
type = updateConc.type,
mode = updateConc.mode,
)
}
@Test
fun updateSuccess() = runRepoTest {
val result = repo.updateChat(DbChatRequest(reqUpdateSucc))
println("ERRORS: ${(result as? DbChatResponseErr)?.errors}")
println("ERRORSWD: ${(result as? DbChatResponseErrWithData)?.errors}")
assertIs<DbChatResponseOk>(result)
assertEquals(reqUpdateSucc.id, result.data.id)
assertEquals(reqUpdateSucc.title, result.data.title)
assertEquals(reqUpdateSucc.description, result.data.description)
assertEquals(reqUpdateSucc.type, result.data.type)
assertEquals(reqUpdateSucc.mode, result.data.mode)
}
@Test
fun updateNotFound() = runRepoTest {
val result = repo.updateChat(DbChatRequest(reqUpdateNotFound))
assertIs<DbChatResponseErr>(result)
val error = result.errors.find { it.code == "repo-not-found" }
assertEquals("id", error?.field)
}
@Test
fun updateConcurrencyError() = runRepoTest {
val result = repo.updateChat(DbChatRequest(reqUpdateConc))
println(result)
assertIs<DbChatResponseErrWithData>(result)
val error = result.errors.find { it.code == "repo-concurrency" }
assertEquals("lock", error?.field)
assertEquals(updateConc, result.data)
}
companion object: BaseInitChats("update") {
override val initObjects: List<MessengerChat> = listOf(
createInitTestModel("update"),
createInitTestModel("updateConc"),
)
}
}

View File

@ -0,0 +1,13 @@
package ru.otus.messenger.repo.tests
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.minutes
fun runRepoTest(testBody: suspend TestScope.() -> Unit) = runTest(timeout = 2.minutes) {
withContext(Dispatchers.Default) {
testBody()
}
}

View File

@ -0,0 +1,58 @@
package ru.otus.messenger.repo.tests
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
import ru.otus.messenger.common.models.MessengerChat
import ru.otus.messenger.common.repo.DbChatFilterRequest
import ru.otus.messenger.common.repo.DbChatIdRequest
import ru.otus.messenger.common.repo.DbChatRequest
import ru.otus.messenger.common.repo.DbChatResponseOk
import ru.otus.messenger.common.repo.DbChatsResponseOk
import ru.otus.messenger.stubs.MessengerChatStub
class ChatRepositoryMockTest {
private val repo = ChatRepositoryMock(
invokeCreateChat = { DbChatResponseOk(MessengerChatStub.prepareResult { title = "create" }) },
invokeReadChat = { DbChatResponseOk(MessengerChatStub.prepareResult { title = "read" }) },
invokeUpdateChat = { DbChatResponseOk(MessengerChatStub.prepareResult { title = "update" }) },
invokeDeleteChat = { DbChatResponseOk(MessengerChatStub.prepareResult { title = "delete" }) },
invokeSearchChat = { DbChatsResponseOk(listOf(MessengerChatStub.prepareResult { title = "search" })) },
)
@Test
fun mockCreate() = runTest {
val result = repo.createChat(DbChatRequest(MessengerChat()))
assertIs<DbChatResponseOk>(result)
assertEquals("create", result.data.title)
}
@Test
fun mockRead() = runTest {
val result = repo.readChat(DbChatIdRequest(MessengerChat()))
assertIs<DbChatResponseOk>(result)
assertEquals("read", result.data.title)
}
@Test
fun mockUpdate() = runTest {
val result = repo.updateChat(DbChatRequest(MessengerChat()))
assertIs<DbChatResponseOk>(result)
assertEquals("update", result.data.title)
}
@Test
fun mockDelete() = runTest {
val result = repo.deleteChat(DbChatIdRequest(MessengerChat()))
assertIs<DbChatResponseOk>(result)
assertEquals("delete", result.data.title)
}
@Test
fun mockSearch() = runTest {
val result = repo.searchChat(DbChatFilterRequest())
assertIs<DbChatsResponseOk>(result)
assertEquals("search", result.data.first().title)
}
}

View File

@ -30,3 +30,8 @@ include(":ok-messenger-common")
include(":ok-messenger-stubs")
include(":ok-messenger-app")
include(":ok-messenger-biz")
include(":ok-messenger-repo-common")
include(":ok-messenger-repo-inmemory")
include(":ok-messenger-repo-clickhouse")
include(":ok-messenger-repo-stubs")
include(":ok-messenger-repo-tests")