diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..5cbdc074c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+*.tex linguist-detectable=false
+*.bib linguist-detectable=false
+*.bibtex linguist-detectable=false
diff --git a/.github/workflows/ci.yml.disabled b/.github/workflows/ci.yml
similarity index 100%
rename from .github/workflows/ci.yml.disabled
rename to .github/workflows/ci.yml
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
new file mode 100644
index 000000000..d1c691708
--- /dev/null
+++ b/.github/workflows/sonarqube.yml
@@ -0,0 +1,36 @@
+name: SonarQube
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, synchronize, reopened]
+jobs:
+ build:
+ name: Build and analyze
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: 17
+ distribution: 'zulu' # Alternative distribution options are available.
+ - name: Cache SonarQube packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache Maven packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2
+ - name: Build and analyze
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=pet-ads_systematic
\ No newline at end of file
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
new file mode 100644
index 000000000..a7265e4c8
--- /dev/null
+++ b/.idea/dictionaries/project.xml
@@ -0,0 +1,7 @@
+
+
+
+ picoc
+
+
+
\ No newline at end of file
diff --git a/account/pom.xml b/account/pom.xml
index 34e1db50c..5ae3d627f 100644
--- a/account/pom.xml
+++ b/account/pom.xml
@@ -30,22 +30,7 @@
org.jetbrains.kotlin
kotlin-maven-plugin
-
-
- compile
- compile
-
- compile
-
-
-
- test-compile
- test-compile
-
- test-compile
-
-
-
+ true
maven-surefire-plugin
@@ -72,6 +57,7 @@
org.postgresql
postgresql
+ 42.7.2
\ No newline at end of file
diff --git a/account/src/main/kotlin/br/all/domain/user/Email.kt b/account/src/main/kotlin/br/all/domain/user/Email.kt
index caf6dce7b..965415718 100644
--- a/account/src/main/kotlin/br/all/domain/user/Email.kt
+++ b/account/src/main/kotlin/br/all/domain/user/Email.kt
@@ -7,51 +7,60 @@ data class Email(val value: String) : ValueObject() {
init {
val notification = validate()
- require(notification.hasNoErrors()) {notification.message()}
+ require(notification.hasNoErrors()) { notification.message() }
}
override fun validate(): Notification {
val notification = Notification()
+ if (value.isEmpty()) {
+ notification.addError("Email must not be empty.")
+ return notification
+ }
+
if (value.isBlank()) {
notification.addError("Email must not be blank.")
return notification
}
+
if (!isValidEmailFormat(value)) notification.addError("Wrong Email format.")
+
return notification
}
private fun isValidEmailFormat(email: String): Boolean {
- if (!isValidEmailAddress(email)) return false
- if (!hasLengthBelowMaximum(email)) return false
- if (hasRepeatedSubdomains(email)) return false
- if (email.contains("..")) return false
- if (email.contains("@.")) return false
- if (email.contains(".@")) return false
- return true
- }
-
- private fun isValidEmailAddress(email: String): Boolean {
- val regex = Regex("^[A-Za-z0-9+_.-]+@[a-z.]+$")
- return regex.matches(email)
- }
+ if (email.contains("..") || email.contains(".@") || email.contains("@.")) return false
+ if (email.startsWith(".") || email.endsWith(".")) return false
+ if (email.startsWith("@") || email.endsWith("@")) return false
- fun hasRepeatedSubdomains(email: String): Boolean {
val parts = email.split("@")
+ if (parts.size != 2) return false
- if (parts.size == 2) {
- val subdomains = parts[1].split(".")
- val verifyedSubdomains = HashSet()
+ val localPart = parts[0]
+ val domainPart = parts[1]
- for (subdomain in subdomains){
- if (!verifyedSubdomains.add(subdomain)) return true
- }
+ if (!hasValidLength(localPart, domainPart)) return false
+ if (!isValidStructure(localPart, domainPart)) return false
+
+ return true
+ }
+
+ private fun isValidStructure(localPart: String, domainPart: String): Boolean {
+ val localRegex = Regex("^[A-Za-z0-9_!#$%&'*+/=?`{|}~^.-]+$")
+ val domainRegex = Regex("^[A-Za-z0-9.-]+$")
+
+ val domainLabels = domainPart.split(".")
+ if (domainLabels.last().length < 2 || domainLabels.any { it.startsWith("-") || it.endsWith("-") }) {
+ return false
}
- return false
+
+ return localRegex.matches(localPart) && domainRegex.matches(domainPart)
}
- fun hasLengthBelowMaximum(email: String): Boolean {
- val parts = email.split("@")
- return !(parts[0].length > 64 || parts[1].length > 255)
+ private fun hasValidLength(localPart: String, domainPart: String): Boolean {
+ if (localPart.length > 64) return false
+ if (domainPart.length > 255) return false
+ if ((localPart.length + 1 + domainPart.length) > 254) return false
+ return true
}
}
\ No newline at end of file
diff --git a/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt b/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt
index c5d6795b9..10f7cca27 100644
--- a/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt
+++ b/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt
@@ -33,7 +33,7 @@ class RegisterUserAccountServiceImplTest {
inner class WhenSuccessfullyRegisteringUser {
@Test
fun `should register a new user`() {
- val request = factory.registerRequest().copy(email = "user@example.com")
+ val request = factory.registerRequest()
every { userAccountRepository.existsByEmail(request.email) } returns false
every { userAccountRepository.existsByUsername(request.username) } returns false
@@ -53,7 +53,7 @@ class RegisterUserAccountServiceImplTest {
inner class WhenFailingToRegisterUser {
@Test
fun `should not register user with existing email`() {
- val request = factory.registerRequest().copy(email = "user@example.com")
+ val request = factory.registerRequest()
every { userAccountRepository.existsByEmail(request.email) } returns true
every { userAccountRepository.existsByUsername(request.username) } returns false
@@ -68,7 +68,7 @@ class RegisterUserAccountServiceImplTest {
@Test
fun `should not register user with existing username`() {
- val request = factory.registerRequest().copy(email = "user@example.com")
+ val request = factory.registerRequest()
every { userAccountRepository.existsByEmail(request.email) } returns false
every { userAccountRepository.existsByUsername(request.username) } returns true
diff --git a/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt b/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt
index fdb27d3ee..25a1e4b8e 100644
--- a/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt
+++ b/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt
@@ -2,11 +2,13 @@ package br.all.domain.user
import io.github.serpro69.kfaker.Faker
import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Tag
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertFailsWith
+@Tag("UnitTest")
class UserAccountTest{
private val faker = Faker()
diff --git a/pom.xml b/pom.xml
index e0f2e0a6b..da1865238 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.2
+ 3.4.4
@@ -29,6 +29,9 @@
UTF-8
official
1.8
+ 1.9.0
+ pet-ads
+ https://sonarcloud.io
@@ -47,18 +50,19 @@
org.jetbrains.kotlin
kotlin-reflect
+ ${kotlin.version}
compile
org.jetbrains.kotlin
kotlin-stdlib
- 1.9.0
+ ${kotlin.version}
compile
org.jetbrains.kotlin
kotlin-test-junit5
- 1.9.0
+ ${kotlin.version}
test
@@ -66,12 +70,6 @@
kotlin-faker
1.15.0
-
- org.junit.jupiter
- junit-jupiter-engine
- 5.8.2
- test
-
com.tngtech.archunit
archunit-junit5
@@ -81,7 +79,7 @@
org.jetbrains.kotlin
kotlin-test-junit
- 1.9.0
+ ${kotlin.version}
test
@@ -109,24 +107,20 @@
org.jetbrains.kotlin
kotlin-maven-plugin
- 1.9.0
+ ${kotlin.version}
-
- org.mapstruct
- mapstruct-processor
- 1.4.2.Final
-
org.jetbrains.kotlin
kotlin-maven-allopen
- 2.0.0-RC1
+ ${kotlin.version}
org.jetbrains.kotlin
kotlin-maven-noarg
- 2.0.0-RC1
+ ${kotlin.version}
+
+ true
spring
jpa
-
- spring
-
diff --git a/review/pom.xml b/review/pom.xml
index 6c9336ec0..920d13193 100644
--- a/review/pom.xml
+++ b/review/pom.xml
@@ -21,7 +21,6 @@
org.springframework.boot
spring-boot-starter-data-mongodb
- 3.1.2
org.springframework
@@ -33,11 +32,11 @@
1.0-SNAPSHOT
compile
+
io.swagger.core.v3
swagger-annotations-jakarta
- 2.2.19
- compile
+ 2.2.31
org.apache.commons
diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt
new file mode 100644
index 000000000..8ba8c5d9b
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt
@@ -0,0 +1,6 @@
+package br.all.application.protocol.find
+
+import br.all.application.shared.presenter.GenericPresenter
+import br.all.application.protocol.find.GetProtocolStageService.ResponseModel
+
+interface GetProtocolStagePresenter : GenericPresenter
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt
new file mode 100644
index 000000000..31ba40773
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt
@@ -0,0 +1,32 @@
+package br.all.application.protocol.find
+
+import io.swagger.v3.oas.annotations.media.Schema
+import java.util.UUID
+
+interface GetProtocolStageService {
+ fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel)
+
+ data class RequestModel(
+ val userId: UUID,
+ val systematicStudyId: UUID
+ )
+
+ @Schema(name = "GetProtocolStageServiceResponseModel", description = "Response model for Get Protocol Stage Service")
+ data class ResponseModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val currentStage: ProtocolStage
+ )
+
+ enum class ProtocolStage {
+ PROTOCOL_PART_I,
+ PICOC,
+ PROTOCOL_PART_II,
+ PROTOCOL_PART_III,
+ IDENTIFICATION,
+ SELECTION,
+ EXTRACTION,
+ GRAPHICS,
+ FINALIZATION
+ }
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt
new file mode 100644
index 000000000..22403c249
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt
@@ -0,0 +1,129 @@
+package br.all.application.protocol.find
+
+import br.all.application.protocol.repository.ProtocolRepository
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.user.CredentialsService
+import br.all.application.protocol.find.GetProtocolStageService.RequestModel
+import br.all.application.protocol.find.GetProtocolStageService.ResponseModel
+import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage
+import br.all.application.protocol.repository.ProtocolDto
+import br.all.application.question.repository.QuestionRepository
+import br.all.application.review.repository.fromDto
+import br.all.application.shared.exceptions.EntityNotFoundException
+import br.all.application.shared.presenter.prepareIfFailsPreconditions
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.domain.model.question.QuestionContextEnum
+import br.all.domain.model.review.SystematicStudy
+import org.springframework.stereotype.Service
+
+@Service
+class GetProtocolStageServiceImpl(
+ private val protocolRepository: ProtocolRepository,
+ private val systematicStudyRepository: SystematicStudyRepository,
+ private val studyReviewRepository: StudyReviewRepository,
+ private val credentialsService: CredentialsService,
+ private val questionRepository: QuestionRepository
+) : GetProtocolStageService {
+ override fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel) {
+ val user = credentialsService.loadCredentials(request.userId)?.toUser()
+ val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId)
+ val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) }
+
+ presenter.prepareIfFailsPreconditions(user, systematicStudy)
+ if (presenter.isDone()) return
+
+ val protocolDto = protocolRepository.findById(request.systematicStudyId)
+ if (protocolDto == null) {
+ val message = "Protocol not found for systematic study ${request.systematicStudyId}"
+ presenter.prepareFailView(EntityNotFoundException(message))
+ return
+ }
+
+ val allStudies = studyReviewRepository.findAllFromReview(request.systematicStudyId)
+ val robQuestions = questionRepository.findAllBySystematicStudyId(systematicStudyDto!!.id, QuestionContextEnum.ROB).size
+ val extractionQuestions = questionRepository.findAllBySystematicStudyId(systematicStudyDto.id, QuestionContextEnum.EXTRACTION).size
+
+ val totalStudiesCount = allStudies.size
+ val includedStudiesCount = allStudies.count { it.selectionStatus == "INCLUDED" }
+ val extractedStudiesCount = allStudies.count { it.extractionStatus == "INCLUDED" }
+
+ val stage = evaluateStage(
+ protocolDto,
+ totalStudiesCount,
+ includedStudiesCount,
+ extractedStudiesCount,
+ robQuestions,
+ extractionQuestions,
+ )
+
+ presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage))
+ }
+
+ private fun evaluateStage(
+ dto: ProtocolDto,
+ totalStudiesCount: Int,
+ includedStudiesCount: Int,
+ extractedStudiesCount: Int,
+ robQuestionCount: Int,
+ extractionQuestionsCount: Int
+ ): ProtocolStage {
+ return when {
+ isProtocolPartI(dto) -> ProtocolStage.PROTOCOL_PART_I
+ picocStage(dto) -> ProtocolStage.PICOC
+ isProtocolPartII(dto) -> ProtocolStage.PROTOCOL_PART_II
+ !isProtocolPartIIICompleted(dto, robQuestionCount, extractionQuestionsCount) -> ProtocolStage.PROTOCOL_PART_III
+ totalStudiesCount == 0 -> ProtocolStage.IDENTIFICATION
+ includedStudiesCount == 0 -> ProtocolStage.SELECTION
+ extractedStudiesCount == 0 -> ProtocolStage.EXTRACTION
+ else -> ProtocolStage.GRAPHICS
+ }
+ }
+
+ private fun isProtocolPartI(dto: ProtocolDto): Boolean {
+ return dto.goal.isNullOrBlank() && dto.justification.isNullOrBlank()
+ }
+
+ private fun isProtocolPartII(dto: ProtocolDto): Boolean {
+ return dto.studiesLanguages.isEmpty() &&
+ dto.eligibilityCriteria.isEmpty() &&
+ dto.informationSources.isEmpty() &&
+ dto.keywords.isEmpty() &&
+ dto.sourcesSelectionCriteria.isNullOrBlank() &&
+ dto.searchMethod.isNullOrBlank() &&
+ dto.selectionProcess.isNullOrBlank()
+ }
+
+ private fun isProtocolPartIIICompleted(dto: ProtocolDto, robQuestionCount: Int, extractionQuestionsCount: Int): Boolean {
+ val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", ignoreCase = true) }
+ val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", ignoreCase = true) }
+
+ val hasExtractionAndRob = robQuestionCount > 0 && extractionQuestionsCount > 0
+
+ val hasDatabases = dto.informationSources.isNotEmpty()
+ val hasResearchQuestions = dto.researchQuestions.isNotEmpty()
+ val hasAnalysisProcess = !dto.analysisAndSynthesisProcess.isNullOrBlank()
+
+ return hasInclusionCriteria && hasExclusionCriteria &&
+ hasExtractionAndRob && hasDatabases &&
+ hasResearchQuestions && hasAnalysisProcess
+ }
+
+ private fun picocStage(dto: ProtocolDto): Boolean {
+ val picoc = dto.picoc
+ if (picoc == null) return false
+
+ val picocIsStarted = !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() ||
+ !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank()
+
+ if (picocIsStarted) {
+ val picocIsCompleted = !picoc.population.isNullOrBlank() && !picoc.intervention.isNullOrBlank() &&
+ !picoc.control.isNullOrBlank() && !picoc.outcome.isNullOrBlank() && !picoc.context.isNullOrBlank()
+
+ if (!picocIsCompleted) {
+ return true
+ }
+ }
+
+ return false
+ }
+}
diff --git a/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt b/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt
new file mode 100644
index 000000000..90e2ad4cf
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt
@@ -0,0 +1,6 @@
+package br.all.application.report.find.presenter
+
+import br.all.application.shared.presenter.GenericPresenter
+import br.all.application.report.find.service.FindStudyReviewCriteriaService
+
+interface FindStudyReviewCriteriaPresenter: GenericPresenter
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt
new file mode 100644
index 000000000..e97df28f4
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt
@@ -0,0 +1,22 @@
+package br.all.application.report.find.service
+
+import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter
+import java.util.*
+
+interface FindStudyReviewCriteriaService {
+ fun findCriteria(presenter: FindStudyReviewCriteriaPresenter, request: RequestModel)
+
+ data class RequestModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val studyReviewId: Long
+ )
+
+ data class ResponseModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val studyReviewId: Long,
+ val inclusionCriteria: Set,
+ val exclusionCriteria: Set
+ )
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt
new file mode 100644
index 000000000..f1b8bc5ac
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt
@@ -0,0 +1,60 @@
+package br.all.application.report.find.service
+
+import br.all.application.protocol.repository.CriterionDto
+import br.all.application.protocol.repository.ProtocolRepository
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.review.repository.fromDto
+import br.all.application.shared.presenter.prepareIfFailsPreconditions
+import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter
+import br.all.application.study.repository.StudyReviewDto
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.application.user.CredentialsService
+import br.all.domain.model.review.SystematicStudy
+
+class FindStudyReviewCriteriaServiceImpl(
+ private val systematicStudyRepository: SystematicStudyRepository,
+ private val studyReviewRepository: StudyReviewRepository,
+ private val credentialsService: CredentialsService,
+ private val protocolRepository: ProtocolRepository,
+): FindStudyReviewCriteriaService {
+ override fun findCriteria(
+ presenter: FindStudyReviewCriteriaPresenter,
+ request: FindStudyReviewCriteriaService.RequestModel
+ ) {
+ val user = credentialsService.loadCredentials(request.userId)?.toUser()
+
+ val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId)
+ val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) }
+
+ presenter.prepareIfFailsPreconditions(user, systematicStudy)
+
+ if(presenter.isDone()) return
+
+ val criteriaSet = protocolRepository.findById(request.systematicStudyId)?.eligibilityCriteria?: emptyList()
+
+ val studyReview = studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId)!!
+
+ val inclusionCriteria = getCriteriaByType(criteriaSet, studyReview, "INCLUSION")
+ val exclusionCriteria = getCriteriaByType(criteriaSet, studyReview, "EXCLUSION")
+
+ val response = FindStudyReviewCriteriaService.ResponseModel(
+ userId = request.userId,
+ systematicStudyId = request.systematicStudyId,
+ studyReviewId = request.studyReviewId,
+ inclusionCriteria = inclusionCriteria,
+ exclusionCriteria = exclusionCriteria,
+ )
+
+ presenter.prepareSuccessView(response)
+ }
+
+ private fun getCriteriaByType(
+ criteriaSet: Collection,
+ studyReview: StudyReviewDto,
+ type: String
+ ): Set {
+ return criteriaSet
+ .filter { it.type == type && it.description in studyReview.criteria }
+ .map { it.description }.toSet()
+ }
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt
new file mode 100644
index 000000000..724075ecd
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt
@@ -0,0 +1,141 @@
+package br.all.application.study.update.implementation
+
+import br.all.application.question.repository.QuestionRepository
+import br.all.application.question.repository.fromDto
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.review.repository.fromDto
+import br.all.application.shared.exceptions.EntityNotFoundException
+import br.all.application.shared.presenter.prepareIfFailsPreconditions
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.application.study.repository.fromDto
+import br.all.application.study.repository.toDto
+import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.RequestModel
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.RequestModel.AnswerDetail
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.FailedAnswer
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.LabelDto
+import br.all.application.user.CredentialsService
+import br.all.domain.model.question.Label
+import br.all.domain.model.question.LabeledScale
+import br.all.domain.model.question.NumberScale
+import br.all.domain.model.question.PickList
+import br.all.domain.model.question.Question
+import br.all.domain.model.question.QuestionContextEnum
+import br.all.domain.model.question.Textual
+import br.all.domain.model.review.SystematicStudy
+import br.all.domain.model.study.Answer
+import br.all.domain.model.study.StudyReview
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+import java.util.UUID
+
+@Service
+class BatchAnswerQuestionServiceImpl(
+ private val studyReviewRepository: StudyReviewRepository,
+ private val questionRepository: QuestionRepository,
+ private val systematicStudyRepository: SystematicStudyRepository,
+ private val credentialsService: CredentialsService,
+): BatchAnswerQuestionService {
+
+ @Transactional
+ override fun batchAnswerQuestion(
+ presenter: BatchAnswerQuestionPresenter,
+ request: RequestModel,
+ context: String
+ ) {
+ val user = credentialsService.loadCredentials(request.userId)?.toUser()
+
+ val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId)
+ val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) }
+
+ presenter.prepareIfFailsPreconditions(user, systematicStudy)
+ if (presenter.isDone()) return
+
+ val reviewDto = studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId)
+ if (reviewDto == null) {
+ val message = "Review with id ${request.studyReviewId} in systematic study ${request.systematicStudyId} does not exist!"
+ presenter.prepareFailView(EntityNotFoundException(message))
+ return
+ }
+ val review = StudyReview.fromDto(reviewDto)
+
+ val questionContext = QuestionContextEnum.valueOf(context.uppercase())
+ val successfulQuestionIds = mutableListOf()
+ val failedAnswers = mutableListOf()
+
+ for (answerDetail in request.answers) {
+ try {
+ val questionDto = questionRepository.findById(request.systematicStudyId, answerDetail.questionId)
+
+ if (questionDto == null) throw EntityNotFoundException("Question with id ${answerDetail.questionId} in systematic study ${request.systematicStudyId} was not found!")
+ if (questionDto.context != questionContext) throw IllegalArgumentException("Should answer question with the context: $context, found: ${questionDto.context}")
+
+ val question = Question.fromDto(questionDto)
+ val answer = convertAnswer(question, answerDetail, questionDto.questionType)
+
+ if (questionDto.context == QuestionContextEnum.ROB) {
+ review.answerQualityQuestionOf(answer)
+ } else {
+ review.answerFormQuestionOf(answer)
+ }
+
+ successfulQuestionIds.add(answerDetail.questionId)
+ } catch (e: Exception) {
+ failedAnswers.add(
+ FailedAnswer(
+ questionId = answerDetail.questionId,
+ reason = e.message ?: "An unknown error occurred!"
+ )
+ )
+ }
+ }
+
+ studyReviewRepository.saveOrUpdate(review.toDto())
+
+ presenter.prepareSuccessView(
+ ResponseModel(
+ userId = request.userId,
+ systematicStudyId = request.systematicStudyId,
+ studyReviewId = request.studyReviewId,
+ succeededAnswers = successfulQuestionIds,
+ failedAnswers = failedAnswers,
+ totalAnswered = successfulQuestionIds.size
+ )
+ )
+ }
+
+ private fun convertAnswer(
+ question: Question<*>,
+ detail: AnswerDetail,
+ questionType: String
+ ): Answer<*> {
+ if (detail.type != questionType) {
+ throw IllegalArgumentException("Type mismatch: Request payload type is '${detail.type}', but question ${question.id} is of type '${questionType}'")
+ }
+ return when {
+ questionType == "TEXTUAL" && detail.answer is String -> (question as Textual).answer(detail.answer)
+ questionType == "PICK_LIST" && detail.answer is String -> (question as PickList).answer(detail.answer)
+ questionType == "NUMBERED_SCALE" && detail.answer is Int -> (question as NumberScale).answer(detail.answer)
+ questionType == "LABELED_SCALE" -> {
+ when (val answer = detail.answer) {
+ is LinkedHashMap<*, *> -> {
+ (answer["name"] as? String)?.let { name ->
+ (answer["value"] as? Int)?.let { value ->
+ (question as LabeledScale).answer(Label(name, value))
+ }
+ } ?: throw IllegalArgumentException("Invalid labeled scale answer: missing 'name' or 'value'")
+ }
+ is LabelDto -> {
+ (question as LabeledScale).answer(Label(answer.name, answer.value))
+ }
+ else -> {
+ throw IllegalArgumentException("Unsupported answer type for 'LABELED_SCALE'")
+ }
+ }
+ }
+ else -> throw IllegalArgumentException("Answer type of '${detail.answer.javaClass}' is not compatible with question type '${questionType}'")
+ }
+ }
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt
new file mode 100644
index 000000000..986bf2327
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt
@@ -0,0 +1,70 @@
+package br.all.application.study.update.implementation
+
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.review.repository.fromDto
+import br.all.application.shared.exceptions.EntityNotFoundException
+import br.all.application.shared.presenter.prepareIfFailsPreconditions
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.application.study.repository.fromDto
+import br.all.application.study.repository.toDto
+import br.all.application.study.update.interfaces.RemoveCriteriaPresenter
+import br.all.application.study.update.interfaces.RemoveCriteriaService
+import br.all.application.user.CredentialsService
+import br.all.domain.model.protocol.Criterion
+import br.all.domain.model.review.SystematicStudy
+import br.all.domain.model.study.StudyReview
+
+class RemoveCriteriaServiceImpl(
+ private val studyReviewRepository: StudyReviewRepository,
+ private val systematicStudyRepository: SystematicStudyRepository,
+ private val credentialsService: CredentialsService,
+): RemoveCriteriaService {
+ override fun removeCriteria(presenter: RemoveCriteriaPresenter, request: RemoveCriteriaService.RequestModel) {
+ val user = credentialsService.loadCredentials(request.userId)?.toUser()
+
+ val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId)
+ val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) }
+
+ presenter.prepareIfFailsPreconditions(user, systematicStudy)
+ if (presenter.isDone()) return
+
+ val studyReviewDto = studyReviewRepository.findById(request.systematicStudyId, request.studyId)
+ if (studyReviewDto == null) {
+ presenter.prepareFailView(
+ EntityNotFoundException("Study review of id ${request.systematicStudyId} not found.")
+ )
+ return
+ }
+
+ val studyReview = StudyReview.fromDto(studyReviewDto)
+
+ val inclusionCriteria: List = studyReview.criteria
+ .filter { it.type == Criterion.CriterionType.INCLUSION }
+ .map { it.description }
+
+ val exclusionCriteria: List = studyReview.criteria
+ .filter { it.type == Criterion.CriterionType.EXCLUSION }
+ .map { it.description }
+
+ request.criteria
+ .filter { it.isNotBlank() }
+ .forEach { criterionString ->
+ studyReview.criteria
+ .filter { it.description == criterionString }
+ .forEach { matching ->
+ studyReview.removeCriterion(matching)
+ }
+ }
+
+ studyReviewRepository.saveOrUpdate(studyReview.toDto())
+
+ presenter.prepareSuccessView(
+ RemoveCriteriaService.ResponseModel(
+ request.systematicStudyId,
+ request.studyId,
+ inclusionCriteria,
+ exclusionCriteria,
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt
new file mode 100644
index 000000000..627d35bb8
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt
@@ -0,0 +1,6 @@
+package br.all.application.study.update.interfaces
+
+import br.all.application.shared.presenter.GenericPresenter
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel
+
+interface BatchAnswerQuestionPresenter: GenericPresenter
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt
new file mode 100644
index 000000000..36c6299d1
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt
@@ -0,0 +1,41 @@
+package br.all.application.study.update.interfaces
+
+import io.swagger.v3.oas.annotations.media.Schema
+import java.util.UUID
+
+interface BatchAnswerQuestionService {
+ fun batchAnswerQuestion(presenter: BatchAnswerQuestionPresenter, request: RequestModel, context: String)
+
+ data class RequestModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val studyReviewId: Long,
+ val answers: List
+ ) {
+ data class AnswerDetail(
+ val questionId: UUID,
+ val type: String,
+ val answer: Any
+ )
+ }
+
+ data class FailedAnswer(
+ val questionId: UUID,
+ val reason: String
+ )
+
+ data class LabelDto(
+ val name: String,
+ val value: Int
+ )
+
+ @Schema(name = "BatchAnswerQuestionServiceResponseModel")
+ data class ResponseModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val studyReviewId: Long,
+ val succeededAnswers: List,
+ val failedAnswers: List,
+ val totalAnswered: Int
+ )
+}
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt
new file mode 100644
index 000000000..ab24adad1
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt
@@ -0,0 +1,6 @@
+package br.all.application.study.update.interfaces
+
+import br.all.application.shared.presenter.GenericPresenter
+
+
+interface RemoveCriteriaPresenter: GenericPresenter
\ No newline at end of file
diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt
new file mode 100644
index 000000000..1f4923842
--- /dev/null
+++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt
@@ -0,0 +1,22 @@
+package br.all.application.study.update.interfaces
+
+import java.util.*
+
+
+interface RemoveCriteriaService {
+ fun removeCriteria(presenter: RemoveCriteriaPresenter, request: RequestModel)
+
+ data class RequestModel(
+ val userId: UUID,
+ val systematicStudyId: UUID,
+ val studyId: Long,
+ val criteria: List
+ )
+
+ data class ResponseModel(
+ val systematicStudyId: UUID,
+ val studyId: Long,
+ val inclusionCriteria: List,
+ val exclusionCriteria: List,
+ )
+}
diff --git a/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt b/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt
index fa3f91af0..aa332b362 100644
--- a/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt
+++ b/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt
@@ -7,7 +7,7 @@ data class Email(val email: String) : ValueObject() {
init {
val notification = validate()
- require(notification.hasNoErrors()) {notification.message()}
+ require(notification.hasNoErrors()) { notification.message() }
}
override fun validate(): Notification {
@@ -17,45 +17,50 @@ data class Email(val email: String) : ValueObject() {
notification.addError("Email must not be empty.")
return notification
}
+
if (email.isBlank()) {
notification.addError("Email must not be blank.")
return notification
}
+
if (!isValidEmailFormat(email)) notification.addError("Wrong Email format.")
+
return notification
}
private fun isValidEmailFormat(email: String): Boolean {
- if (!isValidEmailAddress(email)) return false
- if (!HasLengthBelowMaximum(email)) return false
- if (hasRepeatedSubdomains(email)) return false
- if (email.contains("..")) return false
- if (email.contains("@.")) return false
- if (email.contains(".@")) return false
- return true
- }
-
- private fun isValidEmailAddress(email: String): Boolean {
- val regex = Regex("^[A-Za-z0-9+_.-]+@[a-z.]+$")
- return regex.matches(email)
- }
+ if (email.contains("..") || email.contains(".@") || email.contains("@.")) return false
+ if (email.startsWith(".") || email.endsWith(".")) return false
+ if (email.startsWith("@") || email.endsWith("@")) return false
- fun hasRepeatedSubdomains(email: String): Boolean {
val parts = email.split("@")
+ if (parts.size != 2) return false
+
+ val localPart = parts[0]
+ val domainPart = parts[1]
- if (parts.size == 2) {
- val subdomains = parts[1].split(".")
- val verifyedSubdomains = HashSet()
+ if (!hasValidLength(localPart, domainPart)) return false
+ if (!isValidStructure(localPart, domainPart)) return false
- for (subdomain in subdomains){
- if (!verifyedSubdomains.add(subdomain)) return true
- }
+ return true
+ }
+
+ private fun isValidStructure(localPart: String, domainPart: String): Boolean {
+ val localRegex = Regex("^[A-Za-z0-9_!#$%&'*+/=?`{|}~^.-]+$")
+ val domainRegex = Regex("^[A-Za-z0-9.-]+$")
+
+ val domainLabels = domainPart.split(".")
+ if (domainLabels.last().length < 2 || domainLabels.any { it.startsWith("-") || it.endsWith("-") }) {
+ return false
}
- return false
+
+ return localRegex.matches(localPart) && domainRegex.matches(domainPart)
}
- fun HasLengthBelowMaximum(email: String): Boolean {
- val parts = email.split("@")
- return !(parts[0].length > 64 || parts[1].length > 255)
+ private fun hasValidLength(localPart: String, domainPart: String): Boolean {
+ if (localPart.length > 64) return false
+ if (domainPart.length > 255) return false
+ if ((localPart.length + 1 + domainPart.length) > 254) return false
+ return true
}
}
\ No newline at end of file
diff --git a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt
new file mode 100644
index 000000000..37c359294
--- /dev/null
+++ b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt
@@ -0,0 +1,322 @@
+package br.all.application.protocol.find
+
+import br.all.application.protocol.repository.ProtocolRepository
+import br.all.application.protocol.util.TestDataFactory as ProtocolFactory
+import br.all.application.study.util.TestDataFactory as StudyReviewFactory
+import br.all.application.question.util.TestDataFactory as QuestionFactory
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.application.user.CredentialsService
+import br.all.application.util.PreconditionCheckerMockingNew
+import io.mockk.every
+import io.mockk.impl.annotations.InjectMockKs
+import io.mockk.impl.annotations.MockK
+import io.mockk.junit5.MockKExtension
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.extension.ExtendWith
+import java.util.UUID
+import kotlin.test.Test
+import br.all.application.protocol.find.GetProtocolStageService.RequestModel
+import br.all.application.protocol.find.GetProtocolStageService.ResponseModel
+import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage
+import br.all.application.protocol.repository.CriterionDto
+import br.all.application.protocol.repository.PicocDto
+import br.all.application.question.repository.QuestionRepository
+import br.all.domain.model.question.QuestionContextEnum
+import io.mockk.verify
+
+@Tag("UnitTest")
+@Tag("ServiceTest")
+@ExtendWith(MockKExtension::class)
+class GetProtocolStageServiceImplTest {
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var protocolRepository: ProtocolRepository
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var systematicStudyRepository: SystematicStudyRepository
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var studyReviewRepository: StudyReviewRepository
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var questionRepository: QuestionRepository
+
+ @MockK
+ private lateinit var credentialsService: CredentialsService
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var presenter: GetProtocolStagePresenter
+
+ @InjectMockKs
+ private lateinit var sut: GetProtocolStageServiceImpl
+
+ private lateinit var precondition: PreconditionCheckerMockingNew
+ private lateinit var protocolFactory: ProtocolFactory
+ private lateinit var studyReviewFactory: StudyReviewFactory
+ private lateinit var questionFactory: QuestionFactory
+
+ private lateinit var researcherId: UUID
+ private lateinit var systematicStudyId: UUID
+
+ @BeforeEach
+ fun setup() {
+ protocolFactory = ProtocolFactory()
+ studyReviewFactory = StudyReviewFactory()
+ questionFactory = QuestionFactory()
+
+ researcherId = protocolFactory.researcher
+ systematicStudyId = protocolFactory.systematicStudy
+
+ precondition = PreconditionCheckerMockingNew(
+ presenter,
+ credentialsService,
+ systematicStudyRepository,
+ researcherId,
+ systematicStudyId
+ )
+
+ precondition.makeEverythingWork()
+ }
+
+ @Nested
+ @DisplayName("When successfully getting protocol's current stage")
+ inner class SuccessfullyGettingProtocolStage {
+ @Test
+ fun `should return PROTOCOL_PART_I stage when goal and justification are empty`() {
+ val protocolDto = protocolFactory.protocolDto(
+ systematicStudy = systematicStudyId,
+ goal = null,
+ justification = null
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList()
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_I)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return PICOC stage when it is not filled`() {
+ val protocolDto = protocolFactory.protocolDto(
+ systematicStudy = systematicStudyId,
+ goal = "A goal",
+ justification = "A justification",
+ picoc = PicocDto(
+ population = "P",
+ intervention = null,
+ control = null,
+ outcome = null,
+ context = null
+ )
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList()
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PICOC)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return PROTOCOL_PART_II stage when its fields are empty`() {
+ val protocolDto = protocolFactory.protocolDto(
+ systematicStudy = systematicStudyId,
+ goal = "A goal",
+ justification = "A justification",
+ picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"),
+ studiesLanguages = emptySet(),
+ eligibilityCriteria = emptySet(),
+ informationSources = emptySet(),
+ keywords = emptySet()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList()
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_II)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return PROTOCOL_PART_III stage when its conditions are not met`() {
+ val protocolDto = protocolFactory.protocolDto(
+ systematicStudy = systematicStudyId,
+ goal = "A goal",
+ justification = "A justification",
+ picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"),
+ keywords = setOf("test"),
+ extractionQuestions = emptySet()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList()
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_III)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return IDENTIFICATION stage when no studies have been submitted`() {
+ val protocolDto = createFullProtocolDto()
+ val questions = listOf(
+ questionFactory.generateTextualDto()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList()
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.IDENTIFICATION)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return SELECTION stage when studies exist but none are included`() {
+ val protocolDto = createFullProtocolDto()
+ val studies = listOf(
+ createFullStudyReviewDto(selectionStatus = "", extractionStatus = "")
+ )
+ val questions = listOf(
+ questionFactory.generateTextualDto()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.SELECTION)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return EXTRACTION stage when studies are included but none are extracted`() {
+ val protocolDto = createFullProtocolDto()
+ val studies = listOf(
+ createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "")
+ )
+ val questions = listOf(
+ questionFactory.generateTextualDto()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.EXTRACTION)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+
+ @Test
+ fun `should return GRAPHICS stage when studies have been extracted`() {
+ val protocolDto = createFullProtocolDto()
+ val studies = listOf(
+ createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "INCLUDED")
+ )
+ val questions = listOf(
+ questionFactory.generateTextualDto()
+ )
+
+ every { protocolRepository.findById(systematicStudyId) } returns protocolDto
+ every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions
+ every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions
+
+ val request = RequestModel(researcherId, systematicStudyId)
+
+ sut.getStage(presenter, request)
+
+ val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.GRAPHICS)
+ verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) }
+ }
+ }
+
+
+ private fun createFullProtocolDto() = protocolFactory.protocolDto(
+ systematicStudy = systematicStudyId,
+ goal = "A goal",
+ justification = "A justification",
+ picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"),
+ studiesLanguages = setOf("English"),
+ eligibilityCriteria = setOf(
+ CriterionDto("test description", "INCLUSION"),
+ CriterionDto("test description", "EXCLUSION")
+ ),
+ informationSources = setOf("IEEE"),
+ keywords = setOf("test"),
+ sourcesSelectionCriteria = "criteria",
+ searchMethod = "method",
+ selectionProcess = "process",
+ extractionQuestions = setOf(UUID.randomUUID()),
+ robQuestions = setOf(UUID.randomUUID()),
+ researchQuestions = setOf("RQ1?"),
+ analysisAndSynthesisProcess = "process"
+ )
+
+ private fun createFullStudyReviewDto(selectionStatus: String, extractionStatus: String) = studyReviewFactory.generateDto(
+ studyReviewId = 1L,
+ systematicStudyId = systematicStudyId,
+ searchSessionId = UUID.randomUUID(),
+ type = "type",
+ title = "title",
+ year = 2025,
+ authors = "authors",
+ venue = "venue",
+ abstract = "abstract",
+ keywords = emptySet(),
+ references = emptyList(),
+ doi = "doi",
+ sources = emptySet(),
+ criteria = emptySet(),
+ formAnswers = emptyMap(),
+ robAnswers = emptyMap(),
+ comments = "comments",
+ readingPriority = "HIGH",
+ extractionStatus = extractionStatus,
+ selectionStatus = selectionStatus,
+ score = 10
+ )
+}
diff --git a/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt
index cc4a200c3..42a8eec8a 100644
--- a/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt
+++ b/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt
@@ -1,8 +1,7 @@
+package br.all.application.search.create
import br.all.application.protocol.repository.ProtocolRepository
import br.all.application.review.repository.SystematicStudyRepository
-import br.all.application.search.create.CreateSearchSessionServiceImpl
-import br.all.application.search.create.CreateSearchSessionPresenter
import br.all.application.search.util.TestDataFactory
import br.all.application.search.repository.SearchSessionRepository
import br.all.application.shared.exceptions.EntityNotFoundException
@@ -23,9 +22,12 @@ import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.Test
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class CreateSearchSessionServiceImplTest {
diff --git a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt
index 43fc2453b..3ed9029cf 100644
--- a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt
+++ b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt
@@ -17,9 +17,12 @@ import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class FindAllSearchSessionsBySourceServiceImplTest {
diff --git a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt
index 6efa7829d..2ebb67cba 100644
--- a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt
+++ b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt
@@ -16,9 +16,12 @@ import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class FindAllSearchSessionsServiceImplTest {
diff --git a/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt
index 19ce02703..fae5ee1dc 100644
--- a/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt
+++ b/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt
@@ -17,9 +17,12 @@ import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class FindSearchSessionServiceImplTest {
@MockK(relaxUnitFun = true)
diff --git a/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt
index 43a701154..217307515 100644
--- a/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt
+++ b/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt
@@ -16,9 +16,12 @@ import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class UpdateSearchSessionServiceImplTest {
diff --git a/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt b/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt
index 9bb78ed4f..c69609d0a 100644
--- a/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt
+++ b/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt
@@ -11,9 +11,11 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import java.util.*
+@Tag("UnitTest")
class PreconditionCheckerTest {
private lateinit var presenter: GenericPresenter<*>
diff --git a/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt
new file mode 100644
index 000000000..829b8f8b1
--- /dev/null
+++ b/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt
@@ -0,0 +1,240 @@
+package br.all.application.study.update
+
+import br.all.application.question.repository.QuestionRepository
+import br.all.application.review.repository.SystematicStudyRepository
+import br.all.application.study.repository.StudyReviewRepository
+import br.all.application.study.update.implementation.BatchAnswerQuestionServiceImpl
+import br.all.application.study.update.interfaces.AnswerQuestionService
+import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter
+import br.all.application.study.util.TestDataFactory
+import br.all.application.user.CredentialsService
+import br.all.application.util.PreconditionCheckerMockingNew
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.junit5.MockKExtension
+import io.mockk.verify
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.extension.ExtendWith
+import java.util.UUID
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+@Tag("UnitTest")
+@Tag("ServiceTest")
+@ExtendWith(MockKExtension::class)
+class BatchAnswerQuestionServiceImplTest {
+
+ @MockK(relaxed = true)
+ private lateinit var studyReviewRepository: StudyReviewRepository
+
+ @MockK
+ private lateinit var systematicStudyRepository: SystematicStudyRepository
+
+ @MockK
+ private lateinit var questionRepository: QuestionRepository
+
+ @MockK
+ private lateinit var credentialsService: CredentialsService
+
+ @MockK(relaxed = true)
+ private lateinit var presenter: BatchAnswerQuestionPresenter
+
+ private lateinit var sut: BatchAnswerQuestionServiceImpl
+ private lateinit var factory: TestDataFactory
+ private lateinit var preconditionCheckerMocking: PreconditionCheckerMockingNew
+ private lateinit var questionId: UUID
+
+ @BeforeEach
+ fun setUp() {
+ factory = TestDataFactory()
+ preconditionCheckerMocking = PreconditionCheckerMockingNew(
+ presenter,
+ credentialsService,
+ systematicStudyRepository,
+ factory.researcherId,
+ factory.systematicStudyId
+ )
+ sut = BatchAnswerQuestionServiceImpl(
+ studyReviewRepository,
+ questionRepository,
+ systematicStudyRepository,
+ credentialsService
+ )
+ questionId = UUID.randomUUID()
+ }
+
+ @Nested
+ @DisplayName("When successfully answering questions")
+ inner class WhenSuccessfullyAnsweringQuestions {
+
+ @Test
+ fun `should successfully answer multiple questions of different types`() {
+ val reviewDto = factory.generateDto()
+ val context = "EXTRACTION"
+
+ val textualQId = UUID.randomUUID()
+ val textualQDto = factory.generateQuestionTextualDto(textualQId, factory.systematicStudyId, questionContext = context)
+ val textualAnswer = factory.answerDetail(questionId = textualQId, type = "TEXTUAL", answer = "Valid textual answer")
+
+ val numberedQId = UUID.randomUUID()
+ val numberedQDto = factory.generateQuestionNumberedScaleDto(numberedQId, factory.systematicStudyId, questionContext = context, higher = 5, lower = 1)
+ val numberedAnswer = factory.answerDetail(questionId = numberedQId, type = "NUMBERED_SCALE", answer = 5)
+
+ val pickListQId = UUID.randomUUID()
+ val pickListQDto = factory.generateQuestionPickListDto(pickListQId, factory.systematicStudyId, options = listOf("A", "B"), questionContext = context)
+ val pickListAnswer = factory.answerDetail(questionId = pickListQId, type = "PICK_LIST", answer = "A")
+
+ val request = factory.batchAnswerRequest(listOf(textualAnswer, numberedAnswer, pickListAnswer))
+
+ preconditionCheckerMocking.makeEverythingWork()
+
+ every { studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId) } returns reviewDto
+ every { questionRepository.findById(request.systematicStudyId, textualQId) } returns textualQDto
+ every { questionRepository.findById(request.systematicStudyId, numberedQId) } returns numberedQDto
+ every { questionRepository.findById(request.systematicStudyId, pickListQId) } returns pickListQDto
+
+ sut.batchAnswerQuestion(presenter, request, context)
+
+ verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) }
+ verify(exactly = 1) {
+ presenter.prepareSuccessView(withArg { response ->
+ assertEquals(3, response.succeededAnswers.size)
+ assertTrue(response.failedAnswers.isEmpty())
+ assertEquals(3, response.totalAnswered)
+ assertTrue(response.succeededAnswers.containsAll(listOf(textualQId, numberedQId, pickListQId)))
+ })
+ }
+ }
+
+ @Test
+ fun `should handle a mix of successful and failed answers`() {
+ val reviewDto = factory.generateDto()
+ val context = "ROB"
+
+ val successQId = UUID.randomUUID()
+ val successQDto = factory.generateQuestionTextualDto(successQId, factory.systematicStudyId, questionContext = context)
+ val successAnswer = factory.answerDetail(questionId = successQId, type = "TEXTUAL", answer = "This will work")
+
+ val notFoundQId = UUID.randomUUID()
+ val notFoundAnswer = factory.answerDetail(questionId = notFoundQId, type = "TEXTUAL", answer = "This will fail")
+
+ val wrongContextQId = UUID.randomUUID()
+ val wrongContextQDto = factory.generateQuestionTextualDto(wrongContextQId, factory.systematicStudyId, questionContext = "EXTRACTION")
+ val wrongContextAnswer = factory.answerDetail(questionId = wrongContextQId, type = "TEXTUAL", answer = "Wrong context")
+
+ val request = factory.batchAnswerRequest(listOf(successAnswer, notFoundAnswer, wrongContextAnswer))
+
+ preconditionCheckerMocking.makeEverythingWork()
+
+ every { studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId) } returns reviewDto
+ every { questionRepository.findById(request.systematicStudyId, successQId) } returns successQDto
+ every { questionRepository.findById(request.systematicStudyId, notFoundQId) } returns null
+ every { questionRepository.findById(request.systematicStudyId, wrongContextQId) } returns wrongContextQDto
+
+ sut.batchAnswerQuestion(presenter, request, context)
+
+ verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) }
+ verify(exactly = 1) {
+ presenter.prepareSuccessView(withArg { response ->
+ assertEquals(1, response.succeededAnswers.size)
+ assertEquals(2, response.failedAnswers.size)
+ assertEquals(1, response.totalAnswered)
+ assertTrue(response.succeededAnswers.contains(successQId))
+ assertTrue(response.failedAnswers.any { it.questionId == notFoundQId })
+ assertTrue(response.failedAnswers.any { it.questionId == wrongContextQId })
+ })
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("When failing to answer questions")
+ inner class WhenFailingToAnswerQuestions {
+ @Test
+ fun `should create a failed answer entry for a question with conflicting types`() {
+ val reviewDto = factory.generateDto()
+ val context = "ROB"
+ val questionId = UUID.randomUUID()
+ val questionDto = factory.generateQuestionTextualDto(questionId, factory.systematicStudyId, questionContext = context)
+
+ val answerDetail = factory.answerDetail(questionId, "PICK_LIST", "Some answer")
+ val request = factory.batchAnswerRequest(listOf(answerDetail))
+
+ preconditionCheckerMocking.makeEverythingWork()
+ every { studyReviewRepository.findById(any(), any()) } returns reviewDto
+ every { questionRepository.findById(any(), questionId) } returns questionDto
+
+ sut.batchAnswerQuestion(presenter, request, context)
+
+ verify(exactly = 1) {
+ presenter.prepareSuccessView(withArg { response ->
+ assertTrue(response.succeededAnswers.isEmpty())
+ assertEquals(1, response.failedAnswers.size)
+ assertEquals(questionId, response.failedAnswers.first().questionId)
+ assertTrue("Type mismatch" in response.failedAnswers.first().reason)
+ })
+ }
+ }
+
+ @Test
+ fun `should create a failed answer entry when answer value type is incompatible`() {
+ val reviewDto = factory.generateDto()
+ val context = "EXTRACTION"
+ val questionId = UUID.randomUUID()
+ val questionDto = factory.generateQuestionTextualDto(questionId, factory.systematicStudyId, questionContext = context)
+
+ val answerDetail = factory.answerDetail(questionId, "TEXTUAL", 12345)
+ val request = factory.batchAnswerRequest(listOf(answerDetail))
+
+ preconditionCheckerMocking.makeEverythingWork()
+
+ every { studyReviewRepository.findById(any(), any()) } returns reviewDto
+ every { questionRepository.findById(any(), questionId) } returns questionDto
+
+ sut.batchAnswerQuestion(presenter, request, context)
+
+ verify(exactly = 1) {
+ presenter.prepareSuccessView(withArg { response ->
+ assertTrue(response.succeededAnswers.isEmpty())
+ assertEquals(1, response.failedAnswers.size)
+ assertEquals(questionId, response.failedAnswers.first().questionId)
+ assertTrue("is not compatible with question type" in response.failedAnswers.first().reason)
+ })
+ }
+ }
+
+ @Test
+ fun `should create a failed answer entry for unsupported labeled scale answer type`() {
+ val reviewDto = factory.generateDto()
+ val context = "ROB"
+ val questionId = UUID.randomUUID()
+ val labelDto = AnswerQuestionService.LabelDto(
+ "LabelTest",
+ 1
+ )
+ val questionDto = factory.generateQuestionLabeledScaleDto(questionId, factory.systematicStudyId, questionContext = context, labelDto = labelDto)
+
+ val answerDetail = factory.answerDetail(questionId, "LABELED_SCALE", "invalid answer format")
+ val request = factory.batchAnswerRequest(listOf(answerDetail))
+
+ preconditionCheckerMocking.makeEverythingWork()
+ every { studyReviewRepository.findById(any(), any()) } returns reviewDto
+ every { questionRepository.findById(any(), questionId) } returns questionDto
+
+ sut.batchAnswerQuestion(presenter, request, context)
+
+ verify(exactly = 1) {
+ presenter.prepareSuccessView(withArg { response ->
+ assertTrue(response.succeededAnswers.isEmpty())
+ assertEquals(1, response.failedAnswers.size)
+ assertEquals(questionId, response.failedAnswers.first().questionId)
+ assertTrue("Unsupported answer type for 'LABELED_SCALE'" in response.failedAnswers.first().reason)
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt b/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt
index ad2554d20..63640d153 100644
--- a/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt
+++ b/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt
@@ -220,6 +220,27 @@ class TestDataFactory {
QuestionContextEnum.valueOf(questionContext),
)
+ fun batchAnswerRequest(
+ answers: List,
+ researcherId: UUID = this.researcherId,
+ systematicStudyId: UUID = this.systematicStudyId,
+ studyReviewId: Long = this.studyReviewId,
+ ) = BatchAnswerQuestionService.RequestModel(
+ userId = researcherId,
+ systematicStudyId = systematicStudyId,
+ studyReviewId = studyReviewId,
+ answers = answers
+ )
+
+ fun answerDetail(
+ questionId: UUID,
+ type: String,
+ answer: Any,
+ ) = BatchAnswerQuestionService.RequestModel.AnswerDetail(
+ questionId = questionId,
+ type = type,
+ answer = answer
+ )
operator fun component1() = researcherId
operator fun component2() = studyReviewId
diff --git a/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt b/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt
index f985969ae..1d41e1d00 100644
--- a/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt
+++ b/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt
@@ -6,8 +6,9 @@ import com.tngtech.archunit.junit.AnalyzeClasses
import com.tngtech.archunit.junit.ArchTest
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
+import org.junit.jupiter.api.Tag
-
+@Tag("ArchitectureTest")
@AnalyzeClasses(packages = ["br.all"], importOptions = [ImportOption.DoNotIncludeTests::class])
class DomainModuleTest {
diff --git a/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt b/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt
index edb3eada2..dcb7de383 100644
--- a/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt
+++ b/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt
@@ -3,6 +3,7 @@ package br.all.domain.model.search
import br.all.domain.model.protocol.SearchSource
import br.all.domain.model.user.ResearcherId
import br.all.domain.model.review.SystematicStudyId
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.time.LocalDateTime
@@ -10,6 +11,7 @@ import java.util.*
import kotlin.test.assertEquals
+@Tag("UnitTest")
class SearchSessionTest {
diff --git a/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt b/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt
index d0845449c..6655ecc1c 100644
--- a/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt
+++ b/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt
@@ -1,8 +1,10 @@
package br.all.domain.model.study
import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Tag
import kotlin.test.Test
+@Tag("UnitTest")
class DoiTest {
@Test
diff --git a/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt b/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt
index 1a6b27d17..1cfacf2e8 100644
--- a/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt
+++ b/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt
@@ -7,10 +7,12 @@ import br.all.domain.model.search.SearchSessionID
import br.all.domain.shared.utils.paragraph
import br.all.domain.shared.utils.year
import io.github.serpro69.kfaker.Faker
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.assertAll
import java.util.*
import kotlin.test.*
+@Tag("UnitTest")
class StudyReviewTest {
private val faker = Faker()
diff --git a/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt
index 7cce5feba..7b226fa5d 100644
--- a/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt
+++ b/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt
@@ -8,6 +8,8 @@ import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
+@Tag("UnitTest")
+@Tag("ServiceTest")
class BibtexConverterServiceTest {
private lateinit var sut: BibtexConverterService
diff --git a/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt
index 3964710f2..0242ecd11 100644
--- a/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt
+++ b/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt
@@ -5,10 +5,13 @@ import br.all.domain.model.search.SearchSessionID
import br.all.domain.services.RisTestData.testInput as risInput
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import java.util.*
import kotlin.test.assertEquals
+@Tag("IntegrationTest")
+@Tag("ServiceTest")
class ConverterFactoryServiceTest {
private lateinit var sut: ConverterFactoryService
private lateinit var bibtexConverterService: BibtexConverterService
diff --git a/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt
index d008b73ab..72993f0fd 100644
--- a/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt
+++ b/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt
@@ -16,6 +16,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
@Tag("IntegrationTest")
+@Tag("ServiceTest")
class ReviewSimilarityServiceTest {
private lateinit var sut: ReviewSimilarityService
diff --git a/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt
index e3ecaa20d..dc1eda98d 100644
--- a/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt
+++ b/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt
@@ -9,6 +9,8 @@ import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertTrue
+@Tag("UnitTest")
+@Tag("ServiceTest")
class RisConverterServiceTest {
private lateinit var sut: RisConverterService
private lateinit var idGeneratorService: IdGeneratorService
diff --git a/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt
index ef35805fa..228c2f31d 100644
--- a/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt
+++ b/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt
@@ -11,6 +11,7 @@ import java.util.*
import kotlin.test.assertEquals
@Tag("IntegrationTest")
+@Tag("ServiceTest")
class ScoreCalculatorServiceTest {
private lateinit var sut: ScoreCalculatorService
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt
index 05f63ee8b..55959a529 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt
@@ -1,6 +1,7 @@
package br.all.domain.shared.ddd
import br.all.domain.shared.valueobject.Email
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
@@ -8,6 +9,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+@Tag("UnitTest")
class EmailTest {
@Test
@@ -59,19 +61,6 @@ class EmailTest {
exception.message?.contains("Wrong Email format")?.let { assertTrue(it) }
}
-
- @Test
- fun `should not accept email with two equal TLDs`() {
- val exception = assertThrows { Email("email@ifsp.com.com") }
- exception.message?.contains("Wrong Email format")?.let { assertTrue(it) }
- }
-
- @Test
- fun `should not accept email with two equal subdomains`() {
- val exception = assertThrows { Email("email@ifsp.ifsp.com") }
- exception.message?.contains("Wrong Email format")?.let { assertTrue(it) }
- }
-
@Test
fun `email with incorrect format should include error message`() {
val exception = assertThrows { Email("invalid-email-format") }
@@ -102,7 +91,7 @@ class EmailTest {
@Test
fun `invalid email with special characters should throw an exception`() {
- val exception = assertThrows { Email("user!name@example.com") }
+ val exception = assertThrows { Email("user name@example.com") }
exception.message?.contains("Wrong Email format")?.let { assertTrue(it) }
}
@@ -149,7 +138,7 @@ class EmailTest {
@Test
fun `valid email with domain under maximum length should not throw an exception`() {
- assertDoesNotThrow { Email("user@" + "a".repeat(251) + ".com") }
+ assertDoesNotThrow { Email("user@" + "a".repeat(240) + ".com") }
}
@Test
@@ -157,6 +146,4 @@ class EmailTest {
val exception = assertThrows { Email("user@domain@com") }
exception.message?.contains("Wrong Email format")?.let { assertTrue(it) }
}
-
-
}
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt
index cc3e586c9..bbf2ae3b7 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt
@@ -1,8 +1,11 @@
package br.all.domain.shared.ddd
import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Tag
+import org.junit.jupiter.api.Tags
import org.junit.jupiter.api.Test
+@Tag("UnitTest")
class EntityTest{
@JvmInline
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt
index aa1ae7d8b..cb82af57a 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt
@@ -1,11 +1,13 @@
package br.all.domain.shared.ddd
import br.all.domain.shared.valueobject.Language
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
+@Tag("UnitTest")
class LanguageTest {
@Test
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt
index 404ac1d06..37ed02b9b 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt
@@ -1,8 +1,10 @@
package br.all.domain.shared.ddd
import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
+@Tag("UnitTest")
class NotificationTest {
@Test
fun `Should notification with no errors present no error message`(){
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt
index 29153d030..facc535a6 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt
@@ -3,11 +3,13 @@ package br.all.domain.shared.ddd
import br.all.domain.model.protocol.ProtocolId
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
-internal class ProtocolIdTest{
+@Tag("UnitTest")
+class ProtocolIdTest{
@Test
fun `valid ProtocolId`() {
val uuid = UUID.randomUUID()
diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt
index e77cfb994..29df084e3 100644
--- a/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.*
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
+@Tag("UnitTest")
class TextTest {
@Test
diff --git a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt
index ac3109ab1..ec2a9d83c 100644
--- a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt
@@ -2,6 +2,7 @@ package br.all.domain.shared.utils
import br.all.domain.shared.valueobject.DOI
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
@@ -9,7 +10,8 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
-internal class DOITest {
+@Tag("UnitTest")
+class DOITest {
@Test
fun `valid DOI should not throw an exception`() {
assertDoesNotThrow { DOI("10.1590/1089-6891v16i428131") }
diff --git a/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt b/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt
index 8e242cacd..6b3878563 100644
--- a/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt
+++ b/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt
@@ -1,10 +1,12 @@
package br.all.domain.shared.utils
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
+@Tag("UnitTest")
class PhraseTest {
@Test
fun `Should accept words with only lowercase letters`() {
diff --git a/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt b/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt
index 5c1269c24..b281a2cc8 100644
--- a/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt
+++ b/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt
@@ -18,8 +18,8 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.util.*
-@Tag("UnitTests")
-@Tag("ServiceTests")
+@Tag("UnitTest")
+@Tag("ServiceTest")
@ExtendWith(MockKExtension::class)
class ProtocolRepositoryImplTest {
@MockK
diff --git a/scas/pom.xml b/scas/pom.xml
index 14a1e5fb8..b8ddb39a2 100644
--- a/scas/pom.xml
+++ b/scas/pom.xml
@@ -81,7 +81,7 @@
org.jetbrains.kotlin
kotlin-stdlib
- 1.9.23
+ 1.9.0
br.all
diff --git a/test-actions.txt b/test-actions.txt
deleted file mode 100644
index af8ea0e99..000000000
--- a/test-actions.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-this is test push
-testing push 2
\ No newline at end of file
diff --git a/web/pom.xml b/web/pom.xml
index 4d14b3ac6..5a2c513f7 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.2
+ 3.4.4
@@ -18,7 +18,7 @@
17
- 1.8.22
+ 1.9.0
@@ -38,10 +38,11 @@
org.springframework.boot
spring-boot-starter-web
+
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.3.0
+ 2.8.9
org.springframework.boot
diff --git a/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt b/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt
index 4bf028a1e..8ef4aef3a 100644
--- a/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt
+++ b/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt
@@ -1,8 +1,10 @@
package br.all.protocol.controller
import br.all.application.protocol.find.FindProtocolService
+import br.all.application.protocol.find.GetProtocolStageService
import br.all.application.protocol.update.UpdateProtocolService
import br.all.protocol.presenter.RestfulFindProtocolPresenter
+import br.all.protocol.presenter.RestfulGetProtocolStagePresenter
import br.all.protocol.presenter.RestfulUpdateProtocolPresenter
import br.all.protocol.requests.PutRequest
import br.all.security.service.AuthenticationInfoService
@@ -17,12 +19,14 @@ import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.util.*
import br.all.application.protocol.find.FindProtocolService.RequestModel as FindOneRequestModel
+import br.all.application.protocol.find.GetProtocolStageService.RequestModel as FindStageRequestModel
@RestController
@RequestMapping("/systematic-study/{systematicStudyId}/protocol")
class ProtocolController(
private val findProtocolService: FindProtocolService,
private val updateProtocolService: UpdateProtocolService,
+ private val getProtocolStageService: GetProtocolStageService,
private val authenticationInfoService: AuthenticationInfoService,
private val linksFactory: LinksFactory
) {
@@ -86,4 +90,38 @@ class ProtocolController(
return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
+ @GetMapping("/stage")
+ @Operation(summary = "Get the current stage of the systematic review protocol")
+ @ApiResponses(value = [
+ ApiResponse(responseCode = "200", description = "Success getting the protocol stage",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = GetProtocolStageService.ResponseModel::class)
+ )]),
+ ApiResponse(
+ responseCode = "401",
+ description = "Fail getting the protocol stage - unauthenticated collaborator",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Fail getting the protocol stage - unauthorized collaborator",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "404",
+ description = "Fail getting the protocol stage - nonexistent protocol or systematic study",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ])
+ fun getStage(
+ @PathVariable systematicStudyId: UUID
+ ) : ResponseEntity<*> {
+ val presenter = RestfulGetProtocolStagePresenter(linksFactory)
+ val userId = authenticationInfoService.getAuthenticatedUserId()
+ val request = FindStageRequestModel(userId, systematicStudyId)
+
+ getProtocolStageService.getStage(presenter, request)
+ return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
+ }
}
diff --git a/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt b/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt
index 70923399e..11efb480f 100644
--- a/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt
+++ b/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt
@@ -1,8 +1,10 @@
package br.all.protocol.controller
import br.all.application.protocol.find.FindProtocolServiceImpl
+import br.all.application.protocol.find.GetProtocolStageServiceImpl
import br.all.application.protocol.repository.ProtocolRepository
import br.all.application.protocol.update.UpdateProtocolServiceImpl
+import br.all.application.question.repository.QuestionRepository
import br.all.application.review.repository.SystematicStudyRepository
import br.all.application.study.repository.StudyReviewRepository
import br.all.application.user.CredentialsService
@@ -27,4 +29,13 @@ class ProtocolServicesConfiguration {
studyReviewRepository: StudyReviewRepository,
scoreCalculatorService: ScoreCalculatorService
) = UpdateProtocolServiceImpl(protocolRepository, systematicStudyRepository, credentialsService, studyReviewRepository, scoreCalculatorService)
+
+ @Bean
+ fun getProtocolStageService(
+ protocolRepository: ProtocolRepository,
+ systematicStudyRepository: SystematicStudyRepository,
+ studyReviewRepository: StudyReviewRepository,
+ credentialsService: CredentialsService,
+ questionRepository: QuestionRepository
+ ) = GetProtocolStageServiceImpl(protocolRepository, systematicStudyRepository, studyReviewRepository, credentialsService, questionRepository)
}
diff --git a/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt
new file mode 100644
index 000000000..760cb5f3f
--- /dev/null
+++ b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt
@@ -0,0 +1,32 @@
+package br.all.protocol.presenter
+
+import br.all.application.protocol.find.GetProtocolStagePresenter
+import br.all.application.protocol.find.GetProtocolStageService.ResponseModel
+import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage
+import br.all.shared.error.createErrorResponseFrom
+import br.all.utils.LinksFactory
+import org.springframework.hateoas.RepresentationModel
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import java.util.UUID
+
+class RestfulGetProtocolStagePresenter(
+ private val linksFactory: LinksFactory
+) : GetProtocolStagePresenter {
+ var responseEntity: ResponseEntity<*>? = null
+
+ override fun prepareSuccessView(response: ResponseModel) {
+ val viewModel = ViewModel(response.userId, response.systematicStudyId, response.currentStage)
+ responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel)
+ }
+
+ override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) }
+
+ override fun isDone() = responseEntity != null
+
+ data class ViewModel(
+ val researcherId: UUID,
+ val systematicStudyId: UUID,
+ val currentStage: ProtocolStage,
+ ) : RepresentationModel()
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/report/controller/ReportController.kt b/web/src/main/kotlin/br/all/report/controller/ReportController.kt
index 7168c0f06..be2394a3a 100644
--- a/web/src/main/kotlin/br/all/report/controller/ReportController.kt
+++ b/web/src/main/kotlin/br/all/report/controller/ReportController.kt
@@ -3,6 +3,7 @@ package br.all.report.controller
import br.all.application.report.find.service.*
import br.all.report.presenter.*
import br.all.security.service.AuthenticationInfoService
+import br.all.report.presenter.RestfulFindStudyReviewCriteriaPresenter
import br.all.utils.LinksFactory
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
@@ -27,6 +28,7 @@ class ReportController(
private val authorNetworkService: AuthorNetworkService,
private val findKeywordsService: FindKeywordsService,
private val findStudiesByStageService: FindStudiesByStageService,
+ private val findStudyReviewCriteriaService: FindStudyReviewCriteriaService,
private val exportProtocolService: ExportProtocolService,
private val findAnswerService: FindAnswerServiceImpl,
private val studiesFunnelService: StudiesFunnelService,
@@ -350,4 +352,32 @@ class ReportController(
findAnswerService.find(presenter, request)
return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
+
+ @GetMapping("/study-review/{studyReview}/criteria")
+ @Operation(summary = "Return inclusion and exclusion criteria of study review")
+ @ApiResponses(
+ value = [
+ ApiResponse(
+ responseCode = "200", description = "Success getting criteria of a review",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = FindStudyReviewCriteriaService.ResponseModel::class)
+ )]
+ ),
+ ApiResponse(responseCode = "401", description = "Fail getting criteria of a review - unauthenticated user",
+ content = [Content(schema = Schema(hidden = true))]),
+ ApiResponse(responseCode = "403", description = "Fail getting criteria of a review - unauthorized user",
+ content = [Content(schema = Schema(hidden = true))]),
+ ]
+ )
+ fun findCriteria(
+ @PathVariable systematicStudyId: UUID,
+ @PathVariable studyReview: Long
+ ): ResponseEntity<*> {
+ val presenter = RestfulFindStudyReviewCriteriaPresenter(linksFactory)
+ val userId = authenticationInfoService.getAuthenticatedUserId()
+ val request = FindStudyReviewCriteriaService.RequestModel(userId, systematicStudyId, studyReview)
+ findStudyReviewCriteriaService.findCriteria(presenter, request)
+ return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
+ }
}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt b/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt
index 5712b44cf..2edc0fc1c 100644
--- a/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt
+++ b/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt
@@ -137,4 +137,17 @@ class ReportControllerConfiguration {
systematicStudyRepository,
questionRepository
)
+
+ @Bean
+ fun findStudyReviewCriteriaService(
+ systematicStudyRepository: SystematicStudyRepository,
+ studyReviewRepository: StudyReviewRepository,
+ credentialsService: CredentialsService,
+ protocolRepository: ProtocolRepository,
+ ) = FindStudyReviewCriteriaServiceImpl(
+ systematicStudyRepository,
+ studyReviewRepository,
+ credentialsService,
+ protocolRepository
+ )
}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt b/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt
new file mode 100644
index 000000000..017ed9d37
--- /dev/null
+++ b/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt
@@ -0,0 +1,37 @@
+package br.all.report.presenter
+
+import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter
+import br.all.application.report.find.service.FindStudyReviewCriteriaService
+import br.all.shared.error.createErrorResponseFrom
+import br.all.utils.LinksFactory
+import org.springframework.hateoas.RepresentationModel
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import org.springframework.http.ResponseEntity.status
+
+class RestfulFindStudyReviewCriteriaPresenter(
+ private val linksFactory: LinksFactory
+): FindStudyReviewCriteriaPresenter {
+ var responseEntity: ResponseEntity<*>? = null
+
+ override fun prepareSuccessView(response: FindStudyReviewCriteriaService.ResponseModel) {
+ val restfulResponse = ViewModel(
+ response.exclusionCriteria,
+ response.inclusionCriteria
+ )
+
+ val selfRef = linksFactory.findStudyReviewCriteria(response.systematicStudyId, response.studyReviewId)
+
+ restfulResponse.add(selfRef)
+ responseEntity = status(HttpStatus.OK).body(restfulResponse)
+ }
+
+ override fun prepareFailView(throwable: Throwable)= run { responseEntity = createErrorResponseFrom(throwable) }
+
+ override fun isDone() = responseEntity != null
+
+ private data class ViewModel(
+ val exclusionCriteria: Set,
+ val inclusionCriteria: Set,
+ ) : RepresentationModel()
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt
index 11670b2ba..f2005b0aa 100644
--- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt
+++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt
@@ -21,6 +21,9 @@ import org.springframework.web.bind.annotation.*
import java.util.*
import br.all.application.study.find.service.FindAllStudyReviewsBySourceService.RequestModel as FindAllBySourceRequest
import br.all.application.study.find.service.FindStudyReviewService.RequestModel as FindOneRequest
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService
+import br.all.study.presenter.RestfulBatchAnswerQuestionPresenter
+import br.all.study.requests.PatchBatchAnswerQuestionStudyReviewRequest
@RestController
@RequestMapping("/api/v1/systematic-study/{systematicStudy}")
@@ -35,8 +38,10 @@ class StudyReviewController(
private val updateSelectionService: UpdateStudyReviewSelectionService,
private val updateExtractionService: UpdateStudyReviewExtractionService,
private val updateReadingPriorityService: UpdateStudyReviewPriorityService,
+ private val removeCriteriaService: RemoveCriteriaService,
private val markAsDuplicatedService: MarkAsDuplicatedService,
private val answerQuestionService: AnswerQuestionService,
+ private val batchAnswerQuestionService: BatchAnswerQuestionService,
private val authenticationInfoService: AuthenticationInfoService,
private val linksFactory: LinksFactory
@@ -441,6 +446,86 @@ class StudyReviewController(
return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
+ @PatchMapping("/study-review/{studyReview}/batch-riskOfBias-answers")
+ @Operation(summary = "Update a batch of answers for risk of bias questions")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Success updating a batch of answers to risk of bias questions",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = BatchAnswerQuestionService.ResponseModel::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "400",
+ description = "Fail updating answers - invalid payload",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Fail updating answers - unauthenticated user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Fail updating answers - unauthorized user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ]
+ )
+ fun batchRiskOfBiasAnswers(
+ @PathVariable systematicStudy: UUID,
+ @PathVariable studyReview: Long,
+ @RequestBody requestBody: PatchBatchAnswerQuestionStudyReviewRequest
+ ): ResponseEntity<*> {
+ val presenter = RestfulBatchAnswerQuestionPresenter(linksFactory)
+ val userId = authenticationInfoService.getAuthenticatedUserId()
+ val request = requestBody.toRequestModel(userId, systematicStudy, studyReview)
+
+ batchAnswerQuestionService.batchAnswerQuestion(presenter, request, "ROB")
+ return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
+ }
+
+ @PatchMapping("/study-review/{studyReview}/batch-extraction-answers")
+ @Operation(summary = "Update a batch of answers for extraction questions")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Success updating a batch of answers to extraction questions",
+ content = [Content(
+ mediaType = "application/json",
+ schema = Schema(implementation = BatchAnswerQuestionService.ResponseModel::class)
+ )]
+ ),
+ ApiResponse(
+ responseCode = "400",
+ description = "Fail updating answers - invalid payload",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Fail updating answers - unauthenticated user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Fail updating answers - unauthorized user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ]
+ )
+ fun batchExtractionAnswers(
+ @PathVariable systematicStudy: UUID,
+ @PathVariable studyReview: Long,
+ @RequestBody requestBody: PatchBatchAnswerQuestionStudyReviewRequest
+ ): ResponseEntity<*> {
+ val presenter = RestfulBatchAnswerQuestionPresenter(linksFactory)
+ val userId = authenticationInfoService.getAuthenticatedUserId()
+ val request = requestBody.toRequestModel(userId, systematicStudy, studyReview)
+
+ batchAnswerQuestionService.batchAnswerQuestion(presenter, request, "EXTRACTION")
+ return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
+ }
+
@PatchMapping("/study-review/{referenceStudyId}/duplicated")
@Operation(summary = "Mark multiple existing studies as duplicated in the systematic study")
@ApiResponses(
@@ -481,4 +566,39 @@ class StudyReviewController(
markAsDuplicatedService.markAsDuplicated(presenter, request)
return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
+
+ @PatchMapping("/study-review/remove-criteria/{studyReviewId}")
+ @Operation(summary = "Remove a criteria from study review")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Success removing criteria of study review",
+ content = [Content(schema = Schema(hidden = true))]),
+ ApiResponse(
+ responseCode = "400",
+ description = "Fail removing criteria of study review - invalid status",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "401",
+ description = "Fail removing criteria of study review - unauthenticated user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ApiResponse(
+ responseCode = "403",
+ description = "Fail removing criteria of study review - unauthorized user",
+ content = [Content(schema = Schema(hidden = true))]
+ ),
+ ]
+ )
+ fun removeCriterion(
+ @PathVariable systematicStudy: UUID,
+ @PathVariable studyReviewId: Long,
+ @RequestBody patchRequest: RemoveCriteriaRequest
+ ): ResponseEntity<*> {
+ val presenter = RestfulRemoveCriteriaPresenter(linksFactory)
+ val userId = authenticationInfoService.getAuthenticatedUserId()
+ val request = patchRequest.toRequestModel(userId, systematicStudy, studyReviewId)
+ removeCriteriaService.removeCriteria(presenter, request)
+ return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
+ }
}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt
index 4c89d9144..ea5d2acd6 100644
--- a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt
+++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt
@@ -138,4 +138,28 @@ class StudyReviewServicesConfiguration {
systematicStudyRepository,
credentialsService
)
+
+ @Bean
+ fun batchAnswerQuestionService(
+ studyReviewRepository: StudyReviewRepository,
+ questionRepository: QuestionRepository,
+ systematicStudyRepository: SystematicStudyRepository,
+ credentialsService: CredentialsService
+ ) = BatchAnswerQuestionServiceImpl(
+ studyReviewRepository,
+ questionRepository,
+ systematicStudyRepository,
+ credentialsService
+ )
+
+ @Bean
+ fun removeCriteriaService(
+ studyReviewRepository: StudyReviewRepository,
+ systematicStudyRepository: SystematicStudyRepository,
+ credentialsService: CredentialsService,
+ ) = RemoveCriteriaServiceImpl(
+ studyReviewRepository,
+ systematicStudyRepository,
+ credentialsService
+ )
}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt
new file mode 100644
index 000000000..7cc8f06b2
--- /dev/null
+++ b/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt
@@ -0,0 +1,48 @@
+package br.all.study.presenter
+
+import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService.FailedAnswer
+import br.all.shared.error.createErrorResponseFrom
+import br.all.utils.LinksFactory
+import org.springframework.hateoas.RepresentationModel
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+import java.util.UUID
+
+class RestfulBatchAnswerQuestionPresenter(
+ private val linksFactory: LinksFactory
+): BatchAnswerQuestionPresenter {
+ var responseEntity: ResponseEntity<*>?= null
+
+ override fun prepareSuccessView(response: ResponseModel) {
+ val viewModel = ViewModel(
+ systematicStudyId = response.systematicStudyId,
+ studyReviewId = response.studyReviewId,
+ succeededAnswers = response.succeededAnswers,
+ failedAnswers = response.failedAnswers,
+ totalAnswered = response.totalAnswered
+ )
+
+ val link = linksFactory.findStudy(response.systematicStudyId, response.studyReviewId)
+ viewModel.add(link)
+
+ responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel)
+ }
+
+ override fun prepareFailView(throwable: Throwable) {
+ responseEntity = createErrorResponseFrom(throwable)
+ }
+
+ override fun isDone(): Boolean {
+ return responseEntity != null
+ }
+
+ data class ViewModel(
+ val systematicStudyId: UUID,
+ val studyReviewId: Long,
+ val succeededAnswers: List,
+ val failedAnswers: List,
+ val totalAnswered: Int
+ ) : RepresentationModel()
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt
index 16b363b89..6aab812d7 100644
--- a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt
+++ b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt
@@ -4,11 +4,8 @@ import br.all.application.study.find.presenter.FindAllStudyReviewsPresenter
import br.all.application.study.find.service.FindAllStudyReviewsService.ResponseModel
import br.all.application.study.repository.StudyReviewDto
import br.all.shared.error.createErrorResponseFrom
-import br.all.study.controller.StudyReviewController
-import br.all.study.requests.PostStudyReviewRequest
import br.all.utils.LinksFactory
import org.springframework.hateoas.RepresentationModel
-import org.springframework.hateoas.server.mvc.linkTo
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Component
@@ -25,10 +22,13 @@ class RestfulFindAllStudyReviewsPresenter(
val restfulResponse = ViewModel(response.systematicStudyId, response.studyReviews.size, response.studyReviews)
val selfRef = linksFactory.findAllStudies(response.systematicStudyId)
- val allBySource = linksFactory.findAllStudiesBySource(response.systematicStudyId, "")
+ val sources = getSources(response.studyReviews)
+ for (source in sources) {
+ restfulResponse.add(linksFactory.findAllStudiesBySource(response.systematicStudyId, source))
+ }
val createStudyReview = linksFactory.createStudy(response.systematicStudyId)
- restfulResponse.add(selfRef, allBySource, createStudyReview)
+ restfulResponse.add(selfRef, createStudyReview)
responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse)
}
@@ -36,6 +36,8 @@ class RestfulFindAllStudyReviewsPresenter(
override fun isDone() = responseEntity != null
+ fun getSources(studies: List): List = studies.flatMap { it.searchSources }.distinct()
+
private data class ViewModel (
val systematicStudyId : UUID,
val size: Int,
diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt
new file mode 100644
index 000000000..438284947
--- /dev/null
+++ b/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt
@@ -0,0 +1,60 @@
+package br.all.study.presenter
+
+import br.all.application.study.update.interfaces.RemoveCriteriaPresenter
+import br.all.application.study.update.interfaces.RemoveCriteriaService
+import br.all.shared.error.createErrorResponseFrom
+import br.all.study.requests.RemoveCriteriaRequest
+import br.all.utils.LinksFactory
+import org.springframework.hateoas.RepresentationModel
+import org.springframework.http.HttpStatus
+import org.springframework.http.ResponseEntity
+
+class RestfulRemoveCriteriaPresenter(
+ private val linksFactory: LinksFactory,
+): RemoveCriteriaPresenter {
+ var responseEntity: ResponseEntity<*>? = null
+
+ override fun prepareSuccessView(response: RemoveCriteriaService.ResponseModel) {
+ val restfulResponse = ViewModel(
+ response.inclusionCriteria,
+ response.exclusionCriteria,
+ )
+
+ response.inclusionCriteria.forEach {
+ restfulResponse.add(linksFactory.removeStudyReviewCriteria
+ (
+ response.systematicStudyId,
+ response.studyId,
+ RemoveCriteriaRequest(
+ criteria = listOf(it)
+ )
+ )
+ )
+ }
+
+ response.exclusionCriteria.forEach {
+ restfulResponse.add(linksFactory.removeStudyReviewCriteria
+ (
+ response.systematicStudyId,
+ response.studyId,
+ RemoveCriteriaRequest(
+ criteria = listOf(it)
+ )
+ )
+ )
+ }
+
+ val protocol = linksFactory.findProtocol(response.systematicStudyId)
+ restfulResponse.add(protocol)
+ responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse)
+ }
+
+ override fun prepareFailView(throwable: Throwable) = run {responseEntity = createErrorResponseFrom(throwable) }
+
+ override fun isDone() = responseEntity != null
+
+ private data class ViewModel(
+ val inclusionCriteria: List,
+ val exclusionCriteria: List,
+ ) : RepresentationModel()
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt b/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt
new file mode 100644
index 000000000..110711493
--- /dev/null
+++ b/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt
@@ -0,0 +1,34 @@
+package br.all.study.requests
+
+import br.all.application.study.update.interfaces.BatchAnswerQuestionService
+import java.util.UUID
+
+class PatchBatchAnswerQuestionStudyReviewRequest(
+ val answers: List
+) {
+ data class AnswerPayload(
+ val questionId: UUID,
+ val type: String,
+ val answer: Any
+ )
+
+ fun toRequestModel(userId: UUID,
+ systematicStudyId: UUID,
+ studyReviewId: Long
+ ): BatchAnswerQuestionService.RequestModel {
+ val serviceAnswers = this.answers.map {
+ BatchAnswerQuestionService.RequestModel.AnswerDetail(
+ questionId = it.questionId,
+ type = it.type,
+ answer = it.answer
+ )
+ }
+
+ return BatchAnswerQuestionService.RequestModel(
+ userId = userId,
+ systematicStudyId = systematicStudyId,
+ studyReviewId = studyReviewId,
+ answers = serviceAnswers
+ )
+ }
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt b/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt
new file mode 100644
index 000000000..391995f94
--- /dev/null
+++ b/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt
@@ -0,0 +1,15 @@
+package br.all.study.requests
+
+import br.all.application.study.update.interfaces.RemoveCriteriaService
+import java.util.*
+
+data class RemoveCriteriaRequest (
+ val criteria: List,
+) {
+ fun toRequestModel(userId: UUID, systematicStudyId: UUID, studyReviewId: Long) = RemoveCriteriaService.RequestModel (
+ userId,
+ systematicStudyId,
+ studyReviewId,
+ criteria
+ )
+}
diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt
index ce4765c8b..205d590f6 100644
--- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt
+++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt
@@ -12,6 +12,7 @@ import br.all.study.controller.StudyReviewController
import br.all.study.requests.PatchDuplicatedStudiesRequest
import br.all.study.requests.PatchStatusStudyReviewRequest
import br.all.study.requests.PostStudyReviewRequest
+import br.all.study.requests.RemoveCriteriaRequest
import org.springframework.hateoas.Link
import org.springframework.hateoas.server.mvc.linkTo
import org.springframework.stereotype.Component
@@ -316,5 +317,21 @@ class LinksFactory {
questionId
)
}.withRel("find-answer").withType("GET")
-}
+ fun findStudyReviewCriteria(systematicStudyId: UUID, studyReviewId: Long): Link =
+ linkTo {
+ findCriteria(
+ systematicStudyId,
+ studyReviewId
+ )
+ }.withRel("find-study-criteria").withType("GET")
+
+ fun removeStudyReviewCriteria(systematicStudyId: UUID, studyReviewId: Long, request: RemoveCriteriaRequest): Link =
+ linkTo {
+ removeCriterion(
+ systematicStudyId,
+ studyReviewId,
+ request
+ )
+ }.withRel("remove-criteria").withType("PATCH")
+}
diff --git a/web/src/main/resources/application.yaml b/web/src/main/resources/application.yaml
index d5956b6bc..6e9a2abe7 100644
--- a/web/src/main/resources/application.yaml
+++ b/web/src/main/resources/application.yaml
@@ -21,7 +21,6 @@ spring:
properties:
hibernate:
format_sql: true
- database-platform: org.hibernate.dialect.PostgreSQLDialect
jwt:
key: "dasdasdadadasdasdadadasdasdadadasdasdadadasdasdada"
diff --git a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt
index 317efff83..b99006f85 100644
--- a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt
+++ b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt
@@ -1,5 +1,6 @@
package br.all.protocol.controller
+import br.all.application.protocol.repository.CriterionDto
import br.all.infrastructure.protocol.MongoProtocolRepository
import br.all.infrastructure.review.MongoSystematicStudyRepository
import br.all.infrastructure.review.SystematicStudyDocument
@@ -23,6 +24,7 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory
@SpringBootTest
@AutoConfigureMockMvc
@Tag("IntegrationTest")
+@Tag("ControllerTest")
@DisplayName("Protocol Controller Integration Tests")
class ProtocolControllerTest(
@Autowired private val protocolRepository: MongoProtocolRepository,
@@ -63,6 +65,12 @@ class ProtocolControllerTest(
private fun getUrl(systematicStudy: UUID = factory.protocol) =
"/systematic-study/$systematicStudy/protocol"
+ private fun putUrl(systematicStudyId: UUID = factory.protocol) =
+ "/systematic-study/$systematicStudyId/protocol"
+
+ private fun getStage(systematicStudy: UUID = factory.protocol) =
+ "/systematic-study/$systematicStudy/protocol/stage"
+
@Nested
@DisplayName("When getting protocols")
inner class WhenGettingProtocols {
@@ -124,9 +132,6 @@ class ProtocolControllerTest(
}
}
- private fun putUrl(systematicStudyId: UUID = factory.protocol) =
- "/systematic-study/$systematicStudyId/protocol"
-
@Nested
@DisplayName("When putting protocols")
inner class WhenPuttingProtocols {
@@ -153,7 +158,11 @@ class ProtocolControllerTest(
@Test
fun `should update an existing protocol without deleting existing collection-type variables`() {
- val document = factory.createProtocolDocument()
+ val document = factory.createProtocolDocument(
+ keywords = setOf("keyword1", "keyword2"),
+ robQuestions = setOf(UUID.randomUUID(), UUID.randomUUID()),
+ selectionCriteria = setOf(CriterionDto("desc", "INCLUSION"))
+ )
val json = factory.validPutRequest()
protocolRepository.save(document)
@@ -249,4 +258,43 @@ class ProtocolControllerTest(
}
}
}
+
+ @Nested
+ @DisplayName("When getting protocol stage")
+ inner class WhenGettingProtocolStage {
+ @Test
+ fun `should return protocol stage with 200 status when protocol exists`() {
+ val document = factory.createProtocolDocument()
+ protocolRepository.save(document)
+
+ mockMvc.perform(get(getStage())
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isOk)
+ .andExpect(jsonPath("$.systematicStudyId").value(factory.protocol.toString()))
+ .andExpect(jsonPath("$.currentStage").exists())
+ }
+
+ @Test
+ fun `should return 404 when protocol does not exist`() {
+ mockMvc.perform(get(getStage())
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ .contentType(MediaType.APPLICATION_JSON)
+ )
+ .andExpect(status().isNotFound)
+ .andExpect(jsonPath("$.message").exists())
+ .andExpect(jsonPath("$.detail").exists())
+ }
+
+ @Test
+ fun `should not authorize researchers that are not collaborators to get protocol stage`() {
+ testHelperService.testForUnauthorizedUser(mockMvc, get(getStage()))
+ }
+
+ @Test
+ fun `should not allow unauthenticated users to get protocol stage`() {
+ testHelperService.testForUnauthenticatedUser(mockMvc, get(getStage()))
+ }
+ }
}
\ No newline at end of file
diff --git a/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt b/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt
index fee36a63e..1c8f03b11 100644
--- a/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt
+++ b/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt
@@ -10,6 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
@Tag("IntegrationTest")
+@Tag("RepositoryTest")
class MongoProtocolRepositoryTest(
@Autowired private val sut: MongoProtocolRepository,
) {
diff --git a/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt b/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt
index a0da57f75..0a47a7b1e 100644
--- a/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt
+++ b/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt
@@ -62,6 +62,14 @@ class TestDataFactory {
picoc,
)
+ fun createCriteria(
+ type: String,
+ description: String,
+ ) = CriterionDto(
+ type = type,
+ description = description,
+ )
+
private fun eligibilityCriteria() = List(5) {
CriterionDto(faker.paragraph(5), faker.random.randomValue(listOf("INCLUSION", "EXCLUSION")))
}.toSet()
diff --git a/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt b/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt
index 87fb4da8a..f9b441db8 100644
--- a/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt
+++ b/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt
@@ -22,6 +22,7 @@ import java.util.*
@SpringBootTest
@AutoConfigureMockMvc
@Tag("IntegrationTest")
+@Tag("ControllerTest")
class ExtractionQuestionControllerTest(
@Autowired val repository: MongoQuestionRepository,
@Autowired val systematicStudyRepository: MongoSystematicStudyRepository,
diff --git a/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt b/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt
index 71f5f4ff0..2ea33c5e5 100644
--- a/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt
+++ b/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt
@@ -22,6 +22,7 @@ import java.util.*
@SpringBootTest
@AutoConfigureMockMvc
@Tag("IntegrationTest")
+@Tag("ControllerTest")
class RiskOfBiasQuestionControllerTest(
@Autowired val repository: MongoQuestionRepository,
@Autowired val systematicStudyRepository: MongoSystematicStudyRepository,
diff --git a/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt b/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt
index 53c818e05..78c7ddb5c 100644
--- a/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt
+++ b/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt
@@ -13,6 +13,8 @@ import org.springframework.boot.test.context.SpringBootTest
import java.util.UUID
@SpringBootTest
+@Tag("IntegrationTest")
+@Tag("RepositoryTest")
class MongoQuestionRepositoryTest(
@Autowired private val sut: MongoQuestionRepository,
) {
diff --git a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt
index 382b3dbed..8c3cb88c4 100644
--- a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt
+++ b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt
@@ -27,10 +27,10 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory
import br.all.study.utils.TestDataFactory as StudyReviewTestDataFactory
import br.all.protocol.shared.TestDataFactory as ProtocolTestDataFactory
-
@SpringBootTest
@AutoConfigureMockMvc
@Tag("IntegrationTest")
+@Tag("ControllerTest")
@DisplayName("Report Controller Integration Tests")
class ReportControllerTest(
@Autowired private val studyReviewRepository: MongoStudyReviewRepository,
@@ -118,6 +118,9 @@ class ReportControllerTest(
private fun studiesFunnelUrl() =
"$baseReportUrl/studies-funnel"
+ private fun findStudyReviewCriteria(studyReviewId: Long) =
+ "$baseReportUrl/study-review/$studyReviewId/criteria"
+
private fun findKeywordsUrl(filter: String?) =
if (filter.isNullOrBlank())
"$baseReportUrl/keywords"
@@ -694,4 +697,42 @@ class ReportControllerTest(
}
}
}
+
+ @Nested
+ @DisplayName("When finding criteria of a study review")
+ inner class WhenFindingCriteriaOfAStudyReview {
+ @Test
+ fun `should return 200 and find correctly the criteria`() {
+ val systematicStudyId = systematicStudy.id
+ // o intelli j tava reclamando que o valor do id era sempre o mesmo
+ // então é por isso que tem esse negócio ai, qualquer coisa só
+ // coloca val studyReviewId = 1000L
+ val seed = System.currentTimeMillis()
+ val studyReviewId = (seed * 31) % Long.MAX_VALUE
+
+
+ val studyReview = studyReviewDataFactory.reviewDocument(
+ criteria = setOf("criteria 1", "criteria 2"),
+ systematicStudyId = systematicStudyId,
+ studyReviewId = studyReviewId,
+ )
+ studyReviewRepository.save(studyReview)
+
+ val criteria1 = protocolDataFactory.createCriteria("INCLUSION", "criteria 1")
+
+ val criteria2 = protocolDataFactory.createCriteria("EXCLUSION", "criteria 2")
+
+ val protocol = protocolDataFactory.createProtocolDocument(
+ id = systematicStudyId,
+ selectionCriteria = setOf(criteria1, criteria2)
+ )
+ protocolRepository.save(protocol)
+
+ mockMvc.perform(
+ get(findStudyReviewCriteria(studyReviewId = studyReviewId))
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ )
+ .andExpect(status().isOk)
+ }
+ }
}
diff --git a/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt b/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt
index facd73d72..43224dc85 100644
--- a/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt
+++ b/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt
@@ -23,6 +23,7 @@ import java.util.*
@SpringBootTest
@AutoConfigureMockMvc
@Tag("IntegrationTest")
+@Tag("ControllerTest")
class SystematicStudyControllerTest(
@Autowired private val repository: MongoSystematicStudyRepository,
@Autowired private val testHelperService: TestHelperService,
diff --git a/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt b/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt
index 1a81d5f4a..c8c8cf4a6 100644
--- a/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt
+++ b/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt
@@ -11,6 +11,7 @@ import java.util.*
@SpringBootTest
@Tag("IntegrationTest")
+@Tag("RepositoryTest")
class MongoSystematicStudyRepositoryTest(
@Autowired private val sut: MongoSystematicStudyRepository,
) {
diff --git a/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt b/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt
index 0f7fe576d..bf5ded6e2 100644
--- a/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt
+++ b/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt
@@ -1,14 +1,12 @@
package br.all.review.shared
-import br.all.application.user.repository.UserAccountDto
import br.all.infrastructure.review.SystematicStudyDocument
import io.github.serpro69.kfaker.Faker
-import java.time.LocalDateTime
import java.util.*
class TestDataFactory {
private val faker = Faker()
- val researcherId: UUID = UUID.randomUUID()
+ private val researcherId: UUID = UUID.randomUUID()
val systematicStudyId: UUID = UUID.randomUUID()
val ownerId: UUID = UUID.randomUUID()
diff --git a/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt b/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt
index 2aef8a289..1d3fe1a22 100644
--- a/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt
+++ b/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt
@@ -29,6 +29,8 @@ import java.util.*
@SpringBootTest
@AutoConfigureMockMvc
+@Tag("IntegrationTest")
+@Tag("ControllerTest")
class SearchSessionControllerTest(
@Autowired val repository: MongoSearchSessionRepository,
@Autowired val systematicStudyRepository: MongoSystematicStudyRepository,
diff --git a/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt b/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt
index fbeba7400..2f62a0fe3 100644
--- a/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt
+++ b/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt
@@ -8,6 +8,8 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import java.util.*
@SpringBootTest
+@Tag("IntegrationTest")
+@Tag("RepositoryTest")
class MongoSearchSessionRepositoryTest(
@Autowired private val sut: MongoSearchSessionRepository,
) {
diff --git a/web/src/test/kotlin/br/all/shared/TestHelperService.kt b/web/src/test/kotlin/br/all/shared/TestHelperService.kt
index fd86578cb..bf6d40fac 100644
--- a/web/src/test/kotlin/br/all/shared/TestHelperService.kt
+++ b/web/src/test/kotlin/br/all/shared/TestHelperService.kt
@@ -4,6 +4,7 @@ import br.all.application.user.repository.UserAccountDto
import br.all.application.user.repository.UserAccountRepository
import br.all.security.service.ApplicationUser
import io.github.serpro69.kfaker.Faker
+import org.junit.jupiter.api.Tag
import org.springframework.http.MediaType
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors
import org.springframework.stereotype.Service
@@ -14,6 +15,7 @@ import java.time.LocalDateTime
import java.util.*
@Service
+@Tag("UnitTest")
class TestHelperService(
private val repository: UserAccountRepository,
) {
diff --git a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt
index aab3d409f..7a2233ff9 100644
--- a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt
+++ b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt
@@ -9,8 +9,12 @@ import br.all.infrastructure.study.StudyReviewIdGeneratorService
import br.all.security.service.ApplicationUser
import br.all.shared.TestHelperService
import br.all.study.utils.TestDataFactory
+import org.hamcrest.Matchers.containsString
+import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.hasSize
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
@@ -27,6 +31,8 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory
@SpringBootTest
@AutoConfigureMockMvc
+@Tag("IntegrationTest")
+@Tag("ControllerTest")
class StudyReviewControllerTest(
@Autowired val repository: MongoStudyReviewRepository,
@Autowired val systematicStudyRepository: MongoSystematicStudyRepository,
@@ -69,6 +75,12 @@ class StudyReviewControllerTest(
fun answerExtractionQuestion(studyReviewId: Long) =
"/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/extraction-answer"
+ fun batchAnswerRiskOfBiasQuestion(studyReviewId: Long) =
+ "/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/batch-riskOfBias-answers"
+
+ fun batchAnswerExtractionQuestion(studyReviewId: Long) =
+ "/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/batch-extraction-answers"
+
@BeforeEach
fun setUp() {
repository.deleteAll()
@@ -637,6 +649,167 @@ class StudyReviewControllerTest(
}
}
+ @Nested
+ @DisplayName("When batch answering ROB questions in a review")
+ inner class BatchAnswerRobQuestionsTests(
+ @Autowired val questionRepository: MongoQuestionRepository
+ ) {
+ @Test
+ @DisplayName("should save valid answers, ignore invalid ones, and return 200 with a detailed report")
+ fun `should handle partial success correctly`() {
+ val studyId = idService.next()
+ val studyReview = factory.reviewDocument(systematicStudyId, studyId)
+ repository.insert(studyReview)
+
+ val validQId1 = UUID.randomUUID()
+ val validQId2 = UUID.randomUUID()
+ val invalidQId = UUID.randomUUID()
+
+ val question1 = factory.generateRobQuestionTextualDto(validQId1, systematicStudyId)
+ val question2 = factory.generateRobQuestionTextualDto(validQId2, systematicStudyId)
+ val question3 = factory.generateRobQuestionNumberScaleDto(invalidQId, systematicStudyId)
+
+ questionRepository.insert(question1)
+ questionRepository.insert(question2)
+ questionRepository.insert(question3)
+
+ val jsonPayload = """
+ {
+ "answers": [
+ { "questionId": "$validQId1", "type": "TEXTUAL", "answer": "First valid answer" },
+ { "questionId": "$validQId2", "type": "TEXTUAL", "answer": "Second valid answer" },
+ { "questionId": "$invalidQId", "type": "NUMBERED_SCALE", "answer": "This shouldn't be a string" }
+ ]
+ }
+ """.trimIndent()
+
+ mockMvc.perform(
+ patch(batchAnswerRiskOfBiasQuestion(studyId))
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ .contentType(MediaType.APPLICATION_JSON).content(jsonPayload)
+ )
+ .andExpect(status().isOk)
+ .andExpect(jsonPath("$.totalAnswered").value(2))
+ .andExpect(jsonPath("$.succeededAnswers", hasSize(2)))
+ .andExpect(jsonPath("$.succeededAnswers", hasItem(validQId1.toString())))
+ .andExpect(jsonPath("$.succeededAnswers", hasItem(validQId2.toString())))
+ .andExpect(jsonPath("$.failedAnswers", hasSize(1)))
+ .andExpect(jsonPath("$.failedAnswers[0].questionId").value(invalidQId.toString()))
+ .andExpect(jsonPath("$.failedAnswers[0].reason", containsString("not compatible with question type 'NUMBERED_SCALE'")))
+
+ val updatedReview = repository.findById(StudyReviewId(systematicStudyId, studyId)).get()
+
+ assertEquals(3, updatedReview.qualityAnswers.size)
+ assertEquals("First valid answer", updatedReview.qualityAnswers[validQId1])
+ assertEquals("Second valid answer", updatedReview.qualityAnswers[validQId2])
+ assertNull(updatedReview.qualityAnswers[invalidQId])
+ }
+
+ @Test
+ @DisplayName("should return 401 for unauthenticated users")
+ fun `should not update if user is unauthenticated`() {
+ testHelperService.testForUnauthenticatedUser(mockMvc, patch(batchAnswerRiskOfBiasQuestion(1L)))
+ }
+
+ @Test
+ @DisplayName("should return 403 for unauthorized users")
+ fun `should not update if user is unauthorized`() {
+ val jsonPayload = """{ "answers": [] }"""
+ testHelperService.testForUnauthorizedUser(
+ mockMvc,
+ patch(batchAnswerRiskOfBiasQuestion(1L)).content(jsonPayload)
+ )
+ }
+ }
+
+ @Nested
+ @DisplayName("When batch answering Extraction questions in a review")
+ inner class BatchAnswerExtractionQuestionsTests(
+ @Autowired val questionRepository: MongoQuestionRepository
+ ) {
+ @Test
+ @DisplayName("should save all valid answers and return 200 with no failures")
+ fun `should handle full success correctly`() {
+ val studyId = idService.next()
+ val studyReview = factory.reviewDocument(systematicStudyId, studyId)
+ repository.insert(studyReview)
+
+ val textQId = UUID.randomUUID()
+ val pickListQId = UUID.randomUUID()
+
+ val question1 = factory.generateExtractionQuestionTextualDto(textQId, systematicStudyId = systematicStudyId)
+ val question2 = factory.generateExtractionQuestionPickListDto(pickListQId, systematicStudyId = systematicStudyId)
+
+ questionRepository.insert(question1)
+ questionRepository.insert(question2)
+
+ val jsonPayload = """
+ {
+ "answers": [
+ { "questionId": "$textQId", "type": "TEXTUAL", "answer": "Another valid answer" },
+ { "questionId": "$pickListQId", "type": "PICK_LIST", "answer": "Option A" }
+ ]
+ }
+ """.trimIndent()
+
+ mockMvc.perform(
+ patch(batchAnswerExtractionQuestion(studyId))
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ .contentType(MediaType.APPLICATION_JSON).content(jsonPayload)
+ )
+ .andExpect(status().isOk)
+ .andExpect(jsonPath("$.totalAnswered").value(2))
+ .andExpect(jsonPath("$.succeededAnswers", hasSize(2)))
+ .andExpect(jsonPath("$.failedAnswers", hasSize(0)))
+
+ val updatedReview = repository.findById(StudyReviewId(systematicStudyId, studyId)).get()
+ assertEquals(3, updatedReview.formAnswers.size)
+ assertEquals("Another valid answer", updatedReview.formAnswers[textQId])
+ assertEquals("Option A", updatedReview.formAnswers[pickListQId])
+ }
+
+ @Test
+ @DisplayName("should not save answers if the question context is wrong")
+ fun `should fail on context mismatch`() {
+ val studyId = idService.next()
+ repository.insert(factory.reviewDocument(systematicStudyId, studyId))
+
+ val extractionQId = UUID.randomUUID()
+ questionRepository.insert(factory.generateExtractionQuestionTextualDto(extractionQId, systematicStudyId = systematicStudyId))
+
+ val jsonPayload = """
+ { "answers": [{ "questionId": "$extractionQId", "type": "TEXTUAL", "answer": "some answer" }] }
+ """
+
+ mockMvc.perform(
+ patch(batchAnswerRiskOfBiasQuestion(studyId))
+ .with(SecurityMockMvcRequestPostProcessors.user(user))
+ .contentType(MediaType.APPLICATION_JSON).content(jsonPayload)
+ )
+ .andExpect(status().isOk)
+ .andExpect(jsonPath("$.totalAnswered").value(0))
+ .andExpect(jsonPath("$.failedAnswers", hasSize(1)))
+ .andExpect(jsonPath("$.failedAnswers[0].questionId").value(extractionQId.toString()))
+ .andExpect(jsonPath("$.failedAnswers[0].reason", containsString("Should answer question with the context: ROB")))
+ }
+
+ @Test
+ @DisplayName("should return 401 for unauthenticated users")
+ fun `should not update if user is unauthenticated`() {
+ testHelperService.testForUnauthenticatedUser(mockMvc, patch(batchAnswerExtractionQuestion(1L)))
+ }
+
+ @Test
+ @DisplayName("should return 403 for unauthorized users")
+ fun `should not update if user is unauthorized`() {
+ val jsonPayload = """{ "answers": [] }"""
+ testHelperService.testForUnauthorizedUser(
+ mockMvc,
+ patch(batchAnswerExtractionQuestion(1L)).content(jsonPayload)
+ )
+ }
+ }
+
@Nested
@DisplayName("When marking a study review as duplicated")
inner class MarkingAsDuplicatedTests {
diff --git a/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt b/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt
index 1278542e3..1ebed0a45 100644
--- a/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt
+++ b/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt
@@ -8,12 +8,15 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import java.util.UUID
@SpringBootTest
+@Tag("IntegrationTest")
+@Tag("RepositoryTest")
class MongoStudyReviewRepositoryTest (
@Autowired private val sut: MongoStudyReviewRepository,
@Autowired private val idService: StudyReviewIdGeneratorService
diff --git a/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt b/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt
index e98f932f5..d72a0dc5c 100644
--- a/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt
+++ b/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt
@@ -164,6 +164,27 @@ class TestDataFactory {
QuestionContextEnum.ROB
)
+ fun generateRobQuestionNumberScaleDto(
+ questionId: UUID,
+ systematicStudyId: UUID = this.systematicStudyId,
+ code: String = faker.lorem.words(),
+ description: String = faker.lorem.words(),
+ lower: Int = 1,
+ higher: Int = 5
+ ) =
+ QuestionDocument(
+ questionId,
+ systematicStudyId,
+ code,
+ description,
+ "NUMBERED_SCALE",
+ null,
+ higher,
+ lower,
+ null,
+ QuestionContextEnum.ROB
+ )
+
fun generateExtractionQuestionTextualDto(
questionId: UUID,
systematicStudyId: UUID = this.systematicStudyId,
@@ -182,4 +203,24 @@ class TestDataFactory {
null,
QuestionContextEnum.EXTRACTION
)
+
+ fun generateExtractionQuestionPickListDto(
+ questionId: UUID,
+ systematicStudyId: UUID = this.systematicStudyId,
+ code: String = faker.lorem.words(),
+ description: String = faker.lorem.words(),
+ options: List = listOf("Option A", "Option B", "Option C")
+ ) =
+ QuestionDocument(
+ questionId,
+ systematicStudyId,
+ code,
+ description,
+ "PICK_LIST",
+ null,
+ null,
+ null,
+ options,
+ QuestionContextEnum.EXTRACTION
+ )
}
\ No newline at end of file