diff --git a/src/docs/asciidoc/judgment.adoc b/src/docs/asciidoc/judgment.adoc index 48524e602..0fb62d906 100644 --- a/src/docs/asciidoc/judgment.adoc +++ b/src/docs/asciidoc/judgment.adoc @@ -4,6 +4,10 @@ operation::judgment-judge-example-post[snippets='http-request,http-response'] +== 예제 테스트 결과 조회 + +operation::judgment-judge-example-get[snippets='http-request,http-response'] + == 자동 채점 성공 operation::judgment-success-result-post[snippets='http-request,http-response'] diff --git a/src/main/kotlin/apply/application/AssignmentDtos.kt b/src/main/kotlin/apply/application/AssignmentDtos.kt index 94263ef95..3c948951d 100644 --- a/src/main/kotlin/apply/application/AssignmentDtos.kt +++ b/src/main/kotlin/apply/application/AssignmentDtos.kt @@ -15,7 +15,7 @@ data class AssignmentRequest( @field:Pattern( regexp = "https://github\\.com(/[\\w\\-]+){2}/pull/[1-9]\\d*", - message = "올바른 형식의 URL이어야 합니다" + message = "올바른 형식의 Pull Request URL이어야 합니다" ) val pullRequestUrl: String, diff --git a/src/main/kotlin/apply/application/EvaluationTargetService.kt b/src/main/kotlin/apply/application/EvaluationTargetService.kt index 3cafb1a12..b6d149e97 100644 --- a/src/main/kotlin/apply/application/EvaluationTargetService.kt +++ b/src/main/kotlin/apply/application/EvaluationTargetService.kt @@ -149,7 +149,6 @@ class EvaluationTargetService( fun grade(evaluationTargetId: Long, request: EvaluationTargetData) { val evaluationTarget = evaluationTargetRepository.getById(evaluationTargetId) - val evaluationAnswers = request.evaluationItemScores .map { EvaluationAnswer(it.score, it.id) } .toMutableList() diff --git a/src/main/kotlin/apply/application/GradingService.kt b/src/main/kotlin/apply/application/GradingService.kt new file mode 100644 index 000000000..fc66980a0 --- /dev/null +++ b/src/main/kotlin/apply/application/GradingService.kt @@ -0,0 +1,52 @@ +package apply.application + +import apply.domain.assignment.AssignmentRepository +import apply.domain.assignment.getById +import apply.domain.evaluationtarget.EvaluationTargetRepository +import apply.domain.evaluationtarget.getByEvaluationIdAndUserId +import apply.domain.judgment.JudgmentStartedEvent +import apply.domain.judgment.JudgmentSucceededEvent +import apply.domain.judgment.JudgmentTouchedEvent +import apply.domain.judgmentitem.JudgmentItemRepository +import apply.domain.judgmentitem.getByMissionId +import apply.domain.mission.MissionRepository +import apply.domain.mission.getById +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.event.TransactionalEventListener + +@Transactional +@Service +class GradingService( + private val evaluationTargetRepository: EvaluationTargetRepository, + private val missionRepository: MissionRepository, + private val judgmentItemRepository: JudgmentItemRepository, + private val assignmentRepository: AssignmentRepository +) { + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(condition = "#event.type.evaluable") + fun grade(event: JudgmentStartedEvent) { + grade(event.assignmentId, 0) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(condition = "#event.type.evaluable") + fun grade(event: JudgmentTouchedEvent) { + grade(event.assignmentId, event.passCount) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(condition = "#event.type.evaluable") + fun grade(event: JudgmentSucceededEvent) { + grade(event.assignmentId, event.passCount) + } + + private fun grade(assignmentId: Long, score: Int) { + val assignment = assignmentRepository.getById(assignmentId) + val mission = missionRepository.getById(assignment.missionId) + val judgmentItem = judgmentItemRepository.getByMissionId(mission.id) + val target = evaluationTargetRepository.getByEvaluationIdAndUserId(mission.evaluationId, assignment.userId) + target.updateScore(judgmentItem.evaluationItemId, score) + } +} diff --git a/src/main/kotlin/apply/application/JudgmentAllService.kt b/src/main/kotlin/apply/application/JudgmentAllService.kt new file mode 100644 index 000000000..706a6466b --- /dev/null +++ b/src/main/kotlin/apply/application/JudgmentAllService.kt @@ -0,0 +1,27 @@ +package apply.application + +import apply.domain.assignment.AssignmentRepository +import apply.domain.judgment.JudgmentType +import apply.domain.judgmentitem.JudgmentItemRepository +import apply.domain.mission.MissionRepository +import apply.domain.mission.getByEvaluationId +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service + +@Service +class JudgmentAllService( + private val judgmentService: JudgmentService, + private val missionRepository: MissionRepository, + private val judgmentItemRepository: JudgmentItemRepository, + private val assignmentRepository: AssignmentRepository +) { + @Async + fun judgeAll(evaluationId: Long) { + val mission = missionRepository.getByEvaluationId(evaluationId) + check(judgmentItemRepository.existsByMissionId(mission.id)) { "자동 채점을 실행할 수 없습니다." } + val assignments = assignmentRepository.findAllByMissionId(mission.id) + assignments.forEach { + runCatching { judgmentService.judge(mission, it, JudgmentType.REAL) } + } + } +} diff --git a/src/main/kotlin/apply/application/JudgmentDtos.kt b/src/main/kotlin/apply/application/JudgmentDtos.kt index b2b9f46c7..c541ebf55 100644 --- a/src/main/kotlin/apply/application/JudgmentDtos.kt +++ b/src/main/kotlin/apply/application/JudgmentDtos.kt @@ -66,8 +66,13 @@ data class JudgmentData( val startedDateTime: LocalDateTime?, val id: Long ) { - constructor(id: Long?, evaluationItemId: Long?, assignmentId: Long?, judgmentRecord: JudgmentRecord?) : this( - evaluationItemId ?: 0L, + constructor( + id: Long? = null, + evaluationItemId: Long, + assignmentId: Long? = null, + judgmentRecord: JudgmentRecord? = null + ) : this( + evaluationItemId, assignmentId ?: 0L, judgmentRecord?.commit?.hash, judgmentRecord?.status, diff --git a/src/main/kotlin/apply/application/JudgmentService.kt b/src/main/kotlin/apply/application/JudgmentService.kt index ebbfd78b9..bcda199e8 100644 --- a/src/main/kotlin/apply/application/JudgmentService.kt +++ b/src/main/kotlin/apply/application/JudgmentService.kt @@ -4,8 +4,6 @@ import apply.domain.assignment.Assignment import apply.domain.assignment.AssignmentRepository import apply.domain.assignment.getById import apply.domain.assignment.getByUserIdAndMissionId -import apply.domain.evaluationtarget.EvaluationTargetRepository -import apply.domain.evaluationtarget.getById import apply.domain.judgment.AssignmentArchive import apply.domain.judgment.Commit import apply.domain.judgment.Judgment @@ -26,69 +24,54 @@ class JudgmentService( private val assignmentRepository: AssignmentRepository, private val missionRepository: MissionRepository, private val judgmentItemRepository: JudgmentItemRepository, - private val evaluationTargetRepository: EvaluationTargetRepository, private val assignmentArchive: AssignmentArchive ) { fun judgeExample(userId: Long, missionId: Long): LastJudgmentResponse { val mission = missionRepository.getById(missionId) - check(mission.isSubmitting) { "예제 테스트를 실행할 수 없습니다." } + check(mission.isSubmitting && judgmentItemRepository.existsByMissionId(mission.id)) { + "예제 테스트를 실행할 수 없습니다." + } val assignment = assignmentRepository.getByUserIdAndMissionId(userId, missionId) return judge(mission, assignment, JudgmentType.EXAMPLE) } - fun judgeReal(userId: Long, missionId: Long): LastJudgmentResponse { - return judgeReal(assignmentRepository.getByUserIdAndMissionId(userId, missionId)) - } - - fun judgeRealByAssignmentId(assignmentId: Long): LastJudgmentResponse { - return judgeReal(assignmentRepository.getById(assignmentId)) - } - - private fun judgeReal(assignment: Assignment): LastJudgmentResponse { - val mission = missionRepository.getById(assignment.missionId) - return judge(mission, assignment, JudgmentType.REAL) - } - - private fun judge(mission: Mission, assignment: Assignment, judgmentType: JudgmentType): LastJudgmentResponse { - check(judgmentItemRepository.existsByMissionId(mission.id)) { "예제 테스트를 실행할 수 없습니다." } - var judgment = judgmentRepository.findByAssignmentIdAndType(assignment.id, judgmentType) - ?: judgmentRepository.save(Judgment(assignment.id, judgmentType)) - val commit = assignmentArchive.getLastCommit(assignment.pullRequestUrl, mission.period.endDateTime) - judgment.start(commit) - judgment = judgmentRepository.save(judgment) - return LastJudgmentResponse(assignment.pullRequestUrl, judgment.lastRecord) + fun findLastExampleJudgment(userId: Long, missionId: Long): LastJudgmentResponse? { + val assignment = assignmentRepository.findByUserIdAndMissionId(userId, missionId) ?: return null + val judgment = judgmentRepository.findByAssignmentIdAndType(assignment.id, JudgmentType.EXAMPLE) + return judgment?.let { LastJudgmentResponse(assignment.pullRequestUrl, it.lastRecord) } } fun success(judgmentId: Long, request: SuccessJudgmentRequest) { val judgment = judgmentRepository.getById(judgmentId) judgment.success(Commit(request.commit), request.passCount, request.totalCount) - // TODO: reflect result to evaluation answer + judgmentRepository.save(judgment) } fun fail(judgmentId: Long, request: FailJudgmentRequest) { val judgment = judgmentRepository.getById(judgmentId) judgment.fail(Commit(request.commit), request.message) + judgmentRepository.save(judgment) } fun cancel(judgmentId: Long, request: CancelJudgmentRequest) { val judgment = judgmentRepository.getById(judgmentId) judgment.cancel(Commit(request.commit), request.message) + judgmentRepository.save(judgment) + } + + fun judgeReal(assignmentId: Long): LastJudgmentResponse { + val assignment = assignmentRepository.getById(assignmentId) + val mission = missionRepository.getById(assignment.missionId) + check(judgmentItemRepository.existsByMissionId(mission.id)) { "자동 채점을 실행할 수 없습니다." } + return judge(mission, assignment, JudgmentType.REAL) } - fun findByEvaluationTargetId(evaluationTargetId: Long, type: JudgmentType): JudgmentData? { - val evaluationTarget = evaluationTargetRepository.getById(evaluationTargetId) - val mission = missionRepository.findByEvaluationId(evaluationTarget.evaluationId) ?: return null - val judgmentItem = judgmentItemRepository.findByMissionId(mission.id) ?: return null - val assignment = assignmentRepository.findByUserIdAndMissionId(evaluationTarget.userId, mission.id) - return assignment - ?.let { judgmentRepository.findByAssignmentIdAndType(it.id, type) } - .let { - JudgmentData( - id = it?.id, - evaluationItemId = judgmentItem.evaluationItemId, - assignmentId = assignment?.id, - judgmentRecord = it?.lastRecord - ) - } + fun judge(mission: Mission, assignment: Assignment, judgmentType: JudgmentType): LastJudgmentResponse { + val commit = assignmentArchive.getLastCommit(assignment.pullRequestUrl, mission.period.endDateTime) + var judgment = judgmentRepository.findByAssignmentIdAndType(assignment.id, judgmentType) + ?: judgmentRepository.save(Judgment(assignment.id, judgmentType)) + judgment.start(commit) + judgment = judgmentRepository.save(judgment) + return LastJudgmentResponse(assignment.pullRequestUrl, judgment.lastRecord) } } diff --git a/src/main/kotlin/apply/application/MissionDtos.kt b/src/main/kotlin/apply/application/MissionDtos.kt index aac5f0711..e815b4833 100644 --- a/src/main/kotlin/apply/application/MissionDtos.kt +++ b/src/main/kotlin/apply/application/MissionDtos.kt @@ -103,9 +103,16 @@ data class MyMissionResponse( val submitted: Boolean, val startDateTime: LocalDateTime, val endDateTime: LocalDateTime, - val status: MissionStatus + val status: MissionStatus, + val runnable: Boolean, + val judgment: LastJudgmentResponse? ) { - constructor(mission: Mission, submitted: Boolean) : this( + constructor( + mission: Mission, + submitted: Boolean = false, + runnable: Boolean = false, + judgment: LastJudgmentResponse? = null + ) : this( mission.id, mission.title, mission.description, @@ -113,7 +120,9 @@ data class MyMissionResponse( submitted, mission.period.startDateTime, mission.period.endDateTime, - mission.status + mission.status, + runnable, + judgment ) } diff --git a/src/main/kotlin/apply/application/MissionService.kt b/src/main/kotlin/apply/application/MissionService.kt index 26687d9e0..9203420e1 100644 --- a/src/main/kotlin/apply/application/MissionService.kt +++ b/src/main/kotlin/apply/application/MissionService.kt @@ -1,10 +1,8 @@ package apply.application -import apply.domain.assignment.AssignmentRepository import apply.domain.evaluation.EvaluationRepository import apply.domain.evaluation.getById import apply.domain.evaluationitem.EvaluationItemRepository -import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.judgmentitem.JudgmentItem import apply.domain.judgmentitem.JudgmentItemRepository import apply.domain.mission.Mission @@ -19,9 +17,7 @@ import org.springframework.transaction.annotation.Transactional class MissionService( private val missionRepository: MissionRepository, private val evaluationRepository: EvaluationRepository, - private val evaluationTargetRepository: EvaluationTargetRepository, private val evaluationItemRepository: EvaluationItemRepository, - private val assignmentRepository: AssignmentRepository, private val judgmentItemRepository: JudgmentItemRepository ) { fun save(request: MissionData): MissionResponse { @@ -98,16 +94,6 @@ class MissionService( return missions.map { MissionAndEvaluationResponse(it, evaluationsById.getValue(it.evaluationId)) } } - fun findAllByUserIdAndRecruitmentId(userId: Long, recruitmentId: Long): List { - val evaluationIds = evaluationRepository.findAllByRecruitmentId(recruitmentId).map { it.id } - val includedEvaluationIds = evaluationIds - .filter { evaluationTargetRepository.existsByUserIdAndEvaluationId(userId, it) } - val assignments = assignmentRepository.findAllByUserId(userId) - return missionRepository.findAllByEvaluationIdIn(includedEvaluationIds) - .filterNot { it.hidden } - .map { mission -> MyMissionResponse(mission, assignments.any { it.missionId == mission.id }) } - } - fun deleteById(id: Long) { val mission = missionRepository.getById(id) check(!mission.submittable) { "제출 가능한 과제는 삭제할 수 없습니다." } diff --git a/src/main/kotlin/apply/application/MyMissionService.kt b/src/main/kotlin/apply/application/MyMissionService.kt new file mode 100644 index 000000000..14b12c702 --- /dev/null +++ b/src/main/kotlin/apply/application/MyMissionService.kt @@ -0,0 +1,97 @@ +package apply.application + +import apply.domain.assignment.Assignment +import apply.domain.assignment.AssignmentRepository +import apply.domain.evaluation.EvaluationRepository +import apply.domain.evaluationtarget.EvaluationTargetRepository +import apply.domain.evaluationtarget.getById +import apply.domain.judgment.Judgment +import apply.domain.judgment.JudgmentRepository +import apply.domain.judgment.JudgmentType +import apply.domain.judgmentitem.JudgmentItem +import apply.domain.judgmentitem.JudgmentItemRepository +import apply.domain.mission.Mission +import apply.domain.mission.MissionRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional(readOnly = true) +@Service +class MyMissionService( + private val evaluationRepository: EvaluationRepository, + private val evaluationTargetRepository: EvaluationTargetRepository, + private val missionRepository: MissionRepository, + private val judgmentItemRepository: JudgmentItemRepository, + private val assignmentRepository: AssignmentRepository, + private val judgmentRepository: JudgmentRepository +) { + fun findAllByUserIdAndRecruitmentId(userId: Long, recruitmentId: Long): List { + val missions = findMissions(userId, recruitmentId) + if (missions.isEmpty()) return emptyList() + + val assignments = assignmentRepository.findAllByUserId(userId) + if (assignments.isEmpty()) return missions.map(::MyMissionResponse) + + val judgmentItems = judgmentItemRepository.findAllByMissionIdIn(missions.map { it.id }) + if (judgmentItems.isEmpty()) return missions.mapBy(assignments) + + val judgments = judgmentRepository + .findAllByAssignmentIdInAndType(assignments.map { it.id }, JudgmentType.EXAMPLE) + return missions.mapBy(assignments, judgmentItems, judgments) + } + + private fun findMissions(userId: Long, recruitmentId: Long): List { + val evaluationIds = evaluationRepository.findAllByRecruitmentId(recruitmentId).map { it.id } + val targets = evaluationTargetRepository.findAllByUserIdAndEvaluationIdIn(userId, evaluationIds) + return missionRepository.findAllByEvaluationIdIn(targets.map { it.id }).filterNot { it.hidden } + } + + private fun List.mapBy(assignments: List): List { + return map { mission -> + val assignment = assignments.find { it.missionId == mission.id } + MyMissionResponse(mission, assignment != null) + } + } + + private fun List.mapBy( + assignments: List, + judgmentItems: List, + judgments: List + ): List { + return map { mission -> + val assignment = assignments.find { it.missionId == mission.id } + val judgmentItem = judgmentItems.find { it.missionId == mission.id } + val judgment = judgments.findLastJudgment(assignment, judgmentItem) + MyMissionResponse( + mission = mission, + submitted = assignment != null, + runnable = assignment != null && judgmentItem != null, + judgment = judgment + ) + } + } + + private fun List.findLastJudgment( + assignment: Assignment?, + judgmentItem: JudgmentItem? + ): LastJudgmentResponse? { + if (assignment == null || judgmentItem == null) return null + val judgment = find { it.assignmentId == assignment.id } ?: return null + return LastJudgmentResponse(assignment.pullRequestUrl, judgment.lastRecord) + } + + fun findLastRealJudgmentByEvaluationTargetId(evaluationTargetId: Long): JudgmentData? { + val evaluationTarget = evaluationTargetRepository.getById(evaluationTargetId) + val mission = missionRepository.findByEvaluationId(evaluationTarget.evaluationId) ?: return null + val judgmentItem = judgmentItemRepository.findByMissionId(mission.id) ?: return null + val assignment = assignmentRepository.findByUserIdAndMissionId(evaluationTarget.userId, mission.id) + ?: return JudgmentData(evaluationItemId = judgmentItem.evaluationItemId) + val judgment = judgmentRepository.findByAssignmentIdAndType(assignment.id, JudgmentType.REAL) + return JudgmentData( + id = judgment?.id, + evaluationItemId = judgmentItem.evaluationItemId, + assignmentId = assignment.id, + judgmentRecord = judgment?.lastRecord + ) + } +} diff --git a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationAnswers.kt b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationAnswers.kt index c983de553..37ecb8c61 100644 --- a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationAnswers.kt +++ b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationAnswers.kt @@ -17,8 +17,9 @@ class EvaluationAnswers( val answers: List get() = _answers - fun add(evaluationAnswer: EvaluationAnswer) { - _answers.add(evaluationAnswer) + fun add(evaluationItemId: Long, score: Int) { + _answers.removeIf { it.evaluationItemId == evaluationItemId } + _answers.add(EvaluationAnswer(score, evaluationItemId)) } fun allZero(): Boolean = answers.all { it.score == 0 } diff --git a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTarget.kt b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTarget.kt index e8087f39c..230271280 100644 --- a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTarget.kt +++ b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTarget.kt @@ -51,4 +51,8 @@ class EvaluationTarget( this.evaluationAnswers = evaluationAnswers this.note = note } + + fun updateScore(evaluationItemId: Long, score: Int) { + evaluationAnswers.add(evaluationItemId, score) + } } diff --git a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepository.kt b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepository.kt index 53f1e8367..47263ce5c 100644 --- a/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepository.kt +++ b/src/main/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepository.kt @@ -3,6 +3,10 @@ package apply.domain.evaluationtarget import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.findByIdOrNull +fun EvaluationTargetRepository.getByEvaluationIdAndUserId(evaluationId: Long, userId: Long) = + findByEvaluationIdAndUserId(evaluationId, userId) + ?: throw NoSuchElementException("평가 대상자가 존재하지 않습니다. evaluationId: $evaluationId, userId: $userId") + fun EvaluationTargetRepository.getById(id: Long): EvaluationTarget = findByIdOrNull(id) ?: throw NoSuchElementException("평가 대상자가 존재하지 않습니다. id: $id") @@ -11,11 +15,11 @@ interface EvaluationTargetRepository : JpaRepository { fun findAllByEvaluationId(evaluationId: Long): List fun deleteByUserIdIn(userIds: Collection) fun deleteByEvaluationIdAndUserIdIn(evaluationId: Long, userIds: Collection) - fun findAllByEvaluationIdAndUserIdIn(evaluationId: Long, userIds: Set): List + fun findAllByEvaluationIdAndUserIdIn(evaluationId: Long, userIds: Collection): List fun findAllByEvaluationIdAndEvaluationStatus( evaluationId: Long, evaluationStatus: EvaluationStatus ): List - fun existsByUserIdAndEvaluationId(userId: Long, evaluationId: Long): Boolean + fun findAllByUserIdAndEvaluationIdIn(userId: Long, evaluationIds: Collection): List } diff --git a/src/main/kotlin/apply/domain/judgment/Judgment.kt b/src/main/kotlin/apply/domain/judgment/Judgment.kt index a02de0352..5362ffe1d 100644 --- a/src/main/kotlin/apply/domain/judgment/Judgment.kt +++ b/src/main/kotlin/apply/domain/judgment/Judgment.kt @@ -7,6 +7,7 @@ import javax.persistence.Column import javax.persistence.Entity import javax.persistence.EnumType import javax.persistence.Enumerated +import javax.persistence.FetchType import javax.persistence.ForeignKey import javax.persistence.JoinColumn import javax.persistence.OneToMany @@ -22,7 +23,7 @@ class Judgment( records: List = emptyList(), id: Long = 0L ) : BaseRootEntity(id) { - @OneToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE]) + @OneToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE], fetch = FetchType.EAGER) @JoinColumn( name = "judgment_id", nullable = false, updatable = false, foreignKey = ForeignKey(name = "fk_judgment_record_judgment_id_ref_judgment_id") @@ -43,6 +44,8 @@ class Judgment( val record = findRecord(commit) ?: createRecord(commit) if (record.touchable) { record.touch() + val event = JudgmentTouchedEvent(id, assignmentId, type, record.result.passCount, record.result.totalCount) + registerEvent(event) } else { record.start() registerEvent(JudgmentStartedEvent(id, assignmentId, type, commit)) @@ -55,23 +58,25 @@ class Judgment( } private fun createRecord(commit: Commit): JudgmentRecord { - return JudgmentRecord(commit) - .also { records.add(it) } + return JudgmentRecord(commit).also { records.add(it) } } fun success(commit: Commit, passCount: Int, totalCount: Int) { val record = getRecord(commit) record.applyResult(JudgmentResult(passCount, totalCount, status = JudgmentStatus.SUCCEEDED)) + registerEvent(JudgmentSucceededEvent(id, assignmentId, type, passCount, totalCount)) } fun fail(commit: Commit, message: String) { val record = getRecord(commit) record.applyResult(JudgmentResult(message = message, status = JudgmentStatus.FAILED)) + registerEvent(JudgmentFailedEvent(id, assignmentId, type)) } fun cancel(commit: Commit, message: String) { val record = getRecord(commit) record.applyResult(JudgmentResult(message = message, status = JudgmentStatus.CANCELLED)) + registerEvent(JudgmentCancelledEvent(id, assignmentId, type)) } private fun getRecord(commit: Commit): JudgmentRecord = findRecord(commit) diff --git a/src/main/kotlin/apply/domain/judgment/JudgmentEvents.kt b/src/main/kotlin/apply/domain/judgment/JudgmentEvents.kt new file mode 100644 index 000000000..64d615e14 --- /dev/null +++ b/src/main/kotlin/apply/domain/judgment/JudgmentEvents.kt @@ -0,0 +1,36 @@ +package apply.domain.judgment + +data class JudgmentStartedEvent( + val judgmentId: Long, + val assignmentId: Long, + val type: JudgmentType, + val commit: Commit +) + +data class JudgmentTouchedEvent( + val judgmentId: Long, + val assignmentId: Long, + val type: JudgmentType, + val passCount: Int, + val totalCount: Int +) + +data class JudgmentSucceededEvent( + val judgmentId: Long, + val assignmentId: Long, + val type: JudgmentType, + val passCount: Int, + val totalCount: Int +) + +data class JudgmentFailedEvent( + val judgmentId: Long, + val assignmentId: Long, + val type: JudgmentType +) + +data class JudgmentCancelledEvent( + val judgmentId: Long, + val assignmentId: Long, + val type: JudgmentType +) diff --git a/src/main/kotlin/apply/domain/judgment/JudgmentRepository.kt b/src/main/kotlin/apply/domain/judgment/JudgmentRepository.kt index eb5ef802f..3f0e51032 100644 --- a/src/main/kotlin/apply/domain/judgment/JudgmentRepository.kt +++ b/src/main/kotlin/apply/domain/judgment/JudgmentRepository.kt @@ -8,4 +8,5 @@ fun JudgmentRepository.getById(id: Long): Judgment = findByIdOrNull(id) interface JudgmentRepository : JpaRepository { fun findByAssignmentIdAndType(assignmentId: Long, type: JudgmentType): Judgment? + fun findAllByAssignmentIdInAndType(assignmentIds: Collection, type: JudgmentType): List } diff --git a/src/main/kotlin/apply/domain/judgment/JudgmentStartedEvent.kt b/src/main/kotlin/apply/domain/judgment/JudgmentStartedEvent.kt deleted file mode 100644 index 55eee5b5d..000000000 --- a/src/main/kotlin/apply/domain/judgment/JudgmentStartedEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package apply.domain.judgment - -data class JudgmentStartedEvent( - val judgmentId: Long, - val assignmentId: Long, - val type: JudgmentType, - val commit: Commit -) diff --git a/src/main/kotlin/apply/domain/judgment/JudgmentType.kt b/src/main/kotlin/apply/domain/judgment/JudgmentType.kt index 7716676ba..8b9499cdd 100644 --- a/src/main/kotlin/apply/domain/judgment/JudgmentType.kt +++ b/src/main/kotlin/apply/domain/judgment/JudgmentType.kt @@ -1,5 +1,5 @@ package apply.domain.judgment -enum class JudgmentType { - EXAMPLE, REAL +enum class JudgmentType(val evaluable: Boolean) { + EXAMPLE(false), REAL(true) } diff --git a/src/main/kotlin/apply/domain/judgmentitem/JudgmentItemRepository.kt b/src/main/kotlin/apply/domain/judgmentitem/JudgmentItemRepository.kt index 4a445f570..6f10aa273 100644 --- a/src/main/kotlin/apply/domain/judgmentitem/JudgmentItemRepository.kt +++ b/src/main/kotlin/apply/domain/judgmentitem/JudgmentItemRepository.kt @@ -7,6 +7,7 @@ fun JudgmentItemRepository.getByMissionId(missionId: Long): JudgmentItem = findB interface JudgmentItemRepository : JpaRepository { fun findByMissionId(missionId: Long): JudgmentItem? + fun findAllByMissionIdIn(missionIds: Collection): List fun deleteByMissionId(missionId: Long) fun existsByMissionId(missionId: Long): Boolean } diff --git a/src/main/kotlin/apply/domain/mission/MissionRepository.kt b/src/main/kotlin/apply/domain/mission/MissionRepository.kt index 3651d3df0..4d34335da 100644 --- a/src/main/kotlin/apply/domain/mission/MissionRepository.kt +++ b/src/main/kotlin/apply/domain/mission/MissionRepository.kt @@ -3,6 +3,9 @@ package apply.domain.mission import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.findByIdOrNull +fun MissionRepository.getByEvaluationId(evaluationId: Long): Mission = findByEvaluationId(evaluationId) + ?: throw NoSuchElementException("과제가 존재하지 않습니다. evaluationId: $evaluationId") + fun MissionRepository.getById(id: Long): Mission = findByIdOrNull(id) ?: throw NoSuchElementException("과제가 존재하지 않습니다. id: $id") diff --git a/src/main/kotlin/apply/infra/github/GitHubClient.kt b/src/main/kotlin/apply/infra/github/GitHubClient.kt index bcea3ede8..748437342 100644 --- a/src/main/kotlin/apply/infra/github/GitHubClient.kt +++ b/src/main/kotlin/apply/infra/github/GitHubClient.kt @@ -6,7 +6,11 @@ import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.http.RequestEntity +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component +import org.springframework.web.client.HttpClientErrorException.Forbidden +import org.springframework.web.client.HttpClientErrorException.NotFound +import org.springframework.web.client.HttpClientErrorException.Unauthorized import org.springframework.web.client.RestTemplate import org.springframework.web.client.exchange import support.toUri @@ -30,22 +34,35 @@ class GitHubClient( .accept(MediaType.APPLICATION_JSON) .header(HttpHeaders.AUTHORIZATION, bearerToken(gitHubProperties.accessKey)) .build() - val zonedDateTime = endDateTime.atZone(ZoneId.systemDefault()) - return restTemplate.exchange>(requestEntity).body - ?.filter { it.date <= zonedDateTime } - ?.maxByOrNull { it.date } - ?.let { Commit(it.hash) } - ?: throw IllegalArgumentException("해당 커밋이 존재하지 않습니다. endDateTime: $endDateTime") + return runCatching { restTemplate.exchange>(requestEntity) } + .onFailure { + when (it) { + is Unauthorized -> throw RuntimeException("유효한 토큰이 아닙니다.") + is Forbidden -> throw RuntimeException("요청 한도에 도달했습니다.") + is NotFound -> throw IllegalArgumentException("PR이 존재하지 않습니다. pullRequestUrl: $pullRequestUrl") + else -> throw RuntimeException("예기치 않은 예외가 발생했습니다.", it) + } + } + .map { it.last(endDateTime) }.getOrThrow() } private fun extract(pullRequestUrl: String): List { val result = PULL_REQUEST_URL_PATTERN.find(pullRequestUrl) - ?: throw IllegalArgumentException("올바른 형식의 URL이어야 합니다") + ?: throw IllegalArgumentException("올바른 형식의 Pull Request URL이어야 합니다.") return result.destructured.toList() } private fun bearerToken(token: String): String = if (token.isEmpty()) "" else "Bearer $token" + private fun ResponseEntity>.last(endDateTime: LocalDateTime): Commit { + val zonedDateTime = endDateTime.atZone(ZoneId.systemDefault()) + return body + ?.filter { it.date <= zonedDateTime } + ?.maxByOrNull { it.date } + ?.let { Commit(it.hash) } + ?: throw IllegalArgumentException("해당 커밋이 존재하지 않습니다. endDateTime: $endDateTime") + } + companion object { private const val PAGE_SIZE: Int = 100 private val PULL_REQUEST_URL_PATTERN: Regex = diff --git a/src/main/kotlin/apply/security/AccessorResolver.kt b/src/main/kotlin/apply/security/AccessorResolver.kt index 8f7875c1a..d0876bec3 100644 --- a/src/main/kotlin/apply/security/AccessorResolver.kt +++ b/src/main/kotlin/apply/security/AccessorResolver.kt @@ -11,7 +11,7 @@ import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.util.Base64 -private const val BASIC = "Basic" +private const val BASIC: String = "Basic" @Component class AccessorResolver( @@ -27,14 +27,14 @@ class AccessorResolver( webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory? ) { - val token = extractBearerToken(webRequest) + val token = extractBasicToken(webRequest) val decoded = token.decode(StandardCharsets.UTF_8) if (!isAuthenticated(decoded, parameter)) { throw LoginFailedException() } } - private fun extractBearerToken(request: NativeWebRequest): String { + private fun extractBasicToken(request: NativeWebRequest): String { val authorization = request.getHeader(AUTHORIZATION) ?: throw LoginFailedException() val (tokenType, token) = authorization.split(" ") if (tokenType != BASIC) { diff --git a/src/main/kotlin/apply/ui/admin/selections/EvaluationTargetFormDialog.kt b/src/main/kotlin/apply/ui/admin/selections/EvaluationTargetFormDialog.kt index 33603f07d..e84a4a212 100644 --- a/src/main/kotlin/apply/ui/admin/selections/EvaluationTargetFormDialog.kt +++ b/src/main/kotlin/apply/ui/admin/selections/EvaluationTargetFormDialog.kt @@ -6,7 +6,7 @@ import apply.application.EvaluationTargetData import apply.application.EvaluationTargetService import apply.application.JudgmentData import apply.application.JudgmentService -import apply.domain.judgment.JudgmentType +import apply.application.MyMissionService import com.vaadin.flow.component.Component import com.vaadin.flow.component.button.Button import com.vaadin.flow.component.dialog.Dialog @@ -23,6 +23,7 @@ import support.views.createPrimaryButton class EvaluationTargetFormDialog( private val evaluationTargetService: EvaluationTargetService, assignmentService: AssignmentService, + myMissionService: MyMissionService, private val judgmentService: JudgmentService, private val evaluationTargetId: Long, reloadComponents: () -> Unit @@ -34,8 +35,7 @@ class EvaluationTargetFormDialog( } private val evaluationTargetForm: BindingFormLayout private val assignment: AssignmentData? = assignmentService.findByEvaluationTargetId(evaluationTargetId) - private val judgment: JudgmentData? = - judgmentService.findByEvaluationTargetId(evaluationTargetId, JudgmentType.REAL) + private val judgment: JudgmentData? = myMissionService.findLastRealJudgmentByEvaluationTargetId(evaluationTargetId) init { val response = evaluationTargetService.getGradeEvaluation(evaluationTargetId) diff --git a/src/main/kotlin/apply/ui/admin/selections/JudgmentForm.kt b/src/main/kotlin/apply/ui/admin/selections/JudgmentForm.kt index b46838561..18aa0d2a0 100644 --- a/src/main/kotlin/apply/ui/admin/selections/JudgmentForm.kt +++ b/src/main/kotlin/apply/ui/admin/selections/JudgmentForm.kt @@ -38,7 +38,7 @@ class JudgmentForm( private fun createJudgmentRequestButton(): Button { return createContrastButton("실행") { try { - judgmentService.judgeRealByAssignmentId(judgmentData.assignmentId) + judgmentService.judgeReal(judgmentData.assignmentId) createNotification("자동 채점이 실행되었습니다.") } catch (e: Exception) { createNotification(e.localizedMessage) diff --git a/src/main/kotlin/apply/ui/admin/selections/SelectionView.kt b/src/main/kotlin/apply/ui/admin/selections/SelectionView.kt index 3002dfdc9..0a7f36e40 100644 --- a/src/main/kotlin/apply/ui/admin/selections/SelectionView.kt +++ b/src/main/kotlin/apply/ui/admin/selections/SelectionView.kt @@ -9,8 +9,9 @@ import apply.application.EvaluationTargetCsvService import apply.application.EvaluationTargetResponse import apply.application.EvaluationTargetService import apply.application.ExcelService +import apply.application.JudgmentAllService import apply.application.JudgmentService -import apply.application.MissionService +import apply.application.MyMissionService import apply.application.RecruitmentItemService import apply.application.RecruitmentService import apply.domain.applicationform.ApplicationForm @@ -52,16 +53,17 @@ import support.views.downloadFile @Route(value = "admin/selections", layout = BaseLayout::class) class SelectionView( - private val applicantService: ApplicantService, private val recruitmentService: RecruitmentService, private val recruitmentItemService: RecruitmentItemService, + private val applicantService: ApplicantService, private val evaluationService: EvaluationService, private val evaluationTargetService: EvaluationTargetService, private val assignmentService: AssignmentService, private val judgmentService: JudgmentService, + private val judgmentAllService: JudgmentAllService, + private val myMissionService: MyMissionService, private val excelService: ExcelService, - private val evaluationTargetCsvService: EvaluationTargetCsvService, - private val missionService: MissionService, + private val evaluationTargetCsvService: EvaluationTargetCsvService ) : VerticalLayout(), HasUrlParameter { private var recruitmentId: Long = 0L private var evaluations: List = @@ -98,7 +100,7 @@ class SelectionView( HorizontalLayout( createLoadButton(tabs), createResultDownloadButton(), - createJudgeAllButton() + createJudgeAllButton(tabs) ) ).apply { setWidthFull() @@ -174,7 +176,13 @@ class SelectionView( private fun createEvaluationButtonRenderer(): Renderer { return ComponentRenderer { response -> createPrimarySmallButton("평가하기") { - EvaluationTargetFormDialog(evaluationTargetService, assignmentService, judgmentService, response.id) { + EvaluationTargetFormDialog( + evaluationTargetService, + assignmentService, + myMissionService, + judgmentService, + response.id + ) { selectedTabIndex = tabs.selectedIndex removeAll() add(createTitle(), createContent()) @@ -205,7 +213,7 @@ class SelectionView( private fun createLoadButton(tabs: Tabs): Button { return createPrimaryButton("평가 대상자 불러오기") { - val evaluation = evaluations.first { it.title == tabs.selectedTab.label } + val evaluation = tabs.findEvaluation() ?: return@createPrimaryButton evaluationTargetService.load(evaluation.id) selectedTabIndex = tabs.selectedIndex removeAll() @@ -223,12 +231,17 @@ class SelectionView( } } - private fun createJudgeAllButton(): Button { + private fun createJudgeAllButton(tabs: Tabs): Button { return createContrastButtonWithDialog("전체 자동 채점하기", "자동 채점을 실행하시겠습니까?") { - // TODO: 전체 자동 채점 기능 구현 + val evaluation = tabs.findEvaluation() ?: return@createContrastButtonWithDialog + judgmentAllService.judgeAll(evaluation.id) } } + private fun Tabs.findEvaluation(): EvaluationSelectData? { + return evaluations.find { it.title == selectedTab.label } + } + private fun createEvaluationFileUpload(): Upload { return createCsvUpload("평가지 업로드", MemoryBuffer()) { val evaluation = evaluations[tabs.selectedIndex - 1] diff --git a/src/main/kotlin/apply/ui/api/JudgmentRestController.kt b/src/main/kotlin/apply/ui/api/JudgmentRestController.kt index 9ada0480a..076efc3e4 100644 --- a/src/main/kotlin/apply/ui/api/JudgmentRestController.kt +++ b/src/main/kotlin/apply/ui/api/JudgmentRestController.kt @@ -2,6 +2,7 @@ package apply.ui.api import apply.application.CancelJudgmentRequest import apply.application.FailJudgmentRequest +import apply.application.JudgmentAllService import apply.application.JudgmentService import apply.application.LastJudgmentResponse import apply.application.SuccessJudgmentRequest @@ -9,6 +10,7 @@ import apply.domain.user.User import apply.security.Accessor import apply.security.LoginUser import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -18,7 +20,8 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api") @RestController class JudgmentRestController( - private val judgmentService: JudgmentService + private val judgmentService: JudgmentService, + private val judgmentAllService: JudgmentAllService ) { @PostMapping("/recruitments/{recruitmentId}/missions/{missionId}/judgments/judge-example") fun judgeExample( @@ -30,13 +33,13 @@ class JudgmentRestController( return ResponseEntity.ok(ApiResponse.success(response)) } - @PostMapping("/recruitments/{recruitmentId}/missions/{missionId}/judgments/judge-real") - fun judgeReal( + @GetMapping("/recruitments/{recruitmentId}/missions/{missionId}/judgments/judge-example") + fun findExample( @PathVariable recruitmentId: Long, @PathVariable missionId: Long, - @LoginUser(administrator = true) user: User + @LoginUser user: User ): ResponseEntity> { - val response = judgmentService.judgeReal(user.id, missionId) + val response = judgmentService.findLastExampleJudgment(user.id, missionId) return ResponseEntity.ok(ApiResponse.success(response)) } @@ -45,7 +48,7 @@ class JudgmentRestController( @PathVariable judgmentId: Long, @RequestBody request: SuccessJudgmentRequest, @Accessor("lambda") ignored: Unit - ): ResponseEntity { + ): ResponseEntity { judgmentService.success(judgmentId, request) return ResponseEntity.ok().build() } @@ -55,7 +58,7 @@ class JudgmentRestController( @PathVariable judgmentId: Long, @RequestBody request: FailJudgmentRequest, @Accessor("lambda") ignored: Unit - ): ResponseEntity { + ): ResponseEntity { judgmentService.fail(judgmentId, request) return ResponseEntity.ok().build() } @@ -65,8 +68,27 @@ class JudgmentRestController( @PathVariable judgmentId: Long, @RequestBody request: CancelJudgmentRequest, @Accessor("lambda") ignored: Unit - ): ResponseEntity { + ): ResponseEntity { judgmentService.cancel(judgmentId, request) return ResponseEntity.ok().build() } + + @PostMapping("/evaluations/{evaluationId}/assignments/{assignmentId}/judgments/judge-real") + fun judgeReal( + @PathVariable evaluationId: Long, + @PathVariable assignmentId: Long, + @LoginUser(administrator = true) user: User + ): ResponseEntity> { + val response = judgmentService.judgeReal(assignmentId) + return ResponseEntity.ok(ApiResponse.success(response)) + } + + @PostMapping("/evaluations/{evaluationId}/judgments/judge-all") + fun judgeAll( + @PathVariable evaluationId: Long, + @LoginUser(administrator = true) user: User + ): ResponseEntity { + judgmentAllService.judgeAll(evaluationId) + return ResponseEntity.ok().build() + } } diff --git a/src/main/kotlin/apply/ui/api/MissionRestController.kt b/src/main/kotlin/apply/ui/api/MissionRestController.kt index 8ec8280d1..b761f99c2 100644 --- a/src/main/kotlin/apply/ui/api/MissionRestController.kt +++ b/src/main/kotlin/apply/ui/api/MissionRestController.kt @@ -5,6 +5,7 @@ import apply.application.MissionData import apply.application.MissionResponse import apply.application.MissionService import apply.application.MyMissionResponse +import apply.application.MyMissionService import apply.domain.user.User import apply.security.LoginUser import org.springframework.http.ResponseEntity @@ -20,7 +21,8 @@ import support.toUri @RequestMapping("/api/recruitments/{recruitmentId}/missions") @RestController class MissionRestController( - private val missionService: MissionService + private val missionService: MissionService, + private val missionQueryService: MyMissionService ) { @PostMapping fun save( @@ -57,7 +59,7 @@ class MissionRestController( @PathVariable recruitmentId: Long, @LoginUser user: User ): ResponseEntity>> { - val responses = missionService.findAllByUserIdAndRecruitmentId(user.id, recruitmentId) + val responses = missionQueryService.findAllByUserIdAndRecruitmentId(user.id, recruitmentId) return ResponseEntity.ok(ApiResponse.success(responses)) } diff --git a/src/main/kotlin/apply/ui/api/UserRestController.kt b/src/main/kotlin/apply/ui/api/UserRestController.kt index 68cf409d0..ef09bb2a7 100644 --- a/src/main/kotlin/apply/ui/api/UserRestController.kt +++ b/src/main/kotlin/apply/ui/api/UserRestController.kt @@ -79,16 +79,16 @@ class UserRestController( @RequestParam keyword: String, @LoginUser(administrator = true) user: User ): ResponseEntity>> { - val users = userService.findAllByKeyword(keyword) - return ResponseEntity.ok(ApiResponse.success(users)) + val responses = userService.findAllByKeyword(keyword) + return ResponseEntity.ok(ApiResponse.success(responses)) } @GetMapping("/me") fun getMyInformation( @LoginUser user: User ): ResponseEntity> { - val user = userService.getInformation(user.id) - return ResponseEntity.ok(ApiResponse.success(user)) + val response = userService.getInformation(user.id) + return ResponseEntity.ok(ApiResponse.success(response)) } @PatchMapping("/information") diff --git a/src/test/kotlin/apply/MissionFixtures.kt b/src/test/kotlin/apply/MissionFixtures.kt index 02358af39..8601415c8 100644 --- a/src/test/kotlin/apply/MissionFixtures.kt +++ b/src/test/kotlin/apply/MissionFixtures.kt @@ -2,6 +2,7 @@ package apply import apply.application.EvaluationSelectData import apply.application.JudgmentItemData +import apply.application.LastJudgmentResponse import apply.application.MissionData import apply.application.MissionResponse import apply.application.MyMissionResponse @@ -79,7 +80,20 @@ fun createMyMissionResponse( startDateTime: LocalDateTime = START_DATE_TIME, endDateTime: LocalDateTime = END_DATE_TIME, missionStatus: MissionStatus = MissionStatus.SUBMITTING, + runnable: Boolean = true, + judgment: LastJudgmentResponse? = createLastJudgmentResponse(), id: Long = 0L ): MyMissionResponse { - return MyMissionResponse(id, title, description, submittable, submitted, startDateTime, endDateTime, missionStatus) + return MyMissionResponse( + id, + title, + description, + submittable, + submitted, + startDateTime, + endDateTime, + missionStatus, + runnable, + judgment + ) } diff --git a/src/test/kotlin/apply/application/JudgmentIntegrationTest.kt b/src/test/kotlin/apply/application/JudgmentIntegrationTest.kt new file mode 100644 index 000000000..29626f154 --- /dev/null +++ b/src/test/kotlin/apply/application/JudgmentIntegrationTest.kt @@ -0,0 +1,263 @@ +package apply.application + +import apply.createAssignment +import apply.createCancelJudgmentRequest +import apply.createCommit +import apply.createFailJudgmentRequest +import apply.createJudgment +import apply.createJudgmentItem +import apply.createJudgmentRecord +import apply.createMission +import apply.createSuccessJudgmentRequest +import apply.domain.assignment.AssignmentRepository +import apply.domain.judgment.AssignmentArchive +import apply.domain.judgment.JudgmentCancelledEvent +import apply.domain.judgment.JudgmentFailedEvent +import apply.domain.judgment.JudgmentRepository +import apply.domain.judgment.JudgmentResult +import apply.domain.judgment.JudgmentStartedEvent +import apply.domain.judgment.JudgmentStatus.CANCELLED +import apply.domain.judgment.JudgmentStatus.FAILED +import apply.domain.judgment.JudgmentStatus.STARTED +import apply.domain.judgment.JudgmentStatus.SUCCEEDED +import apply.domain.judgment.JudgmentSucceededEvent +import apply.domain.judgment.JudgmentTouchedEvent +import apply.domain.judgment.JudgmentType.EXAMPLE +import apply.domain.judgment.JudgmentType.REAL +import apply.domain.judgment.getById +import apply.domain.judgmentitem.JudgmentItemRepository +import apply.domain.mission.MissionRepository +import com.ninjasquad.springmockk.MockkBean +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.springframework.context.annotation.Import +import support.test.IntegrationTest +import support.test.context.event.Events +import support.test.context.event.RecordEventsConfiguration +import support.test.spec.afterRootTest +import java.time.LocalDateTime.now + +@MockkBean(value = [AssignmentArchive::class, JudgmentAgency::class], relaxUnitFun = true) +@Import(RecordEventsConfiguration::class) +@IntegrationTest +class JudgmentIntegrationTest( + private val judgmentService: JudgmentService, + private val missionRepository: MissionRepository, + private val judgmentItemRepository: JudgmentItemRepository, + private val assignmentRepository: AssignmentRepository, + private val judgmentRepository: JudgmentRepository, + private val assignmentArchive: AssignmentArchive, + private val events: Events +) : BehaviorSpec({ + Given("과제 제출물을 제출할 수 있는 특정 과제에 대한 과제 제출물이 있는 경우") { + val userId = 1L + val mission = missionRepository.save(createMission(submittable = true)) + judgmentItemRepository.save(createJudgmentItem(mission.id)) + assignmentRepository.save(createAssignment(userId, mission.id)) + val commit = createCommit() + + every { assignmentArchive.getLastCommit(any(), any()) } returns commit + + When("해당 과제 제출물의 예제 테스트를 실행하면") { + val actual = judgmentService.judgeExample(userId, mission.id) + + Then("마지막 커밋에 대한 자동 채점 기록을 확인할 수 있고 자동 채점이 저장된다") { + assertSoftly(actual) { + commitHash shouldBe commit.hash + status shouldBe STARTED + passCount shouldBe 0 + totalCount shouldBe 0 + } + events.count() shouldBe 1 + judgmentRepository.findAll().shouldHaveSize(1) + } + } + } + + Given("특정 과제 제출물에 대한 마지막 커밋이 없어 예외가 발생하는 경우") { + val userId = 1L + val mission = missionRepository.save(createMission(submittable = true)) + judgmentItemRepository.save(createJudgmentItem(mission.id)) + val assignment = assignmentRepository.save(createAssignment(userId, mission.id)) + + every { assignmentArchive.getLastCommit(any(), any()) } throws RuntimeException() + + When("해당 과제 제출물의 예제 테스트를 실행하면") { + Then("예외가 발생하고 자동 채점이 저장되지 않는다") { + shouldThrowAny { + judgmentService.judgeExample(userId, mission.id) + } + judgmentRepository.findAll().shouldBeEmpty() + } + } + + When("해당 과제 제출물의 본 자동 채점을 실행하면") { + Then("예외가 발생하고 자동 채점이 저장되지 않는다") { + shouldThrowAny { + judgmentService.judgeReal(assignment.id) + } + judgmentRepository.findAll().shouldBeEmpty() + } + } + + When("해당 과제 제출물의 자동 채점을 실행하면") { + Then("예외가 발생하고 자동 채점이 저장되지 않는다") { + shouldThrowAny { + judgmentService.judge(mission, assignment, REAL) + } + judgmentRepository.findAll().shouldBeEmpty() + } + } + } + + Given("특정 과제 제출물에 대한 예제 자동 채점 기록이 있고 이전 커밋과 마지막 커밋이 동일한 경우") { + val userId = 1L + val mission = missionRepository.save(createMission(submittable = true)) + judgmentItemRepository.save(createJudgmentItem(mission.id)) + val assignment = assignmentRepository.save(createAssignment(userId, mission.id)) + val commit = createCommit() + judgmentRepository.save( + createJudgment( + assignment.id, EXAMPLE, + listOf( + createJudgmentRecord( + commit, + JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED), + completedDateTime = now() + ) + ) + ) + ) + + every { assignmentArchive.getLastCommit(any(), any()) } returns commit + + When("해당 과제 제출물의 예제 테스트를 실행하면") { + val actual = judgmentService.judgeExample(userId, mission.id) + + Then("해당 커밋에 대한 자동 채점 기록을 확인할 수 있다") { + assertSoftly(actual) { + commitHash shouldBe commit.hash + status shouldBe SUCCEEDED + passCount shouldBe 9 + totalCount shouldBe 10 + } + events.count() shouldBe 0 + events.count() shouldBe 1 + } + } + } + + Given("특정 과제 제출물에 대한 본 자동 채점 기록이 있고 이전 커밋과 마지막 커밋이 동일한 경우") { + val mission = missionRepository.save(createMission()) + judgmentItemRepository.save(createJudgmentItem(mission.id)) + val assignment = assignmentRepository.save(createAssignment(1L, mission.id)) + val commit = createCommit() + judgmentRepository.save( + createJudgment( + assignment.id, REAL, + listOf( + createJudgmentRecord( + commit, + JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED), + completedDateTime = now() + ) + ) + ) + ) + + every { assignmentArchive.getLastCommit(any(), any()) } returns commit + + When("해당 과제 제출물의 본 자동 채점을 실행하면") { + val actual = judgmentService.judgeReal(assignment.id) + + Then("해당 커밋에 대한 자동 채점 기록을 확인할 수 있다") { + assertSoftly(actual) { + commitHash shouldBe commit.hash + status shouldBe SUCCEEDED + passCount shouldBe 9 + totalCount shouldBe 10 + } + events.count() shouldBe 0 + events.count() shouldBe 1 + } + } + } + + Given("특정 예제 자동 채점에 특정 커밋에 대한 자동 채점 기록이 있는 경우") { + val assignment = assignmentRepository.save(createAssignment(1L, 1L)) + val commit = createCommit() + val judgment = judgmentRepository.save( + createJudgment(assignment.id, EXAMPLE, listOf(createJudgmentRecord(commit))) + ) + + When("해당 커밋의 자동 채점이 성공하면") { + judgmentService.success( + judgment.id, + createSuccessJudgmentRequest(commit.hash, passCount = 9, totalCount = 10) + ) + + Then("자동 채점 기록의 상태가 성공이 된다") { + val actual = judgmentRepository.getById(judgment.id) + actual.lastRecord.result shouldBe JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED) + events.count() shouldBe 1 + } + } + } + + Given("특정 본 자동 채점의 특정 커밋에 대한 자동 채점 기록이 있는 경우") { + val assignment = assignmentRepository.save(createAssignment(1L, 1L)) + val commit = createCommit() + val judgment = judgmentRepository.save( + createJudgment(assignment.id, REAL, listOf(createJudgmentRecord(commit))) + ) + + When("해당 커밋의 자동 채점이 성공하면") { + judgmentService.success( + judgment.id, + createSuccessJudgmentRequest(commit.hash, passCount = 9, totalCount = 10) + ) + + Then("자동 채점 기록의 상태가 성공이 된다") { + val actual = judgmentRepository.getById(judgment.id) + actual.lastRecord.result shouldBe JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED) + events.count() shouldBe 1 + } + } + + When("해당 커밋의 자동 채점이 실패하면") { + judgmentService.fail(judgment.id, createFailJudgmentRequest(commit.hash)) + + Then("자동 채점 기록의 상태가 실패가 된다") { + val actual = judgmentRepository.getById(judgment.id) + actual.lastRecord.result.status shouldBe FAILED + events.count() shouldBe 1 + } + } + + When("해당 커밋의 자동 채점이 취소되면") { + judgmentService.cancel(judgment.id, createCancelJudgmentRequest(commit.hash)) + + Then("자동 채점 기록의 상태가 취소가 된다") { + val actual = judgmentRepository.getById(judgment.id) + actual.lastRecord.result.status shouldBe CANCELLED + events.count() shouldBe 1 + } + } + } + + afterEach { + events.clear() + } + + afterRootTest { + judgmentRepository.deleteAll() + assignmentRepository.deleteAll() + judgmentItemRepository.deleteAll() + missionRepository.deleteAll() + } +}) diff --git a/src/test/kotlin/apply/application/JudgmentServiceTest.kt b/src/test/kotlin/apply/application/JudgmentServiceTest.kt index 112750aa7..1c73a7587 100644 --- a/src/test/kotlin/apply/application/JudgmentServiceTest.kt +++ b/src/test/kotlin/apply/application/JudgmentServiceTest.kt @@ -3,32 +3,28 @@ package apply.application import apply.COMMIT_HASH import apply.PULL_REQUEST_URL import apply.createAssignment -import apply.createCancelJudgmentRequest import apply.createCommit -import apply.createFailJudgmentRequest import apply.createJudgment import apply.createJudgmentRecord import apply.createMission -import apply.createSuccessJudgmentRequest import apply.domain.assignment.AssignmentRepository +import apply.domain.assignment.getById import apply.domain.assignment.getByUserIdAndMissionId -import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.judgment.AssignmentArchive import apply.domain.judgment.JudgmentRepository import apply.domain.judgment.JudgmentResult -import apply.domain.judgment.JudgmentStatus.CANCELLED -import apply.domain.judgment.JudgmentStatus.FAILED import apply.domain.judgment.JudgmentStatus.STARTED import apply.domain.judgment.JudgmentStatus.SUCCEEDED import apply.domain.judgment.JudgmentType.EXAMPLE import apply.domain.judgment.JudgmentType.REAL -import apply.domain.judgment.getById import apply.domain.judgmentitem.JudgmentItemRepository import apply.domain.mission.MissionRepository import apply.domain.mission.getById import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.mockk.clearAllMocks import io.mockk.every @@ -41,7 +37,6 @@ class JudgmentServiceTest : BehaviorSpec({ val assignmentRepository = mockk() val missionRepository = mockk() val judgmentItemRepository = mockk() - val evaluationTargetRepository = mockk() val assignmentArchive = mockk() val judgmentService = JudgmentService( @@ -49,7 +44,6 @@ class JudgmentServiceTest : BehaviorSpec({ assignmentRepository, missionRepository, judgmentItemRepository, - evaluationTargetRepository, assignmentArchive ) @@ -71,15 +65,15 @@ class JudgmentServiceTest : BehaviorSpec({ val mission = createMission(submittable = false, id = 1L) val assignment = createAssignment(missionId = mission.id, pullRequestUrl = PULL_REQUEST_URL, id = 1L) + every { assignmentRepository.getById(any()) } returns assignment every { missionRepository.getById(any()) } returns mission - every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentItemRepository.existsByMissionId(any()) } returns true - every { assignmentArchive.getLastCommit(any(), any()) } returns createCommit() every { judgmentRepository.findByAssignmentIdAndType(any(), any()) } returns null - every { judgmentRepository.save(any()) } returns createJudgment(assignmentId = assignment.id, type = REAL) + every { judgmentRepository.save(any()) } answers { firstArg() } + every { assignmentArchive.getLastCommit(any(), any()) } returns createCommit() When("해당 과제 제출물의 본 자동 채점을 실행하면") { - val actual = judgmentService.judgeReal(1L, mission.id) + val actual = judgmentService.judgeReal(assignment.id) Then("자동 채점 기록을 확인할 수 있다") { assertSoftly(actual) { @@ -95,8 +89,8 @@ class JudgmentServiceTest : BehaviorSpec({ val mission = createMission(submittable = true, id = 1L) val assignment = createAssignment(missionId = mission.id, pullRequestUrl = PULL_REQUEST_URL, id = 1L) + every { assignmentRepository.getById(any()) } returns assignment every { missionRepository.getById(any()) } returns mission - every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentItemRepository.existsByMissionId(any()) } returns false When("해당 과제 제출물의 예제 테스트를 실행하면") { @@ -110,7 +104,7 @@ class JudgmentServiceTest : BehaviorSpec({ When("해당 과제 제출물의 본 자동 채점을 실행하면") { Then("예외가 발생한다") { shouldThrow { - judgmentService.judgeReal(1L, mission.id) + judgmentService.judgeReal(assignment.id) } } } @@ -133,11 +127,11 @@ class JudgmentServiceTest : BehaviorSpec({ ) every { missionRepository.getById(any()) } returns mission - every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentItemRepository.existsByMissionId(any()) } returns true + every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentRepository.findByAssignmentIdAndType(any(), any()) } returns judgment - every { assignmentArchive.getLastCommit(any(), any()) } returns commit - every { judgmentRepository.save(any()) } returns judgment + every { assignmentArchive.getLastCommit(any(), any()) } returns createCommit() + every { judgmentRepository.save(any()) } answers { firstArg() } When("해당 과제 제출물의 예제 테스트를 실행하면") { val actual = judgmentService.judgeExample(1L, mission.id) @@ -169,6 +163,7 @@ class JudgmentServiceTest : BehaviorSpec({ ) ) + every { assignmentRepository.getById(any()) } returns assignment every { missionRepository.getById(any()) } returns mission every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentItemRepository.existsByMissionId(any()) } returns true @@ -177,7 +172,7 @@ class JudgmentServiceTest : BehaviorSpec({ every { judgmentRepository.save(any()) } returns judgment When("해당 과제 제출물의 본 자동 채점을 실행하면") { - val actual = judgmentService.judgeReal(1L, mission.id) + val actual = judgmentService.judgeReal(assignment.id) Then("동일한 자동 채점 기록을 확인할 수 있다") { assertSoftly(actual) { @@ -241,6 +236,7 @@ class JudgmentServiceTest : BehaviorSpec({ ) val commit = createCommit("commit2") + every { assignmentRepository.getById(any()) } returns assignment every { missionRepository.getById(any()) } returns mission every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment every { judgmentItemRepository.existsByMissionId(any()) } returns true @@ -249,7 +245,7 @@ class JudgmentServiceTest : BehaviorSpec({ every { judgmentRepository.save(any()) } returns judgment When("해당 과제 제출물의 본 자동 채점을 실행하면") { - val actual = judgmentService.judgeReal(1L, mission.id) + val actual = judgmentService.judgeReal(assignment.id) Then("최신 커밋에 대한 자동 채점 기록을 확인할 수 있다") { assertSoftly(actual) { @@ -262,37 +258,81 @@ class JudgmentServiceTest : BehaviorSpec({ } } - Given("특정 자동 채점의 특정 커밋에 대한 자동 채점 기록이 있는 경우") { - val commit = createCommit() - val record = createJudgmentRecord(commit, JudgmentResult(), completedDateTime = null) - val judgment = createJudgment(records = listOf(record), id = 1L) + Given("특정 과제 제출물에 대한 예제 자동 채점 기록이 있는 경우") { + val assignment = createAssignment(pullRequestUrl = PULL_REQUEST_URL) + val judgment = createJudgment( + assignmentId = assignment.id, + type = EXAMPLE, + records = listOf( + createJudgmentRecord( + commit = createCommit(COMMIT_HASH), + result = JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED), + completedDateTime = now() + ) + ) + ) - every { judgmentRepository.getById(any()) } returns judgment + every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment + every { judgmentRepository.findByAssignmentIdAndType(any(), any()) } returns judgment - When("해당 커밋의 자동 채점이 성공하면") { - judgmentService.success(judgment.id, createSuccessJudgmentRequest(commit = commit.hash)) + When("예제 자동 채점 결과를 조회하면") { + val actual = judgmentService.findLastExampleJudgment(1L, 1L) - Then("자동 채점 기록의 상태가 성공으로 변경된다") { - record.commit shouldBe commit - record.status shouldBe SUCCEEDED + Then("예제 자동 채점 결과를 확인할 수 있다") { + actual.shouldNotBeNull() + assertSoftly(actual) { + commitHash shouldBe COMMIT_HASH + passCount shouldBe 9 + totalCount shouldBe 10 + status shouldBe SUCCEEDED + } } } + } - When("해당 커밋의 자동 채점이 실패하면") { - judgmentService.fail(judgment.id, createFailJudgmentRequest(commit = commit.hash)) + Given("특정 과제 제출물에 대한 본 자동 채점 기록이 있는 경우") { + val assignment = createAssignment(pullRequestUrl = PULL_REQUEST_URL) + val judgment = createJudgment( + assignmentId = assignment.id, + type = REAL, + records = listOf( + createJudgmentRecord( + commit = createCommit(COMMIT_HASH), + result = JudgmentResult(passCount = 9, totalCount = 10, status = SUCCEEDED), + completedDateTime = now() + ) + ) + ) - Then("자동 채점 기록의 상태가 실패로 변경된다") { - record.commit shouldBe commit - record.status shouldBe FAILED + every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment + every { judgmentRepository.findByAssignmentIdAndType(any(), any()) } returns judgment + + When("본 자동 채점 결과를 조회하면") { + val actual = judgmentService.findLastExampleJudgment(1L, 1L) + + Then("본 자동 채점 결과를 확인할 수 있다") { + actual.shouldNotBeNull() + assertSoftly(actual) { + commitHash shouldBe COMMIT_HASH + passCount shouldBe 9 + totalCount shouldBe 10 + status shouldBe SUCCEEDED + } } } + } + + Given("특정 과제 제출물에 대한 예제 자동 채점 기록이 없는 경우") { + val assignment = createAssignment(pullRequestUrl = PULL_REQUEST_URL) + + every { assignmentRepository.getByUserIdAndMissionId(any(), any()) } returns assignment + every { judgmentRepository.findByAssignmentIdAndType(any(), any()) } returns null - When("해당 커밋의 자동 채점이 취소되면") { - judgmentService.cancel(judgment.id, createCancelJudgmentRequest(commit = commit.hash)) + When("예제 자동 채점 결과를 조회하면") { + val actual = judgmentService.findLastExampleJudgment(1L, 1L) - Then("자동 채점 기록의 상태가 취소로 변경된다") { - record.commit shouldBe commit - record.status shouldBe CANCELLED + Then("null을 반환한다") { + actual.shouldBeNull() } } } diff --git a/src/test/kotlin/apply/application/MissionServiceTest.kt b/src/test/kotlin/apply/application/MissionServiceTest.kt index a53e320ce..3d7b876c8 100644 --- a/src/test/kotlin/apply/application/MissionServiceTest.kt +++ b/src/test/kotlin/apply/application/MissionServiceTest.kt @@ -1,14 +1,11 @@ package apply.application -import apply.createAssignment import apply.createEvaluation import apply.createMission import apply.createMissionData import apply.createMissionResponse -import apply.domain.assignment.AssignmentRepository import apply.domain.evaluation.EvaluationRepository import apply.domain.evaluationitem.EvaluationItemRepository -import apply.domain.evaluationtarget.EvaluationTargetRepository import apply.domain.judgmentitem.JudgmentItemRepository import apply.domain.mission.MissionRepository import io.kotest.assertions.throwables.shouldNotThrowAny @@ -27,17 +24,13 @@ import support.test.spec.afterRootTest class MissionServiceTest : BehaviorSpec({ val missionRepository = mockk() val evaluationRepository = mockk() - val evaluationTargetRepository = mockk() val evaluationItemRepository = mockk() - val assignmentRepository = mockk() val judgmentItemRepository = mockk() val missionService = MissionService( missionRepository, evaluationRepository, - evaluationTargetRepository, evaluationItemRepository, - assignmentRepository, judgmentItemRepository ) @@ -103,34 +96,6 @@ class MissionServiceTest : BehaviorSpec({ } } - Given("과제 및 평가 대상자가 있는 평가가 있고 해당 평가 대상자가 제출한 과제 제출물이 있는 경우") { - val mission1 = createMission(id = 1L, hidden = false) - val mission2 = createMission(id = 2L, hidden = false) - val recruitmentId = 1L - val userId = 1L - - every { evaluationRepository.findAllByRecruitmentId(any()) } returns listOf( - createEvaluation(recruitmentId = recruitmentId, id = 1L), - createEvaluation(recruitmentId = recruitmentId, id = 2L) - ) - every { evaluationTargetRepository.existsByUserIdAndEvaluationId(any(), any()) } returns true - every { missionRepository.findAllByEvaluationIdIn(any()) } returns listOf(mission1, mission2) - every { assignmentRepository.findAllByUserId(any()) } returns listOf( - createAssignment(id = userId, missionId = mission1.id) - ) - - When("해당 사용자의 특정 모집에 대한 모든 과제를 조회하면") { - val actual = missionService.findAllByUserIdAndRecruitmentId(userId, recruitmentId) - - Then("과제 및 과제 제출물이 제출되었는지 확인할 수 있다") { - actual shouldContainExactlyInAnyOrder listOf( - MyMissionResponse(mission1, true), - MyMissionResponse(mission2, false) - ) - } - } - } - Given("과제 제출물을 제출할 수 없는 과제가 있는 경우") { val mission = createMission(submittable = false) diff --git a/src/test/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepositoryTest.kt b/src/test/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepositoryTest.kt index 87060d9c2..160a0f4d0 100644 --- a/src/test/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepositoryTest.kt +++ b/src/test/kotlin/apply/domain/evaluationtarget/EvaluationTargetRepositoryTest.kt @@ -6,7 +6,6 @@ import apply.domain.evaluationtarget.EvaluationStatus.PASS import io.kotest.core.spec.style.ExpectSpec import io.kotest.extensions.spring.SpringExtension import io.kotest.inspectors.forAll -import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.longs.shouldBeZero import io.kotest.matchers.shouldBe @@ -32,13 +31,6 @@ class EvaluationTargetRepositoryTest( val actual = evaluationTargetRepository.findAllByEvaluationId(evaluationId) actual shouldHaveSize 2 } - - expect("특정 회원이 특정 평가의 평가 대상자인지 확인한다") { - listOf(1L, 2L).forAll { userId -> - val actual = evaluationTargetRepository.existsByUserIdAndEvaluationId(userId, evaluationId) - actual.shouldBeTrue() - } - } } context("평가 상태가 다른 평가 대상자 조회") { diff --git a/src/test/kotlin/apply/infra/github/GitHubClientTest.kt b/src/test/kotlin/apply/infra/github/GitHubClientTest.kt index a3e32e379..f157d25ea 100644 --- a/src/test/kotlin/apply/infra/github/GitHubClientTest.kt +++ b/src/test/kotlin/apply/infra/github/GitHubClientTest.kt @@ -26,8 +26,14 @@ class GitHubClientTest( } "해당 커밋이 없으면 예외가 발생한다" { - shouldThrow { + shouldThrow { gitHubClient.getLastCommit(PULL_REQUEST_URL, createLocalDateTime(2018)) } } + + "PR이 없으면 예외가 발생한다" { + shouldThrow { + gitHubClient.getLastCommit("https://github.com/woowacourse/service-apply/pull/1", now) + } + } }) diff --git a/src/test/kotlin/apply/ui/api/JudgmentRestControllerTest.kt b/src/test/kotlin/apply/ui/api/JudgmentRestControllerTest.kt index 907cc28ef..166c379ad 100644 --- a/src/test/kotlin/apply/ui/api/JudgmentRestControllerTest.kt +++ b/src/test/kotlin/apply/ui/api/JudgmentRestControllerTest.kt @@ -1,5 +1,6 @@ package apply.ui.api +import apply.application.JudgmentAllService import apply.application.JudgmentService import apply.createCancelJudgmentRequest import apply.createFailJudgmentRequest @@ -12,6 +13,7 @@ import io.mockk.just import org.junit.jupiter.api.Test import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.test.web.servlet.get import org.springframework.test.web.servlet.post import support.test.web.servlet.bearer @@ -20,6 +22,9 @@ class JudgmentRestControllerTest : RestControllerTest() { @MockkBean private lateinit var judgmentService: JudgmentService + @MockkBean + private lateinit var judgmentAllService: JudgmentAllService + @Test fun `예제 테스트를 실행한다`() { val response = createLastJudgmentResponse() @@ -36,15 +41,17 @@ class JudgmentRestControllerTest : RestControllerTest() { } @Test - fun `본 자동 채점을 실행한다`() { + fun `예제 테스트 결과를 조회한다`() { val response = createLastJudgmentResponse() - every { judgmentService.judgeReal(any(), any()) } returns response + every { judgmentService.findLastExampleJudgment(any(), any()) } returns response - mockMvc.post("/api/recruitments/{recruitmentId}/missions/{missionId}/judgments/judge-real", 1L, 1L) { + mockMvc.get("/api/recruitments/{recruitmentId}/missions/{missionId}/judgments/judge-example", 1L, 1L) { bearer("valid_token") }.andExpect { status { isOk } content { success(response) } + }.andDo { + handle(document("judgment-judge-example-get")) } } @@ -57,7 +64,7 @@ class JudgmentRestControllerTest : RestControllerTest() { }.andExpect { status { isOk } }.andDo { - handle(document("judgment-success-result-post")) + handle(document("judgment-success-post")) } } @@ -70,7 +77,7 @@ class JudgmentRestControllerTest : RestControllerTest() { }.andExpect { status { isOk } }.andDo { - handle(document("judgment-fail-result-post")) + handle(document("judgment-fail-post")) } } @@ -83,7 +90,31 @@ class JudgmentRestControllerTest : RestControllerTest() { }.andExpect { status { isOk } }.andDo { - handle(document("judgment-cancel-result-post")) + handle(document("judgment-cancel-post")) + } + } + + @Test + fun `본 자동 채점을 실행한다`() { + val response = createLastJudgmentResponse() + every { judgmentService.judgeReal(any()) } returns response + + mockMvc.post("/api/evaluations/{evaluationId}/assignments/{assignmentId}/judgments/judge-real", 1L, 1L) { + bearer("valid_token") + }.andExpect { + status { isOk } + content { success(response) } + } + } + + @Test + fun `전체 자동 채점을 실행한다`() { + every { judgmentAllService.judgeAll(any()) } just Runs + + mockMvc.post("/api/evaluations/{evaluationId}/judgments/judge-all", 1L) { + bearer("valid_token") + }.andExpect { + status { isOk } } } } diff --git a/src/test/kotlin/apply/ui/api/MissionRestControllerTest.kt b/src/test/kotlin/apply/ui/api/MissionRestControllerTest.kt index 667368227..3441c7555 100644 --- a/src/test/kotlin/apply/ui/api/MissionRestControllerTest.kt +++ b/src/test/kotlin/apply/ui/api/MissionRestControllerTest.kt @@ -2,11 +2,14 @@ package apply.ui.api import apply.application.MissionAndEvaluationResponse import apply.application.MissionService +import apply.application.MyMissionService import apply.createEvaluation +import apply.createLastJudgmentResponse import apply.createMission import apply.createMissionData import apply.createMissionResponse import apply.createMyMissionResponse +import apply.domain.judgment.JudgmentStatus import com.ninjasquad.springmockk.MockkBean import io.mockk.Runs import io.mockk.every @@ -24,6 +27,9 @@ class MissionRestControllerTest : RestControllerTest() { @MockkBean private lateinit var missionService: MissionService + @MockkBean + private lateinit var missionQueryService: MyMissionService + @Test fun `과제를 추가한다`() { val response = createMissionResponse(id = 1L) @@ -69,8 +75,17 @@ class MissionRestControllerTest : RestControllerTest() { @Test fun `나의 과제들을 조회한다`() { - val responses = listOf(createMyMissionResponse(id = 1L), createMyMissionResponse(id = 2L)) - every { missionService.findAllByUserIdAndRecruitmentId(any(), any()) } returns responses + val responses = listOf( + createMyMissionResponse(id = 1L, runnable = false, judgment = null), + createMyMissionResponse(id = 2L, runnable = true, judgment = createLastJudgmentResponse()), + createMyMissionResponse( + id = 3L, + runnable = true, + judgment = createLastJudgmentResponse(passCount = 9, totalCount = 10, status = JudgmentStatus.SUCCEEDED) + ) + ) + + every { missionQueryService.findAllByUserIdAndRecruitmentId(any(), any()) } returns responses mockMvc.get("/api/recruitments/{recruitmentId}/missions/me", 1L) { bearer("valid_token") diff --git a/src/test/kotlin/support/test/context/event/Events.kt b/src/test/kotlin/support/test/context/event/Events.kt new file mode 100644 index 000000000..94cd972d8 --- /dev/null +++ b/src/test/kotlin/support/test/context/event/Events.kt @@ -0,0 +1,19 @@ +package support.test.context.event + +import org.springframework.context.event.EventListener + +class Events { + private val _events: MutableList = mutableListOf() + val events: List + get() = _events + + @EventListener + internal fun addEvent(event: Any) { + _events.add(event) + } + + inline fun events(): List = events.filterIsInstance(T::class.java) + inline fun count(): Int = events().count() + inline fun first(): T = events().first() + fun clear() = _events.clear() +} diff --git a/src/test/kotlin/support/test/context/event/RecordEventsConfiguration.kt b/src/test/kotlin/support/test/context/event/RecordEventsConfiguration.kt new file mode 100644 index 000000000..4ec7675cd --- /dev/null +++ b/src/test/kotlin/support/test/context/event/RecordEventsConfiguration.kt @@ -0,0 +1,15 @@ +package support.test.context.event + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean + +/** + * Spring Boot 2.4.2 이후 `@RecordApplicationEvents`로 대체 + */ +@TestConfiguration +class RecordEventsConfiguration { + @Bean + fun applicationEvents(): Events { + return Events() + } +}