Skip to content

Commit

Permalink
#63 Added deadline check before submit
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman committed Apr 20, 2024
1 parent b34f55f commit 0ee8aae
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ package ru.vityaman.lms.botalka
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import java.time.Clock

@SpringBootApplication(exclude = [JooqAutoConfiguration::class])
class BotalkaApplication
class BotalkaApplication {
@Bean
fun clock() = Clock.systemUTC()
}

fun main(args: Array<String>) {
runApplication<BotalkaApplication>(args = args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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.DeadlinePassedException
import ru.vityaman.lms.botalka.domain.exception.DomainException
import ru.vityaman.lms.botalka.domain.exception.InvalidValueException
import ru.vityaman.lms.botalka.domain.exception.NotFoundException
Expand All @@ -13,12 +14,10 @@ val DomainException.httpCode: HttpStatus
is InvalidValueException -> HttpStatus.BAD_REQUEST
is NotFoundException -> HttpStatus.NOT_FOUND
is PromotionRequestResolvedException -> HttpStatus.CONFLICT
is DeadlinePassedException -> HttpStatus.BAD_REQUEST
else -> TODO()
}

val HttpStatus.reason: String
get() = this.reasonPhrase

fun DomainException.toResponseEntity() =
ResponseEntity
.status(this.httpCode)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ru.vityaman.lms.botalka.domain.exception

class DeadlinePassedException(message: String) :
DomainException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ data class Homework(
val deadlineMoment: OffsetDateTime,
) {
init {
expect(publicationMoment.isBefore(deadlineMoment)) {
append("Homework must be published before deadline")
expect(publicationMoment.isBefore(deadlineMoment.plusMinutes(1))) {
append("Homework must be published 1 minute before deadline")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import ru.vityaman.lms.botalka.domain.model.Homework

interface HomeworkService {
suspend fun create(homework: Homework.Draft): Homework
suspend fun getById(id: Homework.Id): Homework?
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ class BasicHomeworkService(
) : HomeworkService {
override suspend fun create(homework: Homework.Draft): Homework =
storage.create(homework)

override suspend fun getById(id: Homework.Id): Homework? =
storage.getById(id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ import kotlinx.coroutines.flow.lastOrNull
import kotlinx.coroutines.flow.map
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import ru.vityaman.lms.botalka.domain.exception.DeadlinePassedException
import ru.vityaman.lms.botalka.domain.exception.orNotFound
import ru.vityaman.lms.botalka.domain.model.Homework
import ru.vityaman.lms.botalka.domain.model.Workspace
import ru.vityaman.lms.botalka.logic.HomeworkService
import ru.vityaman.lms.botalka.logic.WorkspaceService
import ru.vityaman.lms.botalka.storage.WorkspaceStorage
import java.time.Clock
import java.time.OffsetDateTime

@Service
class BasicWorkspaceService(
@Autowired private val storage: WorkspaceStorage,
@Autowired private val homeworks: HomeworkService,
@Autowired private val clock: Clock,
) : WorkspaceService {
override fun events(id: Workspace.Id): Flow<Workspace.Event> =
storage.events(id)
Expand All @@ -24,7 +31,7 @@ class BasicWorkspaceService(
): Workspace.Event =
when (event) {
is Workspace.Comment.Draft -> storage.save(id, event)
is Workspace.Submission.Draft -> storage.save(id, event)
is Workspace.Submission.Draft -> submit(id, event)
is Workspace.Feedback.Draft -> storage.save(id, event)
}

Expand All @@ -38,4 +45,19 @@ class BasicWorkspaceService(

override fun all(): Flow<Workspace.Id> =
storage.all()

private suspend fun submit(
id: Workspace.Id,
event: Workspace.Submission.Draft,
): Workspace.Submission {
val homework = homeworks.getById(id.homework)
.orNotFound("Homework with id ${id.homework.number} not found")
if (homework.deadlineMoment.isBefore(OffsetDateTime.now(clock))) {
throw DeadlinePassedException(buildString {
append("Deadline for homework with id ${homework.id.number} ")
append(" \"${homework.title}\" was passed")
})
}
return storage.save(id, event)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import ru.vityaman.lms.botalka.domain.model.Homework

interface HomeworkStorage {
suspend fun create(homework: Homework.Draft): Homework
suspend fun getById(id: Homework.Id): Homework?
}
Original file line number Diff line number Diff line change
@@ -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 ru.vityaman.lms.botalka.domain.model.Homework
Expand Down Expand Up @@ -34,4 +35,11 @@ class JooqHomeworkStorage(
.toMono()
.map { it.toModel() }
.awaitSingle()

override suspend fun getById(id: Homework.Id): Homework? =
database.withDSLMono {
selectFrom(HOMEWORK)
.where(HOMEWORK.ID.eq(id.number))
.toMono()
}.awaitSingleOrNull()?.toModel()
}
2 changes: 1 addition & 1 deletion botalka/src/main/resources/static/openapi/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ paths:
schema:
$ref: '#/components/schemas/WorkspaceEvent'
400:
description: Event is invalid
description: Event is invalid or deadline has passed
content:
application/json:
schema:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,28 @@ class HomeworkApiTest(

@Test
fun homeworkInvariants(): Unit = runBlocking {
val left = time("2024-05-04T05:05:05+03:00")
val right = time("2024-05-05T05:05:05+03:00")
left shouldBeBefore right
assertThrows<WebClientResponseException.BadRequest> {
api.homeworkPost(
sampleHomework(
publicationMoment = right,
deadlineMoment = left,
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-04T05:05:05+03:00"),
)
).awaitSingle()
}
assertThrows<WebClientResponseException.BadRequest> {
api.homeworkPost(
sampleHomework(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-05T05:05:45+03:00"),
)
).awaitSingle()
}
api.homeworkPost(
sampleHomework(
publicationMoment = time("2024-05-05T05:05:05+03:00"),
deadlineMoment = time("2024-05-05T05:06:05+03:00"),
)
).awaitSingle()
}

private fun time(format: String): OffsetDateTime =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.reactor.awaitSingle
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.reactive.function.client.WebClientResponseException
import ru.vityaman.lms.botalka.BotalkaTestSuite
import ru.vityaman.lms.botalka.api.http.client.HomeworkDraftMessage
import ru.vityaman.lms.botalka.api.http.client.PromotionRequestDraftMessage
Expand All @@ -22,12 +24,15 @@ import ru.vityaman.lms.botalka.api.http.client.WorkspaceFeedbackDraftMessage
import ru.vityaman.lms.botalka.api.http.client.WorkspaceSubmissionDraftMessage
import ru.vityaman.lms.botalka.api.http.client.apis.HomeworkApi
import ru.vityaman.lms.botalka.api.http.client.apis.UserApi
import ru.vityaman.lms.botalka.env.FakeClock
import java.time.Instant
import java.time.OffsetDateTime
import kotlin.random.Random

class WorkspaceApiTest(
@Autowired private val homeworks: HomeworkApi,
@Autowired private val users: UserApi,
@Autowired private val clock: FakeClock,
) : BotalkaTestSuite() {
private var student: Int = 0
private var teacher: Int = 0
Expand Down Expand Up @@ -85,6 +90,25 @@ class WorkspaceApiTest(
.all { (a, b) -> a <= b }
}

@Test
fun cantSubmitSolutionAfterDeadline(): Unit = runBlocking {
homework = homeworks.homeworkPost(
HomeworkDraftMessage(
title = "Expired Homework",
description = "Description",
maxScore = 200,
publicationMoment = OffsetDateTime.now(),
deadlineMoment = OffsetDateTime.now().plusMinutes(1),
),
).awaitSingle().id

clock.withFixedInstant(Instant.now().plusSeconds(60)) {
assertThrows<WebClientResponseException.BadRequest> {
submit("Oh, sorry, I am late...")
}
}
}

private suspend fun postRandomEvents(count: Int): Unit = coroutineScope {
val random = Random(1_231_313)
repeat(count) {
Expand Down
28 changes: 28 additions & 0 deletions botalka/src/test/kotlin/ru/vityaman/lms/botalka/env/FakeClock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ru.vityaman.lms.botalka.env

import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Component
import java.time.Clock
import java.time.Instant
import java.time.ZoneId

@Primary
@Component
class FakeClock : Clock() {
private val origin = Clock.systemUTC()
private var instant: Instant? = null

override fun instant(): Instant {
return instant ?: origin.instant()
}

override fun withZone(p0: ZoneId?): Clock = origin.withZone(p0)

override fun getZone(): ZoneId = origin.zone

suspend fun withFixedInstant(instant: Instant, action: suspend () -> Unit) {
this.instant = instant
action()
this.instant = null
}
}

0 comments on commit 0ee8aae

Please sign in to comment.