From 2c61e4cca79aa8ca033d7e3023b2f677e78c3184 Mon Sep 17 00:00:00 2001 From: Victor Smirnov <53015676+vityaman@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:28:57 +0300 Subject: [PATCH] #60 Added InvalidArgument and NotFound exceptions (#61) --- .../botalka/api/http/endpoint/UserHttpApi.kt | 2 + .../api/http/error/DomainExceptionCodes.kt | 29 +++++++++++ .../api/http/error/DomainExceptionMapping.kt | 12 +++++ .../http/message/WorkspaceMappingToMessage.kt | 51 +++++++++++++++++++ ...eMapping.kt => WorkspaceMappingToModel.kt} | 47 ----------------- .../vityaman/lms/botalka/commons/Require.kt | 12 ----- .../ru/vityaman/lms/botalka/domain/Require.kt | 15 ++++++ .../domain/exception/InvalidValueException.kt | 4 ++ .../domain/exception/NotFoundException.kt | 4 ++ .../domain/exception/NotFoundExtension.kt | 4 ++ .../lms/botalka/domain/model/Homework.kt | 4 +- .../botalka/domain/model/PromotionRequest.kt | 2 +- .../vityaman/lms/botalka/domain/model/User.kt | 4 +- .../lms/botalka/domain/model/Workspace.kt | 2 +- .../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 +-- .../src/main/resources/static/openapi/api.yml | 28 +++++++++- .../botalka/api/http/endpoint/UserApiTest.kt | 42 +++++++-------- 23 files changed, 197 insertions(+), 103 deletions(-) create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToMessage.kt rename botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/{WorkspaceMapping.kt => WorkspaceMappingToModel.kt} (50%) delete mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/commons/Require.kt create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/Require.kt create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/InvalidValueException.kt create mode 100644 botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundException.kt 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 new file mode 100644 index 0000000..47d045d --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionCodes.kt @@ -0,0 +1,29 @@ +package ru.vityaman.lms.botalka.api.http.error + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import ru.vityaman.lms.botalka.api.http.server.GeneralErrorMessage +import ru.vityaman.lms.botalka.domain.exception.DomainException +import ru.vityaman.lms.botalka.domain.exception.InvalidValueException +import ru.vityaman.lms.botalka.domain.exception.NotFoundException + +val DomainException.httpCode: HttpStatus + get() = when (this) { + is InvalidValueException -> HttpStatus.NOT_FOUND + is NotFoundException -> HttpStatus.NOT_FOUND + else -> TODO() + } + +val HttpStatus.reason: String + get() = this.reasonPhrase + +fun DomainException.toResponseEntity() = + ResponseEntity + .status(this.httpCode) + .body( + GeneralErrorMessage( + 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 new file mode 100644 index 0000000..40f864f --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/error/DomainExceptionMapping.kt @@ -0,0 +1,12 @@ +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 + +@RestControllerAdvice +class DomainExceptionMapping { + @ExceptionHandler(DomainException::class) + fun handle(exception: DomainException) = + exception.toResponseEntity() +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToMessage.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToMessage.kt new file mode 100644 index 0000000..604e5d8 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToMessage.kt @@ -0,0 +1,51 @@ +package ru.vityaman.lms.botalka.api.http.message + +import ru.vityaman.lms.botalka.api.http.server.WorkspaceCommentMessage +import ru.vityaman.lms.botalka.api.http.server.WorkspaceEventKindMessage +import ru.vityaman.lms.botalka.api.http.server.WorkspaceEventMessage +import ru.vityaman.lms.botalka.api.http.server.WorkspaceFeedbackMessage +import ru.vityaman.lms.botalka.api.http.server.WorkspaceSubmissionMessage +import ru.vityaman.lms.botalka.domain.model.Workspace + +private val Workspace.Event.kind + get() = + when (this) { + is Workspace.Comment -> WorkspaceEventKindMessage.COMMENT + is Workspace.Feedback -> WorkspaceEventKindMessage.FEEDBACK + is Workspace.Submission -> WorkspaceEventKindMessage.SUBMISSION + } + +fun Workspace.Comment.toMessage() = + WorkspaceCommentMessage( + kind = this.kind, + id = this.id.number, + producerId = this.producer.number, + text = this.text, + creationMoment = this.creationMoment, + ) + +fun Workspace.Feedback.toMessage() = + WorkspaceFeedbackMessage( + kind = this.kind, + id = this.id.number, + producerId = this.producer.number, + comment = this.comment, + score = this.score?.value?.toInt(), + creationMoment = this.creationMoment, + ) + +fun Workspace.Submission.toMessage() = + WorkspaceSubmissionMessage( + kind = this.kind, + id = this.id.number, + producerId = this.producer.number, + note = this.note, + creationMoment = this.creationMoment, + ) + +fun Workspace.Event.toMessage(): WorkspaceEventMessage = + when (this) { + is Workspace.Comment -> this.toMessage() + is Workspace.Feedback -> this.toMessage() + is Workspace.Submission -> this.toMessage() + } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMapping.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToModel.kt similarity index 50% rename from botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMapping.kt rename to botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToModel.kt index ff710b7..3623c81 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMapping.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/api/http/message/WorkspaceMappingToModel.kt @@ -1,14 +1,9 @@ package ru.vityaman.lms.botalka.api.http.message import ru.vityaman.lms.botalka.api.http.server.WorkspaceCommentDraftMessage -import ru.vityaman.lms.botalka.api.http.server.WorkspaceCommentMessage import ru.vityaman.lms.botalka.api.http.server.WorkspaceEventDraftMessage -import ru.vityaman.lms.botalka.api.http.server.WorkspaceEventKindMessage -import ru.vityaman.lms.botalka.api.http.server.WorkspaceEventMessage import ru.vityaman.lms.botalka.api.http.server.WorkspaceFeedbackDraftMessage -import ru.vityaman.lms.botalka.api.http.server.WorkspaceFeedbackMessage import ru.vityaman.lms.botalka.api.http.server.WorkspaceSubmissionDraftMessage -import ru.vityaman.lms.botalka.api.http.server.WorkspaceSubmissionMessage import ru.vityaman.lms.botalka.domain.model.Homework import ru.vityaman.lms.botalka.domain.model.Teacher import ru.vityaman.lms.botalka.domain.model.User @@ -56,45 +51,3 @@ fun WorkspaceFeedbackDraftMessage.toModel(producer: User.Id) = comment = this.comment, score = this.score?.let { Homework.Score(it.toShort()) }, ) - -private val Workspace.Event.kind - get() = - when (this) { - is Workspace.Comment -> WorkspaceEventKindMessage.COMMENT - is Workspace.Feedback -> WorkspaceEventKindMessage.FEEDBACK - is Workspace.Submission -> WorkspaceEventKindMessage.SUBMISSION - } - -fun Workspace.Event.toMessage(): WorkspaceEventMessage = - when (this) { - is Workspace.Comment -> { - WorkspaceCommentMessage( - kind = this.kind, - id = this.id.number, - producerId = this.producer.number, - text = this.text, - creationMoment = this.creationMoment, - ) - } - - is Workspace.Feedback -> { - WorkspaceFeedbackMessage( - kind = this.kind, - id = this.id.number, - producerId = this.producer.number, - comment = this.comment, - score = this.score?.value?.toInt(), - creationMoment = this.creationMoment, - ) - } - - is Workspace.Submission -> { - WorkspaceSubmissionMessage( - kind = this.kind, - id = this.id.number, - producerId = this.producer.number, - note = this.note, - creationMoment = this.creationMoment, - ) - } - } diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/commons/Require.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/commons/Require.kt deleted file mode 100644 index 4ae80e7..0000000 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/commons/Require.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.vityaman.lms.botalka.commons - -fun expect( - isValid: Boolean, - message: StringBuilder.() -> Unit, -): Unit = require(isValid) { buildString(message) } - -fun expectId(number: Int) { - expect(0 < number) { - append("Unique id must be a positive, got $number") - } -} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/Require.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/Require.kt new file mode 100644 index 0000000..b7480b8 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/Require.kt @@ -0,0 +1,15 @@ +package ru.vityaman.lms.botalka.domain + +import ru.vityaman.lms.botalka.domain.exception.InvalidValueException + +fun expect(isValid: Boolean, message: StringBuilder.() -> Unit) { + if (!isValid) { + throw InvalidValueException(buildString(message)) + } +} + +fun expectId(number: Int) { + expect(0 < number) { + append("Unique id must be a positive, got $number") + } +} diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/InvalidValueException.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/InvalidValueException.kt new file mode 100644 index 0000000..8d379f8 --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/InvalidValueException.kt @@ -0,0 +1,4 @@ +package ru.vityaman.lms.botalka.domain.exception + +class InvalidValueException(string: String) : + DomainException(string) diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundException.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundException.kt new file mode 100644 index 0000000..b6ce2fb --- /dev/null +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/exception/NotFoundException.kt @@ -0,0 +1,4 @@ +package ru.vityaman.lms.botalka.domain.exception + +class NotFoundException(message: String) : + DomainException(message) 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/domain/model/Homework.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Homework.kt index c02d689..493529a 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Homework.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Homework.kt @@ -1,8 +1,8 @@ package ru.vityaman.lms.botalka.domain.model import ru.vityaman.lms.botalka.commons.abbreviated -import ru.vityaman.lms.botalka.commons.expect -import ru.vityaman.lms.botalka.commons.expectId +import ru.vityaman.lms.botalka.domain.expect +import ru.vityaman.lms.botalka.domain.expectId import java.time.OffsetDateTime data class Homework( diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/PromotionRequest.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/PromotionRequest.kt index 14eab17..95ef00e 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/PromotionRequest.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/PromotionRequest.kt @@ -1,6 +1,6 @@ package ru.vityaman.lms.botalka.domain.model -import ru.vityaman.lms.botalka.commons.expectId +import ru.vityaman.lms.botalka.domain.expectId data class PromotionRequest( val id: Id, diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/User.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/User.kt index 7482450..fa05f73 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/User.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/User.kt @@ -1,8 +1,8 @@ package ru.vityaman.lms.botalka.domain.model import ru.vityaman.lms.botalka.commons.abbreviated -import ru.vityaman.lms.botalka.commons.expect -import ru.vityaman.lms.botalka.commons.expectId +import ru.vityaman.lms.botalka.domain.expect +import ru.vityaman.lms.botalka.domain.expectId data class User( val id: Id, diff --git a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Workspace.kt b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Workspace.kt index ca9fdc4..1970ace 100644 --- a/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Workspace.kt +++ b/botalka/src/main/kotlin/ru/vityaman/lms/botalka/domain/model/Workspace.kt @@ -1,6 +1,6 @@ package ru.vityaman.lms.botalka.domain.model -import ru.vityaman.lms.botalka.commons.expectId +import ru.vityaman.lms.botalka.domain.expectId import java.time.OffsetDateTime data class Workspace( 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/main/resources/static/openapi/api.yml b/botalka/src/main/resources/static/openapi/api.yml index b5ad8bd..dd8c61f 100644 --- a/botalka/src/main/resources/static/openapi/api.yml +++ b/botalka/src/main/resources/static/openapi/api.yml @@ -69,6 +69,12 @@ paths: type: array items: $ref: '#/components/schemas/WorkspaceEvent' + 404: + description: Homework or student with given id not found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' post: tags: [ Homework ] operationId: postEvent @@ -104,6 +110,18 @@ paths: application/json: schema: $ref: '#/components/schemas/WorkspaceEvent' + 400: + description: Event is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 404: + description: Homework or student with given id not found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' /rating/grades: get: tags: [ Rating ] @@ -168,7 +186,7 @@ paths: schema: $ref: '#/components/schemas/User' 400: - description: User is invalid + description: User is invalid or alias is taken content: application/json: schema: @@ -200,8 +218,14 @@ paths: application/json: schema: $ref: '#/components/schemas/PromotionRequest' + 404: + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' 409: - description: Duplicated request + description: Request is duplicated or resolved content: application/json: schema: 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)) + } + } }