From 7e95fd091be04ce54a831059f6391a7edc7c50a6 Mon Sep 17 00:00:00 2001 From: vityaman Date: Fri, 19 Apr 2024 17:20:36 +0300 Subject: [PATCH] #60 Optional getById --- .../botalka/api/http/endpoint/UserHttpApi.kt | 2 + .../api/http/error/DomainExceptionCodes.kt | 2 +- .../api/http/error/DomainExceptionMapping.kt | 3 +- .../domain/exception/NotFoundExtension.kt | 4 ++ .../vityaman/lms/botalka/logic/UserService.kt | 2 +- .../logic/basic/BasicPromotionService.kt | 19 +++++---- .../botalka/logic/basic/BasicUserService.kt | 2 +- .../lms/botalka/storage/PromotionStorage.kt | 2 +- .../lms/botalka/storage/UserStorage.kt | 2 +- .../storage/jooq/JooqPromotionStorage.kt | 5 ++- .../botalka/storage/jooq/JooqUserStorage.kt | 6 +-- .../botalka/api/http/endpoint/UserApiTest.kt | 42 ++++++++++--------- 12 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundExtension.kt diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserHttpApi.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserHttpApi.kt index 3a822c3..eec6870 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserHttpApi.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserHttpApi.kt @@ -8,6 +8,7 @@ import ru.vityaman.lms.botalka.api.http.message.toModel import ru.vityaman.lms.botalka.api.http.server.UserDraftMessage import ru.vityaman.lms.botalka.api.http.server.UserMessage import ru.vityaman.lms.botalka.api.http.server.apis.UserApi +import ru.vityaman.lms.botalka.domain.exception.orNotFound import ru.vityaman.lms.botalka.domain.model.User import ru.vityaman.lms.botalka.logic.UserService @@ -18,6 +19,7 @@ class UserHttpApi( override suspend fun userIdGet(id: Int): ResponseEntity { val userId = User.Id(id) val user = userService.getById(userId) + .orNotFound("User with id ${userId.number} not found") return ResponseEntity.ok(user.toMessage()) } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt index e1b691d..47d045d 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt @@ -25,5 +25,5 @@ fun DomainException.toResponseEntity() = code = this.httpCode.value(), status = this.httpCode.reasonPhrase, message = this.message!!, - ) + ), ) diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt index 6cf1a5a..40f864f 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt @@ -3,11 +3,10 @@ package ru.vityaman.lms.botalka.api.http.error import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice import ru.vityaman.lms.botalka.domain.exception.DomainException -import ru.vityaman.lms.botalka.domain.exception.InvalidValueException @RestControllerAdvice class DomainExceptionMapping { @ExceptionHandler(DomainException::class) - fun handle(exception: InvalidValueException) = + fun handle(exception: DomainException) = exception.toResponseEntity() } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundExtension.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundExtension.kt new file mode 100644 index 0000000..1a10b9a --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundExtension.kt @@ -0,0 +1,4 @@ +package ru.vityaman.lms.botalka.domain.exception + +fun T?.orNotFound(message: String): T = + this ?: throw NotFoundException(message) diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/UserService.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/UserService.kt index 60e7879..3c97f21 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/UserService.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/UserService.kt @@ -3,7 +3,7 @@ package ru.vityaman.lms.botalka.logic import ru.vityaman.lms.botalka.domain.model.User interface UserService { - suspend fun getById(id: User.Id): User + suspend fun getById(id: User.Id): User? suspend fun create(user: User.Draft): User suspend fun promote(id: User.Id, role: User.Role) } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicPromotionService.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicPromotionService.kt index 5c0928f..f6a270b 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicPromotionService.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicPromotionService.kt @@ -3,6 +3,7 @@ package ru.vityaman.lms.botalka.logic.basic import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import ru.vityaman.lms.botalka.domain.exception.PromotionRequestResolvedException +import ru.vityaman.lms.botalka.domain.exception.orNotFound import ru.vityaman.lms.botalka.domain.model.PromotionRequest import ru.vityaman.lms.botalka.logic.PromotionService import ru.vityaman.lms.botalka.logic.UserService @@ -19,18 +20,22 @@ class BasicPromotionService( storage.create(promotion) override suspend fun approve(id: PromotionRequest.Id) { - val request = storage.getById(id) - if (request.isResolved) { - throw PromotionRequestResolvedException(request.id) - } + val request = getUnresolvedRequestById(id) storage.updateStatus(request.id, PromotionRequest.Status.APPROVED) userService.promote(request.user, request.role) } override suspend fun reject(id: PromotionRequest.Id) { - if (storage.getById(id).isResolved) { - throw PromotionRequestResolvedException(id) - } + getUnresolvedRequestById(id) storage.updateStatus(id, PromotionRequest.Status.REJECTED) } + + private suspend fun getUnresolvedRequestById(id: PromotionRequest.Id) = + storage.getById(id) + .orNotFound("Promotion request with id ${id.number} not found") + .also { + if (it.isResolved) { + throw PromotionRequestResolvedException(it.id) + } + } } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicUserService.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicUserService.kt index b95ce10..e4fc70c 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicUserService.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/logic/basic/BasicUserService.kt @@ -12,7 +12,7 @@ import ru.vityaman.lms.botalka.storage.UserStorage class BasicUserService( @Autowired private val storage: UserStorage, ) : UserService { - override suspend fun getById(id: User.Id): User = + override suspend fun getById(id: User.Id): User? = storage.getById(id) override suspend fun create(user: User.Draft): User = diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/PromotionStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/PromotionStorage.kt index 73afd37..7876f50 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/PromotionStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/PromotionStorage.kt @@ -10,5 +10,5 @@ interface PromotionStorage { status: PromotionRequest.Status, ) - suspend fun getById(id: PromotionRequest.Id): PromotionRequest + suspend fun getById(id: PromotionRequest.Id): PromotionRequest? } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/UserStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/UserStorage.kt index f54eb8c..90e95a4 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/UserStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/UserStorage.kt @@ -5,7 +5,7 @@ import ru.vityaman.lms.botalka.domain.model.Teacher import ru.vityaman.lms.botalka.domain.model.User interface UserStorage { - suspend fun getById(id: User.Id): User + suspend fun getById(id: User.Id): User? suspend fun create(user: User.Draft): User suspend fun create(teacher: Teacher) suspend fun create(student: Student) diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqPromotionStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqPromotionStorage.kt index c448256..42ce2d6 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqPromotionStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqPromotionStorage.kt @@ -1,6 +1,7 @@ package ru.vityaman.lms.botalka.storage.jooq import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Repository import reactor.kotlin.core.publisher.toMono @@ -42,11 +43,11 @@ class JooqPromotionStorage( .map { } .awaitSingle() - override suspend fun getById(id: PromotionRequest.Id): PromotionRequest = + override suspend fun getById(id: PromotionRequest.Id): PromotionRequest? = database.execute .selectFrom(PROMOTION_REQUEST) .where(PROMOTION_REQUEST.ID.equal(id.number)) .toMono() .map { it.toModel() } - .awaitSingle() + .awaitSingleOrNull() } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt index de1dd6d..72b1e08 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/storage/jooq/JooqUserStorage.kt @@ -17,14 +17,14 @@ import ru.vityaman.lms.botalka.storage.jooq.tables.references.USER class JooqUserStorage( @Autowired private val database: JooqDatabase, ) : UserStorage { - override suspend fun getById(id: User.Id): User = + override suspend fun getById(id: User.Id): User? = database.execute .selectFrom(USER) .where(USER.ID.equal(id.number)) .toMono() .map { it.toModel() } - .awaitSingle() - .copy( + .awaitSingleOrNull() + ?.copy( roles = setOfNotNull( teacher(id)?.let { User.Role.TEACHER }, student(id)?.let { User.Role.STUDENT }, diff --git a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserApiTest.kt b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserApiTest.kt index f4f8750..593aaac 100644 --- a/botalka/src/test/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserApiTest.kt +++ b/botalka/src/test/kotlin/ru/vityaman/lms/botalka/api/http/endpoint/UserApiTest.kt @@ -8,16 +8,15 @@ import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.web.reactive.function.client.WebClientResponseException import ru.vityaman.lms.botalka.BotalkaTestSuite -import ru.vityaman.lms.botalka.api.http.message.toMessage -import ru.vityaman.lms.botalka.api.http.server.apis.UserApi +import ru.vityaman.lms.botalka.api.http.client.UserApiClient import ru.vityaman.lms.botalka.domain.model.User -@SpringBootTest class UserApiTest( - @Autowired private val api: UserApi, + @Autowired private val api: UserApiClient, ) : BotalkaTestSuite() { private val drafts = listOf( User.Draft(User.Alias("admin")), @@ -27,19 +26,17 @@ class UserApiTest( ) @Test - fun createAndGetUser() { - val draftToResultList = runBlocking { + fun createAndGetUser() = runBlocking { + val draftToResultList = drafts.map { draft -> - val message = draft.toMessage() - val result = async { api.userPost(message).body } + val result = async { api.get(api.register(draft.alias)) } Pair(draft, result) }.map { (draft, result) -> - Pair(draft, result.await() ?: throw NullPointerException()) + Pair(draft, result.await()) } - } draftToResultList.forEach { (l, r) -> - l.alias.text shouldBe r.alias + l.alias.text shouldBe r.alias.text r.roles shouldHaveSize 0 } @@ -48,15 +45,20 @@ class UserApiTest( val ids = results.map { it.id } ids.shouldBeUnique() - runBlocking { - results.forEach { expected -> - launch { - val actual = api.userIdGet(expected.id).body!! - expected.id shouldBe actual.id - expected.alias shouldBe actual.alias - expected.roles shouldContainExactly actual.roles - } + results.forEach { expected -> + launch { + val actual = api.get(expected.id) + expected.id shouldBe actual.id + expected.alias shouldBe actual.alias + expected.roles shouldContainExactly actual.roles } } } + + @Test + fun userNotFound(): Unit = runBlocking { + assertThrows { + api.get(User.Id(666)) + } + } }