diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 200eae3fa..4da075285 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -1,6 +1,7 @@ + pickmany picoc scas diff --git a/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountService.kt b/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountService.kt index fb959eab1..36ba6130b 100644 --- a/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountService.kt +++ b/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountService.kt @@ -7,6 +7,7 @@ interface RegisterUserAccountService { fun register(presenter: RegisterUserAccountPresenter, request: RequestModel) data class RequestModel( + val name: String, val username: String, val password: String, val email: String, diff --git a/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountServiceImpl.kt b/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountServiceImpl.kt index 5fafd6c42..f18340148 100644 --- a/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountServiceImpl.kt +++ b/account/src/main/kotlin/br/all/application/user/create/RegisterUserAccountServiceImpl.kt @@ -6,6 +6,9 @@ import br.all.application.user.repository.UserAccountRepository import br.all.application.user.repository.toDto import br.all.domain.shared.exception.UniquenessViolationException import br.all.domain.shared.user.Email +import br.all.domain.shared.user.Name +import br.all.domain.shared.user.Text +import br.all.domain.shared.user.Username import br.all.domain.user.* class RegisterUserAccountServiceImpl(private val repository: UserAccountRepository) : RegisterUserAccountService { @@ -20,12 +23,14 @@ class RegisterUserAccountServiceImpl(private val repository: UserAccountReposito if (presenter.isDone()) return val userAccountId = UserAccountId() + val name = Name(request.name) val username = Username(request.username) val email = Email(request.email) val country = Text(request.country) val userAccount = UserAccount( id = userAccountId, + name = name, username = username, password = request.password, email = email, diff --git a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt index 2016e2c86..79c161926 100644 --- a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt +++ b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileService.kt @@ -10,6 +10,7 @@ interface RetrieveUserProfileService { data class ResponseModel( val userId: UUID, + val name: String, val username: String, val email: String, val affiliation: String, diff --git a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt index f382bc86a..5aeac3761 100644 --- a/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt +++ b/account/src/main/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImpl.kt @@ -26,6 +26,7 @@ class RetrieveUserProfileServiceImpl( val profile = ResponseModel( userId = userProfile.id, + name = userProfile.name, username = userCredentials.username, email = userProfile.email, affiliation = userProfile.affiliation, diff --git a/account/src/main/kotlin/br/all/application/user/repository/UserAccountDto.kt b/account/src/main/kotlin/br/all/application/user/repository/UserAccountDto.kt index c9939393f..11c14e9ef 100644 --- a/account/src/main/kotlin/br/all/application/user/repository/UserAccountDto.kt +++ b/account/src/main/kotlin/br/all/application/user/repository/UserAccountDto.kt @@ -5,6 +5,7 @@ import java.util.* data class UserAccountDto( val id: UUID, + val name: String, val username: String, val password: String, val email: String, diff --git a/account/src/main/kotlin/br/all/application/user/repository/UserAccountMapper.kt b/account/src/main/kotlin/br/all/application/user/repository/UserAccountMapper.kt index dcc0754db..0de5c8eeb 100644 --- a/account/src/main/kotlin/br/all/application/user/repository/UserAccountMapper.kt +++ b/account/src/main/kotlin/br/all/application/user/repository/UserAccountMapper.kt @@ -4,6 +4,7 @@ import br.all.domain.user.UserAccount fun UserAccount.toDto() = UserAccountDto( id.value(), + name.value, accountCredentials.username.value, accountCredentials.password, email.email, @@ -17,4 +18,3 @@ fun UserAccount.toDto() = UserAccountDto( accountCredentials.isCredentialsNonExpired, accountCredentials.isEnabled ) - diff --git a/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt b/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt index 1060041c4..0858c20d9 100644 --- a/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt +++ b/account/src/main/kotlin/br/all/application/user/repository/UserProfileDto.kt @@ -4,6 +4,7 @@ import java.util.UUID data class UserProfileDto( val id: UUID, + val name: String, val email: String, val country: String, val affiliation: String, diff --git a/account/src/main/kotlin/br/all/application/user/update/PatchUserProfilePresenter.kt b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfilePresenter.kt new file mode 100644 index 000000000..3e93382e5 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfilePresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.user.update + +import br.all.domain.shared.presenter.GenericPresenter +import br.all.application.user.update.PatchUserProfileService.ResponseModel + +interface PatchUserProfilePresenter : GenericPresenter \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileService.kt b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileService.kt new file mode 100644 index 000000000..9638c4a68 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileService.kt @@ -0,0 +1,26 @@ +package br.all.application.user.update + +import java.util.UUID + +interface PatchUserProfileService { + fun patchProfile(presenter: PatchUserProfilePresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val name: String?, + val username: String?, + val email: String?, + val affiliation: String?, + val country: String? + ) + + data class ResponseModel( + val userId: UUID, + val name: String, + val username: String, + val email: String, + val affiliation: String, + val country: String, + val invalidEntries: List + ) +} \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileServiceImpl.kt b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileServiceImpl.kt new file mode 100644 index 000000000..57905c917 --- /dev/null +++ b/account/src/main/kotlin/br/all/application/user/update/PatchUserProfileServiceImpl.kt @@ -0,0 +1,48 @@ +package br.all.application.user.update + +import br.all.application.user.repository.UserAccountRepository +import br.all.application.user.update.PatchUserProfileService.RequestModel +import br.all.application.user.update.PatchUserProfileService.ResponseModel +import br.all.domain.shared.user.Email +import br.all.domain.shared.user.Text +import br.all.domain.shared.user.Username + +class PatchUserProfileServiceImpl( + private val repository: UserAccountRepository +) : PatchUserProfileService { + override fun patchProfile( + presenter: PatchUserProfilePresenter, + request: RequestModel + ) { + if (repository.loadCredentialsById(request.userId) == null) { + presenter.prepareFailView(NoSuchElementException("User with id ${request.userId} not found!")) + } + + if (presenter.isDone()) return + +// val newUsername = Username(request.username) +// val newEmail = Email(request.email) +// val affiliation = Text(request.affiliation) +// val country = Text(request.country) + + // TODO(): add invalid entries to the response model array + // TODO(): update only the valid entries + + // data class RequestModel( + // val userId: UUID, + // val username: String, + // val email: String, + // val affiliation: String, + // val country: String + // ) + // + // data class ResponseModel( + // val userId: UUID, + // val username: String, + // val email: String, + // val affiliation: String, + // val country: String, + // val invalidEntries: List + // ) + } +} \ No newline at end of file diff --git a/account/src/main/kotlin/br/all/domain/user/AccountCredentials.kt b/account/src/main/kotlin/br/all/domain/user/AccountCredentials.kt index 07f2379a8..36c62af8a 100644 --- a/account/src/main/kotlin/br/all/domain/user/AccountCredentials.kt +++ b/account/src/main/kotlin/br/all/domain/user/AccountCredentials.kt @@ -1,5 +1,7 @@ package br.all.domain.user +import br.all.domain.shared.user.Username + class AccountCredentials ( val username: Username, val password: String, diff --git a/account/src/main/kotlin/br/all/domain/user/UserAccount.kt b/account/src/main/kotlin/br/all/domain/user/UserAccount.kt index 61ca66dc2..51184e8d9 100644 --- a/account/src/main/kotlin/br/all/domain/user/UserAccount.kt +++ b/account/src/main/kotlin/br/all/domain/user/UserAccount.kt @@ -2,11 +2,15 @@ package br.all.domain.user import br.all.domain.shared.ddd.Entity import br.all.domain.shared.user.Email +import br.all.domain.shared.user.Name +import br.all.domain.shared.user.Text +import br.all.domain.shared.user.Username import java.time.LocalDateTime import java.util.UUID class UserAccount( id: UserAccountId, + var name: Name, val createdAt: LocalDateTime = LocalDateTime.now(), var email: Email, var country: Text, diff --git a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountEntity.kt b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountEntity.kt index eaed0a0e7..9a35c8a5e 100644 --- a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountEntity.kt +++ b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountEntity.kt @@ -8,6 +8,8 @@ import java.util.* @Table(name = "USER_ACCOUNTS") class UserAccountEntity ( @Id var id: UUID, + @Column(nullable = false) + var name: String, @OneToOne( mappedBy = "userAccount", cascade = [CascadeType.ALL]) @PrimaryKeyJoinColumn var accountCredentialsEntity: AccountCredentialsEntity, diff --git a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt index 29982ff06..bdba1744c 100644 --- a/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt +++ b/account/src/main/kotlin/br/all/infrastructure/user/UserAccountMapper.kt @@ -16,13 +16,14 @@ fun UserAccountDto.toUserAccountEntity(): UserAccountEntity { isCredentialsNonExpired, isEnabled ) - return UserAccountEntity(id, credentials, email, country, affiliation, createdAt) + return UserAccountEntity(id, name, credentials, email, country, affiliation, createdAt) } fun AccountCredentialsEntity.toAccountCredentialsDto() = AccountCredentialsDto(id, username, password, authorities, refreshToken) fun UserAccountEntity.toUserProfileDto() = UserProfileDto( id = this.id, + name = this.name, email = this.email, country = this.country, affiliation = this.affiliation diff --git a/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt b/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt index 3b3ebc4ba..202e3b444 100644 --- a/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt +++ b/account/src/test/kotlin/br/all/application/user/find/RetrieveUserProfileServiceImplTest.kt @@ -56,6 +56,7 @@ class RetrieveUserProfileServiceImplTest { val capturedResponse = responseSlot.captured assertEquals(userProfile.id, capturedResponse.userId) + assertEquals(userProfile.name, capturedResponse.name) assertEquals(userCredentials.username, capturedResponse.username) assertEquals(userProfile.email, capturedResponse.email) assertEquals(userProfile.affiliation, capturedResponse.affiliation) diff --git a/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt b/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt index 4bf2c37c3..4315ef03b 100644 --- a/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt +++ b/account/src/test/kotlin/br/all/application/user/utils/TestDataFactory.kt @@ -14,7 +14,8 @@ class TestDataFactory { password = faker.pearlJam.songs(), email = faker.internet.email(), country = faker.address.countryCode(), - affiliation = faker.lorem.words() + affiliation = faker.lorem.words(), + name = faker.name.neutralFirstName() ) fun accountCredentials() @@ -31,5 +32,6 @@ class TestDataFactory { email = faker.internet.email(), country = faker.address.countryCode(), affiliation = faker.leagueOfLegends.rank(), + name = faker.name.firstName() ) } \ No newline at end of file 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 1483e8d4f..29edabbc4 100644 --- a/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt +++ b/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt @@ -1,6 +1,9 @@ package br.all.domain.user import br.all.domain.shared.user.Email +import br.all.domain.shared.user.Name +import br.all.domain.shared.user.Text +import br.all.domain.shared.user.Username import io.github.serpro69.kfaker.Faker import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Tag @@ -53,6 +56,8 @@ class UserAccountTest{ Assertions.assertTrue(user.accountCredentials.isEnabled) } + // Although the user won't be able to change their username through the front-end, + // I think we should still let this test here. @Test fun `should change username in account credentials`(){ val user = createUser() @@ -71,6 +76,7 @@ class UserAccountTest{ private fun createUser( id: UUID = UUID.randomUUID(), + name: Name = Name(faker.name.name()), createdAt: LocalDateTime = LocalDateTime.now(), email: Email = Email(faker.internet.email()), country: Text = Text(faker.address.countryCode()), @@ -78,9 +84,10 @@ class UserAccountTest{ username: Username = Username(faker.name.firstName()), password: String = faker.pearlJam.songs(), authorities: Set = setOf(Authority.USER) - ) = UserAccount( - UserAccountId(id), - createdAt, email, country, affiliation, username, password, authorities + ) = UserAccount( + id = UserAccountId(id), + name = name, + createdAt = createdAt, email = email, country = country, affiliation = affiliation, username = username, password = password, authorities = authorities ) } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageService.kt b/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageService.kt index 09966a5e0..bc30d4088 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageService.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageService.kt @@ -19,14 +19,18 @@ interface FindProtocolStageService { ) enum class ProtocolStage { - PROTOCOL_PART_I, + GENERAL_DEFINITION, + RESEARCH_QUESTIONS, PICOC, - PROTOCOL_PART_II, - PROTOCOL_PART_III, + ELIGIBILITY_CRITERIA, + INFORMATION_SOURCES_AND_SEARCH_STRATEGY, + SELECTION_AND_EXTRACTION, + RISK_OF_BIAS, + ANALYSIS_AND_SYNTHESIS_METHOD, + IDENTIFICATION, SELECTION, EXTRACTION, - GRAPHICS, - FINALIZATION + GRAPHICS } } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImpl.kt index c900bf90f..7eaaf15cd 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImpl.kt @@ -8,6 +8,7 @@ import br.all.application.protocol.find.FindProtocolStageService.ResponseModel import br.all.application.protocol.find.FindProtocolStageService.ProtocolStage import br.all.application.protocol.repository.ProtocolDto import br.all.application.question.repository.QuestionRepository +import br.all.application.review.repository.SystematicStudyDto import br.all.application.review.repository.fromDto import br.all.domain.shared.exception.EntityNotFoundException import br.all.application.shared.presenter.prepareIfFailsPreconditions @@ -27,40 +28,41 @@ class FindProtocolStageServiceImpl( override fun getStage(presenter: FindProtocolStagePresenter, request: RequestModel) { val user = credentialsService.loadCredentials(request.userId)?.toUser() val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) - val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + val systematicStudy = runCatching { + systematicStudyDto?.let { SystematicStudy.fromDto(it) } + }.getOrNull() 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}" + if (protocolDto == null || systematicStudyDto == null) { + val message = "Protocol or Systematic Study not found for id ${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 robQuestionsCount = questionRepository.findAllBySystematicStudyId(systematicStudyDto.id, QuestionContextEnum.ROB).size + val extractionQuestionsCount = questionRepository.findAllBySystematicStudyId(systematicStudyDto.id, QuestionContextEnum.EXTRACTION).size val stage = evaluateStage( protocolDto, - totalStudiesCount, - includedStudiesCount, - extractedStudiesCount, - robQuestions, - extractionQuestions, + systematicStudyDto, + allStudies.size, + allStudies.count { it.selectionStatus == "INCLUDED" }, + allStudies.count { it.extractionStatus == "INCLUDED" }, + robQuestionsCount, + extractionQuestionsCount ) presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) } private fun evaluateStage( - dto: ProtocolDto, + protocolDto: ProtocolDto, + studyDto: SystematicStudyDto, totalStudiesCount: Int, includedStudiesCount: Int, extractedStudiesCount: Int, @@ -68,10 +70,17 @@ class FindProtocolStageServiceImpl( 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 + !isT1Complete(studyDto, protocolDto) -> ProtocolStage.GENERAL_DEFINITION + !isT2Complete(protocolDto) -> ProtocolStage.RESEARCH_QUESTIONS + + isPicocStarted(protocolDto) && !isT3Complete(protocolDto) -> ProtocolStage.PICOC + + !isT4Complete(protocolDto) -> ProtocolStage.ELIGIBILITY_CRITERIA + !isT5Complete(protocolDto) -> ProtocolStage.INFORMATION_SOURCES_AND_SEARCH_STRATEGY + !isT6Complete(protocolDto, extractionQuestionsCount) -> ProtocolStage.SELECTION_AND_EXTRACTION + + !isT8Complete(protocolDto) -> ProtocolStage.ANALYSIS_AND_SYNTHESIS_METHOD + totalStudiesCount == 0 -> ProtocolStage.IDENTIFICATION includedStudiesCount == 0 -> ProtocolStage.SELECTION extractedStudiesCount == 0 -> ProtocolStage.EXTRACTION @@ -79,51 +88,62 @@ class FindProtocolStageServiceImpl( } } - 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 isT1Complete(studyDto: SystematicStudyDto, protocolDto: ProtocolDto): Boolean { + val isStudyInfoComplete = studyDto.title.isNotBlank() && studyDto.description.isNotBlank() + val isProtocolGoalComplete = !protocolDto.goal.isNullOrBlank() + return isStudyInfoComplete && isProtocolGoalComplete } - 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) } + private fun isT2Complete(dto: ProtocolDto): Boolean { + return dto.researchQuestions.isNotEmpty() + } - val hasExtractionAndRob = robQuestionCount > 0 && extractionQuestionsCount > 0 + private fun isPicocStarted(dto: ProtocolDto): Boolean { + val picoc = dto.picoc ?: return false + return !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() || + !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank() + } - val hasDatabases = dto.informationSources.isNotEmpty() - val hasResearchQuestions = dto.researchQuestions.isNotEmpty() - val hasAnalysisProcess = !dto.analysisAndSynthesisProcess.isNullOrBlank() + private fun isT3Complete(dto: ProtocolDto): Boolean { + val picoc = dto.picoc ?: return false - return hasInclusionCriteria && hasExclusionCriteria && - hasExtractionAndRob && hasDatabases && - hasResearchQuestions && hasAnalysisProcess + return !picoc.population.isNullOrBlank() && + !picoc.intervention.isNullOrBlank() && + !picoc.control.isNullOrBlank() && + !picoc.outcome.isNullOrBlank() } - private fun picocStage(dto: ProtocolDto): Boolean { - val picoc = dto.picoc - if (picoc == null) return false + private fun isT4Complete(dto: ProtocolDto): Boolean { + val hasInclusion = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", ignoreCase = true) } + val hasExclusion = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", ignoreCase = true) } - val picocIsStarted = !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() || - !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank() + val hasStudyType = !dto.studyTypeDefinition.isNullOrBlank() + val hasLanguage = dto.studiesLanguages.isNotEmpty() - if (picocIsStarted) { - val picocIsCompleted = !picoc.population.isNullOrBlank() && !picoc.intervention.isNullOrBlank() && - !picoc.control.isNullOrBlank() && !picoc.outcome.isNullOrBlank() && !picoc.context.isNullOrBlank() + return hasInclusion && hasExclusion && hasStudyType && hasLanguage + } - if (!picocIsCompleted) { - return true - } - } + private fun isT5Complete(dto: ProtocolDto): Boolean { + return !dto.sourcesSelectionCriteria.isNullOrBlank() && + dto.informationSources.isNotEmpty() && + !dto.searchMethod.isNullOrBlank() && + dto.keywords.isNotEmpty() && + !dto.searchString.isNullOrBlank() + } + + private fun isT6Complete(dto: ProtocolDto, extractionQuestionsCount: Int): Boolean { + return !dto.selectionProcess.isNullOrBlank() && + !dto.dataCollectionProcess.isNullOrBlank() && + extractionQuestionsCount > 0 + } + + // T7 - Risk Of Bias + // Não é necessária uma função 'isT7Complete' na lógica principal, pois + // a regra é: se não houver questões, o sistema avança para T8. + // A presença de questões (robQuestionCount > 0) simplesmente marca a tarefa como feita. - return false + private fun isT8Complete(dto: ProtocolDto): Boolean { + return !dto.analysisAndSynthesisProcess.isNullOrBlank() } -} +} \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/question/create/CreateQuestionService.kt b/review/src/main/kotlin/br/all/application/question/create/CreateQuestionService.kt index 67a344d61..29c77ec1f 100644 --- a/review/src/main/kotlin/br/all/application/question/create/CreateQuestionService.kt +++ b/review/src/main/kotlin/br/all/application/question/create/CreateQuestionService.kt @@ -5,7 +5,7 @@ import java.util.* interface CreateQuestionService { fun create(presenter: CreateQuestionPresenter, request: RequestModel) - enum class QuestionType{TEXTUAL, PICK_LIST, NUMBERED_SCALE, LABELED_SCALE} + enum class QuestionType{TEXTUAL, PICK_LIST, NUMBERED_SCALE, LABELED_SCALE, PICK_MANY} data class RequestModel( val userId: UUID, diff --git a/review/src/main/kotlin/br/all/application/question/create/CreateQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/question/create/CreateQuestionServiceImpl.kt index 657309808..f1909fb52 100644 --- a/review/src/main/kotlin/br/all/application/question/create/CreateQuestionServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/question/create/CreateQuestionServiceImpl.kt @@ -37,7 +37,7 @@ class CreateQuestionServiceImpl( val type = request.questionType - if (type == PICK_LIST && request.options.isNullOrEmpty()) { + if ((type == PICK_LIST || type == PICK_MANY) && request.options.isNullOrEmpty()) { presenter.prepareFailView(IllegalArgumentException("Options must not be null or empty.")) return } @@ -54,14 +54,20 @@ class CreateQuestionServiceImpl( val questionId = QuestionId(generatedId) val builder = QuestionBuilder.with(questionId, SystematicStudyId(systematicStudyId), request.code, request.description) - val question = when (type) { - TEXTUAL -> builder.buildTextual() - PICK_LIST -> builder.buildPickList(request.options!!) - NUMBERED_SCALE -> builder.buildNumberScale(request.lower!!, request.higher!!) - LABELED_SCALE -> builder.buildLabeledScale(request.scales!!) - } - questionRepository.createOrUpdate(question.toDto(type, request.questionContext)) + try { + val question = when (type) { + TEXTUAL -> builder.buildTextual() + PICK_LIST -> builder.buildPickList(request.options!!) + PICK_MANY -> builder.buildPickMany(request.options!!) + NUMBERED_SCALE -> builder.buildNumberScale(request.lower!!, request.higher!!) + LABELED_SCALE -> builder.buildLabeledScale(request.scales!!) + } + questionRepository.createOrUpdate(question.toDto(type, request.questionContext)) + } catch (e: IllegalArgumentException) { + presenter.prepareFailView(e) + return + } presenter.prepareSuccessView(ResponseModel(request.userId, systematicStudyId, generatedId)) } diff --git a/review/src/main/kotlin/br/all/application/question/repository/QuestionMapper.kt b/review/src/main/kotlin/br/all/application/question/repository/QuestionMapper.kt index bde5cb447..b40bfe219 100644 --- a/review/src/main/kotlin/br/all/application/question/repository/QuestionMapper.kt +++ b/review/src/main/kotlin/br/all/application/question/repository/QuestionMapper.kt @@ -12,7 +12,7 @@ fun Question<*>.toDto(type: QuestionType, context: String) = QuestionDto( (this as? LabeledScale)?.scales?.mapValues { it.value.value }, (this as? NumberScale)?.higher, (this as? NumberScale)?.lower, - (this as? PickList)?.options, + (this as? PickList)?.options ?: (this as? PickMany)?.options, context = QuestionContextEnum.valueOf(context) ) @@ -26,6 +26,7 @@ fun Question.Companion.fromDto(dto: QuestionDto): Question<*> { return when { dto.questionType == "PICK_LIST" && dto.options != null -> builder.buildPickList(dto.options) + dto.questionType == "PICK_MANY" && dto.options != null -> builder.buildPickMany(dto.options) dto.questionType == "NUMBERED_SCALE" && dto.higher != null && dto.lower != null -> builder.buildNumberScale(dto.lower, dto.higher) @@ -36,4 +37,3 @@ fun Question.Companion.fromDto(dto: QuestionDto): Question<*> { println("Loaded question with context: ${dto.context}") } } - diff --git a/review/src/main/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImpl.kt index 4b3bcd8bf..4bbe513ed 100644 --- a/review/src/main/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImpl.kt @@ -1,6 +1,8 @@ package br.all.application.question.update.services import br.all.application.question.create.CreateQuestionService.QuestionType +import br.all.application.question.create.CreateQuestionService.QuestionType.PICK_LIST +import br.all.application.question.create.CreateQuestionService.QuestionType.PICK_MANY import br.all.application.question.repository.QuestionRepository import br.all.application.question.repository.toDto import br.all.application.question.update.presenter.UpdateQuestionPresenter @@ -31,7 +33,7 @@ class UpdateQuestionServiceImpl( val type = request.questionType - if (type == QuestionType.PICK_LIST && request.options.isNullOrEmpty()) { + if ((type == PICK_LIST || type == PICK_MANY) && request.options.isNullOrEmpty()) { presenter.prepareFailView(IllegalArgumentException("Options must not be null or empty.")) return } @@ -47,14 +49,21 @@ class UpdateQuestionServiceImpl( val questionId = QuestionId(request.questionId) val builder = QuestionBuilder.with(questionId, SystematicStudyId(systematicStudyId), request.code, request.description) - val question = when (request.questionType) { - QuestionType.TEXTUAL -> builder.buildTextual() - QuestionType.PICK_LIST -> builder.buildPickList(request.options!!) - QuestionType.NUMBERED_SCALE -> builder.buildNumberScale(request.lower!!, request.higher!!) - QuestionType.LABELED_SCALE -> builder.buildLabeledScale(request.scales!!) + + try { + val question = when (request.questionType) { + QuestionType.TEXTUAL -> builder.buildTextual() + QuestionType.PICK_LIST -> builder.buildPickList(request.options!!) + QuestionType.PICK_MANY -> builder.buildPickMany(request.options!!) + QuestionType.NUMBERED_SCALE -> builder.buildNumberScale(request.lower!!, request.higher!!) + QuestionType.LABELED_SCALE -> builder.buildLabeledScale(request.scales!!) + } + questionRepository.createOrUpdate(question.toDto(type, request.questionContext)) + } catch (e: IllegalArgumentException) { + presenter.prepareFailView(e) + return } - questionRepository.createOrUpdate(question.toDto(type, request.questionContext)) presenter.prepareSuccessView(ResponseModel(userId, systematicStudyId, questionId.value)) } } 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 index 640a8a209..6de203d19 100644 --- 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 @@ -21,6 +21,7 @@ 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.PickMany import br.all.domain.model.question.Question import br.all.domain.model.question.QuestionContextEnum import br.all.domain.model.question.Textual @@ -114,6 +115,12 @@ class BatchAnswerQuestionServiceImpl( 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 == "PICK_MANY" && detail.answer is List<*> -> { + val stringList = detail.answer.map { + it as? String ?: throw IllegalArgumentException("All items in the answer list for PICK_MANY must be strings.") + } + (question as PickMany).answer(stringList) + } questionType == "NUMBERED_SCALE" && detail.answer is Int -> (question as NumberScale).answer(detail.answer) questionType == "LABELED_SCALE" -> { when (val answer = detail.answer) { diff --git a/review/src/main/kotlin/br/all/domain/model/question/PickMany.kt b/review/src/main/kotlin/br/all/domain/model/question/PickMany.kt new file mode 100644 index 000000000..00db9301b --- /dev/null +++ b/review/src/main/kotlin/br/all/domain/model/question/PickMany.kt @@ -0,0 +1,43 @@ +package br.all.domain.model.question + +import br.all.domain.model.review.SystematicStudyId +import br.all.domain.model.study.Answer +import br.all.domain.shared.ddd.Notification + +class PickMany( + id: QuestionId, + systematicStudyId: SystematicStudyId, + code: String, + description: String, + val options: List +) : Question>(id, systematicStudyId, code, description) { + + init { + val notification = validate() + require(notification.hasNoErrors()) { notification.message() } + } + + fun validate(): Notification { + val notification = Notification() + if (options.isEmpty()) + notification.addError("Can not create a picklist without a option to pick.") + + options.forEachIndexed { index, option -> + if (option.isBlank()) notification.addError("Option at index $index is empty or blank.") + } + return notification + } + + override fun answer(value: List): Answer> { + if (value.isEmpty()) throw IllegalArgumentException("Answer must not be empty.") + value.forEach { + if (it.isBlank()) throw IllegalArgumentException("Answer must not be blank.") + if (it !in options) throw IllegalArgumentException("Answer must be one of the valid options: $options") + } + return Answer(id.value(), value) + } + + override fun toString() = + "PickMany(QuestionId: $id, ProtocolId: $systematicStudyId, Code: $code, " + + "Description: $description, Options: $options)" +} \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/domain/model/question/QuestionBuilder.kt b/review/src/main/kotlin/br/all/domain/model/question/QuestionBuilder.kt index 4683ffc60..6f7a77466 100644 --- a/review/src/main/kotlin/br/all/domain/model/question/QuestionBuilder.kt +++ b/review/src/main/kotlin/br/all/domain/model/question/QuestionBuilder.kt @@ -24,6 +24,8 @@ class QuestionBuilder private constructor( fun buildNumberScale(lower: Int, higher: Int) = NumberScale(questionId, systematicStudyId, code, description, lower, higher) fun buildPickList(options: List) = PickList(questionId, systematicStudyId, code, description, options) + + fun buildPickMany(options: List) = PickMany(questionId, systematicStudyId, code, description, options) } diff --git a/review/src/main/kotlin/br/all/domain/model/study/Doi.kt b/review/src/main/kotlin/br/all/domain/model/study/Doi.kt index 175fe677f..fff9b1761 100644 --- a/review/src/main/kotlin/br/all/domain/model/study/Doi.kt +++ b/review/src/main/kotlin/br/all/domain/model/study/Doi.kt @@ -13,7 +13,7 @@ data class Doi(val value: String) : ValueObject() { override fun validate(): Notification { val notification = Notification() if (value.isBlank()) notification.addError("DOI must not be blank.") - val regex = Regex("^https://doi\\.org/10\\.\\d{4,}/[\\w.-]+\$") + val regex = Regex("^https?://[\\w.-]*doi[\\w.-]*\\.org/10\\.\\d{4,}/.+$") if (!value.matches(regex)) notification.addError("Wrong DOI format: $value") return notification } diff --git a/review/src/main/kotlin/br/all/domain/services/BibtexConverterService.kt b/review/src/main/kotlin/br/all/domain/services/BibtexConverterService.kt index 98575092a..02b3e7b78 100644 --- a/review/src/main/kotlin/br/all/domain/services/BibtexConverterService.kt +++ b/review/src/main/kotlin/br/all/domain/services/BibtexConverterService.kt @@ -2,8 +2,15 @@ package br.all.domain.services import br.all.domain.model.review.SystematicStudyId import br.all.domain.model.search.SearchSessionID -import br.all.domain.model.study.* -import java.util.* +import br.all.domain.model.study.Doi +import br.all.domain.model.study.ExtractionStatus +import br.all.domain.model.study.ReadingPriority +import br.all.domain.model.study.SelectionStatus +import br.all.domain.model.study.Study +import br.all.domain.model.study.StudyReview +import br.all.domain.model.study.StudyReviewId +import br.all.domain.model.study.StudyType +import java.util.Locale class BibtexConverterService(private val studyReviewIdGeneratorService: IdGeneratorService) { @@ -81,13 +88,17 @@ class BibtexConverterService(private val studyReviewIdGeneratorService: IdGenera val authors = getValueFromFieldMap(fieldMap, authorTypes) val venue = getValueFromFieldMap(fieldMap, venueTypes) val abstract = fieldMap["abstract"] ?: " " - val keywords = parseKeywords(fieldMap["keywords"]) + val keywords = parseKeywords(fieldMap["keywords"] ?: fieldMap["keyword"]) val references = parseReferences(fieldMap["references"]) val doi = fieldMap["doi"]?.let { - val cleanDoi = it.replace(Regex("}"), "") - Doi("https://doi.org/$cleanDoi") + val cleanDoi = it.replace(Regex("[{}]"), "").trim() + val fullUrl = if (cleanDoi.startsWith("http")) { + cleanDoi + } else { + "https://doi.org/$cleanDoi" + } + Doi(fullUrl) } - val type = extractStudyType(bibtexEntry) return Study(type, title, year, authors, venue, abstract, keywords, references, doi) @@ -123,7 +134,7 @@ class BibtexConverterService(private val studyReviewIdGeneratorService: IdGenera } private fun parseKeywords(keywords: String?): Set { - return keywords?.split(",")?.map { it.trim() }?.toSet() ?: emptySet() + return keywords?.split(",", ";")?.map { it.trim() }?.toSet() ?: emptySet() } private fun parseReferences(references: String?): List { diff --git a/review/src/main/kotlin/br/all/domain/shared/valueobject/Text.kt b/review/src/main/kotlin/br/all/domain/shared/valueobject/Text.kt deleted file mode 100644 index 0aebb109f..000000000 --- a/review/src/main/kotlin/br/all/domain/shared/valueobject/Text.kt +++ /dev/null @@ -1,29 +0,0 @@ -package br.all.domain.shared.valueobject - -import br.all.domain.shared.ddd.Notification -import br.all.domain.shared.ddd.ValueObject - -data class Text(val text: String) : ValueObject() { - - init { - val notification = validate() - require(notification.hasNoErrors()) { notification.message() } - } - - override fun validate(): Notification { - val notification = Notification() - - if (text.isEmpty()) notification.addError("Text must not be empty.") - if (text.isBlank()) notification.addError("Text must not be blank.") - - val pattern = Regex("^(?![!@#$%¨&*()_+='<>,.:;|/?`´^~{}\\[\\]\"-]+).*") - - - if(!pattern.matches(text)){ - notification.addError("Should be a valid text") - } - - return notification - } - -} \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/infrastructure/question/QuestionDbMapper.kt b/review/src/main/kotlin/br/all/infrastructure/question/QuestionDbMapper.kt index b0478ce56..4d275167d 100644 --- a/review/src/main/kotlin/br/all/infrastructure/question/QuestionDbMapper.kt +++ b/review/src/main/kotlin/br/all/infrastructure/question/QuestionDbMapper.kt @@ -3,27 +3,27 @@ package br.all.infrastructure.question import br.all.application.question.repository.QuestionDto fun QuestionDocument.toDto() = QuestionDto( - questionId, - systematicStudyId, - code, - description, - questionType, - scales, - higher, - lower, - options, - context + questionId = questionId, + systematicStudyId = systematicStudyId, + code = code, + description = description, + questionType = questionType, + scales = scales, + higher = higher, + lower = lower, + options = options, + context = context ) fun QuestionDto.toDocument() = QuestionDocument( - questionId, - systematicStudyId, - code, - description, - questionType, - scales, - higher, - lower, - options, - context + questionId = questionId, + systematicStudyId = systematicStudyId, + code = code, + description = description, + questionType = questionType, + scales = scales, + higher = higher, + lower = lower, + options = options, + context = context ) \ No newline at end of file diff --git a/review/src/test/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImplTest.kt index d803ac572..7b2fb2808 100644 --- a/review/src/test/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/protocol/find/FindProtocolStageServiceImplTest.kt @@ -21,10 +21,13 @@ import java.util.UUID import kotlin.test.Test import br.all.application.protocol.find.FindProtocolStageService.RequestModel import br.all.application.protocol.find.FindProtocolStageService.ResponseModel -import br.all.application.protocol.find.FindProtocolStageService.ProtocolStage +import br.all.application.protocol.find.FindProtocolStageService.ProtocolStage // Assuming this enum is updated import br.all.application.protocol.repository.CriterionDto import br.all.application.protocol.repository.PicocDto +import br.all.application.protocol.repository.ProtocolDto import br.all.application.question.repository.QuestionRepository +import br.all.application.review.repository.SystematicStudyDto +import br.all.application.study.repository.StudyReviewDto import br.all.domain.model.question.QuestionContextEnum import io.mockk.verify @@ -80,125 +83,118 @@ class FindProtocolStageServiceImplTest { ) precondition.makeEverythingWork() + + every { systematicStudyRepository.findById(systematicStudyId) } returns createFullSystematicStudyDto() + + every { studyReviewRepository.findAllFromReview(any()) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(any(), any()) } returns emptyList() } @Nested - @DisplayName("When successfully getting protocol's current stage") + @DisplayName("When getting protocol 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 - ) + fun `should return GENERAL_DEFINITION stage (T1) when systematic study name is blank`() { + val incompleteStudyDto = createFullSystematicStudyDto(title = "") + val protocolDto = protocolFactory.protocolDto(systematicStudy = systematicStudyId) + every { systematicStudyRepository.findById(systematicStudyId) } returns incompleteStudyDto 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, RequestModel(researcherId, systematicStudyId)) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.GENERAL_DEFINITION) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return RESEARCH_QUESTIONS stage (T2) when T1 is complete but no research questions exist`() { + val protocolDto = createFullProtocolDto(researchQuestions = emptySet()) - sut.getStage(presenter, request) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) - val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_I) + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.RESEARCH_QUESTIONS) 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 - ) + fun `should return PICOC stage (T3) when it is started but not complete`() { + val protocolDto = createFullProtocolDto( + picoc = PicocDto(population = "P", intervention = "I", 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) 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() + fun `should return ELIGIBILITY_CRITERIA stage (T4) when T3 (PICOC) is skipped and T4 is incomplete`() { + val protocolDto = createFullProtocolDto( + picoc = PicocDto(null, null, null, null, null), + eligibilityCriteria = 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) - val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_II) + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.ELIGIBILITY_CRITERIA) 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() - ) + fun `should return INFORMATION_SOURCES_AND_SEARCH_STRATEGY stage (T5) when T4 is complete but T5 is not`() { + val protocolDto = createFullProtocolDto(searchString = "") every { protocolRepository.findById(systematicStudyId) } returns protocolDto - every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() - every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList() + + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.INFORMATION_SOURCES_AND_SEARCH_STRATEGY) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return SELECTION_AND_EXTRACTION stage (T6) when T5 is complete but T6 is not (no extraction questions)`() { + val protocolDto = createFullProtocolDto() + every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList() - val request = RequestModel(researcherId, systematicStudyId) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.SELECTION_AND_EXTRACTION) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return ANALYSIS_AND_SYNTHESIS_METHOD stage (T8) when T6 is complete but T8 is not`() { + val protocolDto = createFullProtocolDto(analysisAndSynthesisProcess = "") + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns listOf(questionFactory.generateTextualDto()) + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList() - sut.getStage(presenter, request) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) - val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_III) + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.ANALYSIS_AND_SYNTHESIS_METHOD) verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } } @Test - fun `should return IDENTIFICATION stage when no studies have been submitted`() { + fun `should return IDENTIFICATION stage when protocol is complete and no studies are imported`() { val protocolDto = createFullProtocolDto() - val questions = listOf( - questionFactory.generateTextualDto() - ) - every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns listOf(questionFactory.generateTextualDto()) + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns listOf(questionFactory.generateTextualDto()) 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.IDENTIFICATION) verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } @@ -207,116 +203,108 @@ class FindProtocolStageServiceImplTest { @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() - ) - + val studies = listOf(createFullStudyReviewDto(selectionStatus = "EXCLUDED")) every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns listOf(questionFactory.generateTextualDto()) + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns listOf(questionFactory.generateTextualDto()) 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) 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`() { + fun `should return EXTRACTION stage when studies are included but not extracted`() { val protocolDto = createFullProtocolDto() - val studies = listOf( - createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "") - ) - val questions = listOf( - questionFactory.generateTextualDto() - ) - + val studies = listOf(createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "PENDING")) every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns listOf(questionFactory.generateTextualDto()) + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns listOf(questionFactory.generateTextualDto()) 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.EXTRACTION) verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } } @Test - fun `should return GRAPHICS stage when studies have been extracted`() { + fun `should return GRAPHICS stage when at least one study has been fully extracted`() { val protocolDto = createFullProtocolDto() - val studies = listOf( - createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "INCLUDED") - ) - val questions = listOf( - questionFactory.generateTextualDto() - ) - + val studies = listOf(createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "INCLUDED")) every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns listOf(questionFactory.generateTextualDto()) + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns listOf(questionFactory.generateTextualDto()) 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) + sut.getStage(presenter, RequestModel(researcherId, systematicStudyId)) val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.GRAPHICS) verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } } } + private fun createFullSystematicStudyDto( + id: UUID = systematicStudyId, + title: String = "A complete systematic study", + description: String = "A complete description" + ) = SystematicStudyDto( + id = id, + title = title, + description = description, + owner = UUID.randomUUID(), + collaborators = emptySet() + ) - 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") + private fun createFullProtocolDto( + goal: String? = "A complete goal", + researchQuestions: Set = setOf("RQ1?"), + picoc: PicocDto? = PicocDto("P", "I", "C", "O", "Context"), + eligibilityCriteria: Set = setOf( + CriterionDto("Inclusion", "INCLUSION"), + CriterionDto("Exclusion", "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" - ) + studyTypeDefinition: String? = "Randomized Controlled Trial", + studiesLanguages: Set = setOf("English"), + sourcesSelectionCriteria: String? = "Selection criteria", + informationSources: Set = setOf("Scopus"), + searchMethod: String? = "A valid search method", + keywords: Set = setOf("keyword1"), + searchString: String? = "((keyword1) AND (keyword2))", + selectionProcess: String? = "A valid selection process", + dataCollectionProcess: String? = "A valid data collection process", + analysisAndSynthesisProcess: String? = "A final analysis process" + ): ProtocolDto { + val baseDto = protocolFactory.protocolDto( + systematicStudy = systematicStudyId, + goal = goal, + justification = "A justification", + researchQuestions = researchQuestions, + picoc = picoc, + eligibilityCriteria = eligibilityCriteria, + studiesLanguages = studiesLanguages, + sourcesSelectionCriteria = sourcesSelectionCriteria, + informationSources = informationSources, + searchMethod = searchMethod, + keywords = keywords, + selectionProcess = selectionProcess, + analysisAndSynthesisProcess = analysisAndSynthesisProcess + ) + return baseDto.copy( + studyTypeDefinition = studyTypeDefinition, + searchString = searchString, + dataCollectionProcess = dataCollectionProcess + ) + } - private fun createFullStudyReviewDto(selectionStatus: String, extractionStatus: String) = studyReviewFactory.generateDto( - studyReviewId = 1L, + private fun createFullStudyReviewDto( + selectionStatus: String, + extractionStatus: String = "PENDING" + ) = studyReviewFactory.generateDto( 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 + extractionStatus = extractionStatus, ) -} +} \ No newline at end of file diff --git a/review/src/test/kotlin/br/all/application/question/create/CreateQuestionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/question/create/CreateQuestionServiceImplTest.kt index c4b539262..3d8b889cf 100644 --- a/review/src/test/kotlin/br/all/application/question/create/CreateQuestionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/question/create/CreateQuestionServiceImplTest.kt @@ -64,6 +64,7 @@ class CreateQuestionServiceImplTest { TEXTUAL -> factory.createTextualRequestModel() NUMBERED_SCALE -> factory.createNumberedScaleRequestModel() PICK_LIST -> factory.createPickListRequestModel() + PICK_MANY -> factory.createPickManyRequestModel() LABELED_SCALE -> factory.createLabeledScaleRequestModel() } @@ -104,6 +105,36 @@ class CreateQuestionServiceImplTest { } } + @Test + fun `should not be able to create numbered-scale if lower boundary is greater than higher boundary`() { + val request = factory.createNumberedScaleRequestModel(lower = 10, higher = 5) + preconditionCheckerMocking.makeEverythingWork() + every { uuidGeneratorService.next() } returns factory.question + + sut.create(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @ParameterizedTest + @EnumSource(value = QuestionType::class, names = ["PICK_LIST", "PICK_MANY"]) + fun `should not be able to create a question with a blank option in the list`(questionType: QuestionType) { + val blankOptions = listOf("Valid Option 1", " ", "Valid Option 2") + val request = when (questionType) { + PICK_LIST -> factory.createPickListRequestModel(options = blankOptions) + PICK_MANY -> factory.createPickManyRequestModel(options = blankOptions) + else -> throw IllegalStateException("Test configuration error") + } + preconditionCheckerMocking.makeEverythingWork() + every { uuidGeneratorService.next() } returns factory.question + + sut.create(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + @Test fun `should not be able to create picklist question if options is empty`() { val request = factory.createPickListRequestModel(options = emptyList()) @@ -119,6 +150,21 @@ class CreateQuestionServiceImplTest { } } + @Test + fun `should not be able to create pickmany question if options is empty`() { + val request = factory.createPickManyRequestModel(options = emptyList()) + val (_, _, question) = factory + + every { uuidGeneratorService.next() } returns question + preconditionCheckerMocking.makeEverythingWork() + + sut.create(presenter, request) + verify { + presenter.isDone() + presenter.prepareFailView(any()) + } + } + @Test fun `should not be able to create labeledScale question if scales is empty`() { val request = factory.createLabeledScaleRequestModel(scales = emptyMap()) diff --git a/review/src/test/kotlin/br/all/application/question/find/FindQuestionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/question/find/FindQuestionServiceImplTest.kt index 1f09a1877..05a874799 100644 --- a/review/src/test/kotlin/br/all/application/question/find/FindQuestionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/question/find/FindQuestionServiceImplTest.kt @@ -62,6 +62,7 @@ class FindQuestionServiceImplTest { QuestionType.TEXTUAL -> factory.findOneTextualResponseModel() QuestionType.NUMBERED_SCALE -> factory.findOneNumberedScaleResponseModel() QuestionType.PICK_LIST -> factory.findOnePickListResponseModel() + QuestionType.PICK_MANY -> factory.findOnePickManyResponseModel() QuestionType.LABELED_SCALE -> factory.findOneLabeledScaleResponseModel() } diff --git a/review/src/test/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImplTest.kt index 17dd479eb..cdda6df44 100644 --- a/review/src/test/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/question/update/services/UpdateQuestionServiceImplTest.kt @@ -5,11 +5,8 @@ import br.all.application.question.create.CreateQuestionService.QuestionType.* import br.all.application.question.repository.QuestionRepository import br.all.application.question.update.presenter.UpdateQuestionPresenter import br.all.application.question.util.TestDataFactory -import br.all.application.user.credentials.ResearcherCredentialsService import br.all.application.review.repository.SystematicStudyRepository -import br.all.application.search.repository.SearchSessionRepository import br.all.application.user.CredentialsService -import br.all.application.util.PreconditionCheckerMocking import br.all.application.util.PreconditionCheckerMockingNew import br.all.domain.services.UuidGeneratorService import io.mockk.every @@ -20,7 +17,11 @@ import io.mockk.verify import org.junit.jupiter.api.* import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream +import kotlin.collections.emptyList @Tag("UnitTest") @Tag("ServiceTest") @@ -73,6 +74,7 @@ class UpdateQuestionServiceImplTest { TEXTUAL -> factory.generateTextualDto() NUMBERED_SCALE -> factory.generateNumberedScaleDto() PICK_LIST -> factory.generatePickListDto() + PICK_MANY -> factory.generatePickManyDto() LABELED_SCALE -> factory.generateLabeledScaleDto() } val updatedDto = dto.copy(code = "new code") @@ -95,6 +97,7 @@ class UpdateQuestionServiceImplTest { TEXTUAL -> factory.generateTextualDto() NUMBERED_SCALE -> factory.generateNumberedScaleDto() PICK_LIST -> factory.generatePickListDto() + PICK_MANY -> factory.generatePickManyDto() LABELED_SCALE -> factory.generateLabeledScaleDto() } val updatedDto = dto.copy(description = "new description") @@ -125,6 +128,22 @@ class UpdateQuestionServiceImplTest { } } + @Test + fun `should successfully update a pickmany question list of options`() { + val dto = factory.generatePickManyDto() + val updatedDto = dto.copy(options = listOf("new option 1", " new option 2")) + val request = factory.updateQuestionRequestModel(updatedDto, PICK_MANY) + val response = factory.updateQuestionResponseModel() + + every { repository.findById(factory.systematicStudy, factory.question) } returns dto + sut.update(presenter, request) + + verify { + repository.createOrUpdate(updatedDto) + presenter.prepareSuccessView(response) + } + } + @Test fun `should successfully update a labeled-scale question map of labels`() { val dto = factory.generateLabeledScaleDto() @@ -171,7 +190,93 @@ class UpdateQuestionServiceImplTest { @Nested @Tag("InvalidClasses") @DisplayName("When not able to update a question") - inner class WhenNotAbleToUpdateAQuestion{ + inner class WhenNotAbleToUpdateAQuestion { + @BeforeEach + fun setUp() = run { preconditionCheckerMocking.makeEverythingWork() } + + @ParameterizedTest(name = "should fail when updating a PICK_LIST with {0} options") + @MethodSource("br.all.application.question.update.services.UpdateQuestionServiceImplTest#provideNullAndEmptyLists") + fun `should fail when updating a PICK_LIST with null or empty options`(options: List?) { + val dto = factory.generatePickListDto().copy(options = options) + val request = factory.updateQuestionRequestModel(dto, PICK_LIST) + + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @ParameterizedTest(name = "should fail when updating a PICK_MANY with {0} options") + @MethodSource("br.all.application.question.update.services.UpdateQuestionServiceImplTest#provideNullAndEmptyLists") + fun `should fail when updating a PICK_MANY with null or empty options`(options: List?) { + val dto = factory.generatePickManyDto().copy(options = options) + val request = factory.updateQuestionRequestModel(dto, PICK_MANY) + + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @Test + fun `should fail when updating a LABELED_SCALE with a null map of scales`() { + val dto = factory.generateLabeledScaleDto().copy(scales = null) + val request = factory.updateQuestionRequestModel(dto, LABELED_SCALE) + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @Test + fun `should fail when updating a NUMBERED_SCALE with a null lower boundary`() { + val dto = factory.generateNumberedScaleDto().copy(lower = null) + val request = factory.updateQuestionRequestModel(dto, NUMBERED_SCALE) + + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @Test + fun `should fail when updating a NUMBERED_SCALE where lower boundary is greater than higher boundary`() { + val dto = factory.generateNumberedScaleDto().copy(lower = 10, higher = 5) + val request = factory.updateQuestionRequestModel(dto, NUMBERED_SCALE) + + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + + @ParameterizedTest + @EnumSource(value = QuestionType::class, names = ["PICK_LIST", "PICK_MANY"]) + @DisplayName("should fail when updating a PICK_LIST or PICK_MANY with a list with a blank option") + fun `should fail when an option in a list is blank`(questionType: QuestionType) { + val options = listOf("Valid Option", " ", "Another Valid") + val dto = when (questionType) { + PICK_LIST -> factory.generatePickListDto().copy(options = options) + PICK_MANY -> factory.generatePickManyDto().copy(options = options) + else -> throw IllegalStateException("Unsupported type for this test") + } + val request = factory.updateQuestionRequestModel(dto, questionType) + + sut.update(presenter, request) + + verify { presenter.prepareFailView(any()) } + verify(exactly = 0) { repository.createOrUpdate(any()) } + } + } + + companion object { + @JvmStatic + fun provideNullAndEmptyLists(): Stream { + return Stream.of( + Arguments.of(null), + Arguments.of(emptyList()), + ) + } } } \ No newline at end of file diff --git a/review/src/test/kotlin/br/all/application/question/util/TestDataFactory.kt b/review/src/test/kotlin/br/all/application/question/util/TestDataFactory.kt index f1bdb5c59..0e229a3c7 100644 --- a/review/src/test/kotlin/br/all/application/question/util/TestDataFactory.kt +++ b/review/src/test/kotlin/br/all/application/question/util/TestDataFactory.kt @@ -50,6 +50,22 @@ class TestDataFactory { context = QuestionContextEnum.EXTRACTION ) + fun generatePickManyDto( + questionId: UUID = question, + systematicStudyId: UUID = systematicStudy, + ) = QuestionDto( + questionId, + systematicStudyId, + code, + description, + "PICK_MANY", + null, + null, + null, + listOf(faker.lorem.words(), faker.lorem.words()), + context = QuestionContextEnum.EXTRACTION + ) + fun generateLabeledScaleDto( questionId: UUID = question, systematicStudyId: UUID = systematicStudy, @@ -139,6 +155,25 @@ class TestDataFactory { options ) + fun createPickManyRequestModel( + researcherId: UUID = researcher, + systematicStudyId: UUID = systematicStudy, + questionType: QuestionType = QuestionType.PICK_MANY, + options: List? = listOf(faker.lorem.words(), faker.lorem.words()), + context: String = QuestionContextEnum.EXTRACTION.toString(), + ) = RequestModel( + researcherId, + systematicStudyId, + context, + questionType, + code, + description, + null, + null, + null, + options + ) + fun createLabeledScaleRequestModel( researcherId: UUID = researcher, systematicStudyId: UUID = systematicStudy, @@ -202,6 +237,11 @@ class TestDataFactory { questionDto: QuestionDto = generatePickListDto() ) = Find.ResponseModel(researcherId, questionDto) + fun findOnePickManyResponseModel( + researcherId: UUID = this.researcher, + questionDto: QuestionDto = generatePickManyDto() + ) = Find.ResponseModel(researcherId, questionDto) + fun findOneLabeledScaleResponseModel( researcherId: UUID = this.researcher, questionDto: QuestionDto = generateLabeledScaleDto() 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 index c32915128..c8cd1ec5f 100644 --- a/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt @@ -107,6 +107,149 @@ class BatchAnswerQuestionServiceImplTest { } } + @Test + fun `should successfully answer a single pick-many question`() { + val reviewDto = factory.generateDto() + + val pickManyQId = UUID.randomUUID() + val pickManyOptions = listOf("Java", "Kotlin", "Go", "Rust") + val pickManyQDto = factory.generateQuestionPickManyDto(pickManyQId, factory.systematicStudyId, options = pickManyOptions, questionContext = "EXTRACTION") + val pickManyAnswer = factory.answerDetail(questionId = pickManyQId, type = "PICK_MANY", answer = listOf("Kotlin", "Rust")) + + val request = factory.batchAnswerRequest(listOf(pickManyAnswer)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), pickManyQId) } returns pickManyQDto + + sut.batchAnswerQuestion(presenter, request) + + verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) } + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(1, response.succeededAnswers.size) + assertTrue(response.failedAnswers.isEmpty()) + assertEquals(pickManyQId, response.succeededAnswers.first()) + }) + } + } + + @Test + fun `should successfully answer three pick-many questions together`() { + val reviewDto = factory.generateDto() + + val q1Id = UUID.randomUUID() + val q1Dto = factory.generateQuestionPickManyDto(q1Id, factory.systematicStudyId, options = listOf("A", "B", "C"), questionContext = "EXTRACTION") + val a1 = factory.answerDetail(q1Id, "PICK_MANY", listOf("A", "C")) + + val q2Id = UUID.randomUUID() + val q2Dto = factory.generateQuestionPickManyDto(q2Id, factory.systematicStudyId, options = listOf("X", "Y"), questionContext = "EXTRACTION") + val a2 = factory.answerDetail(q2Id, "PICK_MANY", listOf("Y")) + + val q3Id = UUID.randomUUID() + val q3Dto = factory.generateQuestionPickManyDto(q3Id, factory.systematicStudyId, options = listOf("1", "2", "3"), questionContext = "EXTRACTION") + val a3 = factory.answerDetail(q3Id, "PICK_MANY", listOf("1", "2", "3")) + + val request = factory.batchAnswerRequest(listOf(a1, a2, a3)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), q1Id) } returns q1Dto + every { questionRepository.findById(any(), q2Id) } returns q2Dto + every { questionRepository.findById(any(), q3Id) } returns q3Dto + + sut.batchAnswerQuestion(presenter, request) + + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(3, response.succeededAnswers.size) + assertTrue(response.failedAnswers.isEmpty()) + assertTrue(response.succeededAnswers.containsAll(listOf(q1Id, q2Id, q3Id))) + }) + } + } + + @Test + fun `should successfully answer all question types together in a single batch`() { + val reviewDto = factory.generateDto() + + val textualQId = UUID.randomUUID() + val textualQDto = factory.generateQuestionTextualDto(textualQId, factory.systematicStudyId, questionContext = "EXTRACTION") + val textualAnswer = factory.answerDetail(textualQId, "TEXTUAL", "Comprehensive answer") + + val pickListQId = UUID.randomUUID() + val pickListQDto = factory.generateQuestionPickListDto(pickListQId, factory.systematicStudyId, options = listOf("Yes", "No"), questionContext = "EXTRACTION") + val pickListAnswer = factory.answerDetail(pickListQId, "PICK_LIST", "Yes") + + val pickManyQId = UUID.randomUUID() + val pickManyQDto = factory.generateQuestionPickManyDto(pickManyQId, factory.systematicStudyId, options = listOf("High", "Medium", "Low"), questionContext = "EXTRACTION") + val pickManyAnswer = factory.answerDetail(pickManyQId, "PICK_MANY", listOf("High", "Low")) + + val numberedQId = UUID.randomUUID() + val numberedQDto = factory.generateQuestionNumberedScaleDto(numberedQId, factory.systematicStudyId, higher = 10, lower = 0, questionContext = "ROB") + val numberedAnswer = factory.answerDetail(numberedQId, "NUMBERED_SCALE", 7) + + val labeledScaleQId = UUID.randomUUID() + val labelDto = BatchAnswerQuestionService.LabelDto("Critical", 5) + val labeledScaleQDto = factory.generateQuestionLabeledScaleDto(labeledScaleQId, factory.systematicStudyId, labelDto = labelDto, questionContext = "ROB") + val labeledScaleAnswer = factory.answerDetail(labeledScaleQId, "LABELED_SCALE", labelDto) + + val request = factory.batchAnswerRequest(listOf(textualAnswer, pickListAnswer, pickManyAnswer, numberedAnswer, labeledScaleAnswer)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), textualQId) } returns textualQDto + every { questionRepository.findById(any(), pickListQId) } returns pickListQDto + every { questionRepository.findById(any(), pickManyQId) } returns pickManyQDto + every { questionRepository.findById(any(), numberedQId) } returns numberedQDto + every { questionRepository.findById(any(), labeledScaleQId) } returns labeledScaleQDto + + sut.batchAnswerQuestion(presenter, request) + + verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) } + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(5, response.totalAnswered) + assertTrue(response.failedAnswers.isEmpty()) + val expectedIds = listOf(textualQId, pickListQId, pickManyQId, numberedQId, labeledScaleQId) + assertTrue(response.succeededAnswers.containsAll(expectedIds)) + }) + } + } + + @Test + fun `should successfully answer pick-many and labeled-scale questions`() { + val reviewDto = factory.generateDto() + + val pickManyQId = UUID.randomUUID() + val pickManyOptions = listOf("Option 1", "Option 2", "Option 3") + val pickManyQDto = factory.generateQuestionPickManyDto(pickManyQId, factory.systematicStudyId, options = pickManyOptions, questionContext = "EXTRACTION") + val pickManyAnswer = factory.answerDetail(questionId = pickManyQId, type = "PICK_MANY", answer = listOf("Option 1", "Option 3")) + + val labeledScaleQId = UUID.randomUUID() + val labelDto = BatchAnswerQuestionService.LabelDto("Good", 3) + val labeledScaleQDto = factory.generateQuestionLabeledScaleDto(labeledScaleQId, factory.systematicStudyId, labelDto = labelDto, questionContext = "ROB") + val labeledScaleAnswer = factory.answerDetail(questionId = labeledScaleQId, type = "LABELED_SCALE", answer = labelDto) + + val request = factory.batchAnswerRequest(listOf(pickManyAnswer, labeledScaleAnswer)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), pickManyQId) } returns pickManyQDto + every { questionRepository.findById(any(), labeledScaleQId) } returns labeledScaleQDto + + sut.batchAnswerQuestion(presenter, request) + + verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) } + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(2, response.succeededAnswers.size) + assertTrue(response.failedAnswers.isEmpty()) + assertTrue(response.succeededAnswers.containsAll(listOf(pickManyQId, labeledScaleQId))) + }) + } + } + @Test fun `should handle a mix of successful and failed answers`() { val reviewDto = factory.generateDto() @@ -179,6 +322,30 @@ class BatchAnswerQuestionServiceImplTest { @Nested @DisplayName("When failing to answer questions") inner class WhenFailingToAnswerQuestions { + + @Test + fun `should create a failed answer entry for a pick-list answer not in options`() { + val reviewDto = factory.generateDto() + val questionId = UUID.randomUUID() + val questionDto = factory.generateQuestionPickListDto(questionId, factory.systematicStudyId, options = listOf("A", "B"), questionContext = "EXTRACTION") + val answerDetail = factory.answerDetail(questionId, "PICK_LIST", "C") + 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) + + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertTrue(response.succeededAnswers.isEmpty()) + assertEquals(1, response.failedAnswers.size) + assertTrue("must be one of the valid options" in response.failedAnswers.first().reason) + }) + } + } + @Test fun `should create a failed answer entry for a question with conflicting types`() { val reviewDto = factory.generateDto() @@ -216,7 +383,7 @@ class BatchAnswerQuestionServiceImplTest { every { studyReviewRepository.findById(any(), any()) } returns reviewDto every { questionRepository.findById(any(), questionId) } returns questionDto - sut.batchAnswerQuestion(presenter, request) // Updated call + sut.batchAnswerQuestion(presenter, request) verify(exactly = 1) { presenter.prepareSuccessView(withArg { response -> @@ -242,7 +409,7 @@ class BatchAnswerQuestionServiceImplTest { every { studyReviewRepository.findById(any(), any()) } returns reviewDto every { questionRepository.findById(any(), questionId) } returns questionDto - sut.batchAnswerQuestion(presenter, request) // Updated call + sut.batchAnswerQuestion(presenter, request) verify(exactly = 1) { presenter.prepareSuccessView(withArg { response -> 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 7cc0bbc18..64962c363 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 @@ -217,6 +217,27 @@ class TestDataFactory { QuestionContextEnum.valueOf(questionContext), ) + fun generateQuestionPickManyDto( + questionId: UUID, + systematicStudyId: UUID = this.systematicStudyId, + code: String = faker.lorem.words(), + description: String = faker.lorem.words(), + options: List, + questionContext: String + ) = + QuestionDto( + questionId, + systematicStudyId, + code, + description, + "PICK_MANY", + null, + null, + null, + options, + QuestionContextEnum.valueOf(questionContext), + ) + fun batchAnswerRequest( answers: List, researcherId: UUID = this.researcherId, diff --git a/review/src/test/kotlin/br/all/domain/model/question/PickManyTest.kt b/review/src/test/kotlin/br/all/domain/model/question/PickManyTest.kt new file mode 100644 index 000000000..35eea3e9d --- /dev/null +++ b/review/src/test/kotlin/br/all/domain/model/question/PickManyTest.kt @@ -0,0 +1,108 @@ +package br.all.domain.model.question + +import br.all.domain.model.review.SystematicStudyId +import br.all.domain.model.study.Answer +import io.github.serpro69.kfaker.Faker +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.util.* +import kotlin.test.assertEquals + +@Tag("UnitTest") +class PickManyTest { + private val faker = Faker() + private lateinit var validPickMany: PickMany + + @BeforeEach + fun setUp() { + val validOptions = listOf(faker.lorem.words(), faker.lorem.words(), faker.lorem.words()) + validPickMany = PickMany( + QuestionId(UUID.randomUUID()), + SystematicStudyId(UUID.randomUUID()), + faker.lorem.words(), + faker.lorem.words(), + validOptions + ) + } + + @Nested + @Tag("ValidClasses") + @DisplayName("When successfully answering a pick-many question") + inner class WhenSuccessfullyAnsweringPickManyQuestion { + @Test + fun `should answer with a single valid option from the list`() { + val chosenAnswer = listOf(validPickMany.options[1]) + val expectedAnswer = Answer(validPickMany.id.value(), chosenAnswer) + + val actualAnswer = validPickMany.answer(chosenAnswer) + + assertEquals(expectedAnswer, actualAnswer) + } + + @Test + fun `should answer with multiple valid options from the list`() { + val chosenAnswer = listOf(validPickMany.options[0], validPickMany.options[2]) + val expectedAnswer = Answer(validPickMany.id.value(), chosenAnswer) + + val actualAnswer = validPickMany.answer(chosenAnswer) + + assertEquals(expectedAnswer, actualAnswer) + } + } + + @Nested + @Tag("InvalidClasses") + @DisplayName("When being unable to create or answer PickMany questions") + inner class WhenBeingUnableToCreateOrAnswerPickMany { + + @Test + fun `should throw IllegalArgumentException for an empty options list during creation`() { + assertThrows { + PickMany( + QuestionId(UUID.randomUUID()), + SystematicStudyId(UUID.randomUUID()), + faker.lorem.words(), + faker.lorem.words(), + emptyList() + ) + } + } + + @ParameterizedTest(name = "[{index}]: option = \"{0}\"") + @ValueSource(strings = ["", " ", " "]) + fun `should throw IllegalArgumentException for a blank option during creation`(option: String) { + val options = listOf(faker.lorem.words(), option) + + assertThrows { + PickMany( + QuestionId(UUID.randomUUID()), + SystematicStudyId(UUID.randomUUID()), + faker.lorem.words(), + faker.lorem.words(), + options + ) + } + } + + @Test + fun `should throw IllegalArgumentException for an empty answer list`() { + assertThrows { validPickMany.answer(emptyList()) } + } + + @ParameterizedTest(name = "[{index}]: value = \"{0}\"") + @ValueSource(strings = ["", " ", " "]) + fun `should throw IllegalArgumentException for a blank value in the answer list`(value: String) { + val invalidAnswer = listOf(validPickMany.options.first(), value) + + assertThrows { validPickMany.answer(invalidAnswer) } + } + + @Test + fun `should throw IllegalArgumentException for a value in the answer list that is not in options`() { + val invalidAnswer = listOf(validPickMany.options.first(), "invalid option") + + assertThrows { validPickMany.answer(invalidAnswer) } + } + } +} \ No newline at end of file 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 deleted file mode 100644 index 29df084e3..000000000 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt +++ /dev/null @@ -1,175 +0,0 @@ -package br.all.domain.shared.ddd - -import br.all.domain.shared.valueobject.Text -import org.junit.jupiter.api.* -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals - -@Tag("UnitTest") -class TextTest { - - @Test - fun `valid TEXT should not throw an exception`() { - val textValue = "Mussum Ipsum, cacilds vidis litro abertis. Nulla id gravida magna, ut semper sapien. Todo mundo vê os porris que eu tomo, mas ninguém vê os tombis que eu levo! Leite de capivaris, leite de mula manquis sem cabeça. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Si num tem leite então bota uma pinga aí cumpadi! A ordem dos tratores não altera o pão duris. Negão é teu passadis, eu sou faxa pretis. Nullam volutpat risus nec leo commodo, ut interdum diam laoreet. Sed non consequat odio." - val text = Text(textValue) - } - - @Test - fun `equal TEXTs should be equal`() { - val textValue1 = "Mussum Ipsum, cacilds vidis litro abertis. Delegadis gente finis, bibendum egestas augue arcu ut est. Negão é teu passadis, eu sou faxa pretis. Manduma pindureta quium dia nois paga. Interessantiss quisso pudia ce receita de bolis, mais bolis eu num gostis." - val textValue2 = "Mussum Ipsum, cacilds vidis litro abertis. Delegadis gente finis, bibendum egestas augue arcu ut est. Negão é teu passadis, eu sou faxa pretis. Manduma pindureta quium dia nois paga. Interessantiss quisso pudia ce receita de bolis, mais bolis eu num gostis." - - val text1 = Text(textValue1) - val text2 = Text(textValue2) - - assertEquals(text1, text2) - } - - @Test - fun `diferent TEXTs should not be equal`() { - val textValue1 = "Mussum Ipsum, cacilds vidis litro abertis. Delegadis gente finis, bibendum egestas augue arcu ut est. Negão é teu passadis, eu sou faxa pretis. Manduma pindureta quium dia nois paga. Interessantiss quisso pudia ce receita de bolis, mais bolis eu num gostis." - val textValue2 = "Mussum Ipsum, cacilds vidis litro abertis. Per aumento de cachacis, eu reclamis. Tá deprimidis, eu conheço uma cachacis que pode alegrar sua vidis. Nulla id gravida magna, ut semper sapien. Aenean aliquam molestie leo, vitae iaculis nisl." - - val text1 = Text(textValue1) - val text2 = Text(textValue2) - - assertNotEquals(text1, text2) - } - - @Test - fun `valid text with special characters should stil valid`() { - val textValue = "Mussum Ipsum, cacilds vidis litro abertis. Nulla id gravida magna, ut semper sapien. Todo mundo vê os porris que eu tomo, mas ninguém vê os tombis que eu levo! Leite de capivaris, leite de mula manquis sem cabeça. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Si num tem leite então bota uma pinga aí cumpadi! A ordem dos tratores não altera o pão duris. Negão é teu passadis, eu sou faxa pretis. Nullam volutpat risus nec leo commodo, ut interdum diam laoreet. Sed non consequat odio." - val text = Text(textValue) - val newTextValue = textValue + "(test!@#$%)" - assertDoesNotThrow { - Text(textValue) - Text(newTextValue) - } - - } - - @Test - fun `empty TEXT should throw an exception`() { - val textValue = "" - assertThrows { - Text(textValue) - } - } - - @Test - fun `blank TEXT should throw an exception`() { - val textValue = " " - assertThrows { - Text(textValue) - } - } - - @Nested - @DisplayName("should NOT be able to accept as TEXT") - inner class InvalidTextTest { - - - @Test - fun `@@@`() { - val textValue = "@@@" - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName(".") - fun ShouldNotAcceptDotsAsText() { - val textValue = "." - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName(",") - fun ShouldNotAcceptCommasAsText() { - val textValue = "." - assertThrows { - Text(textValue) - } - } - @Test - fun `!!!`() { - val textValue = "!!!" - assertThrows { - Text(textValue) - } - } - @Test - fun `###`() { - val textValue = "###" - assertThrows { - Text(textValue) - } - } - @Test - fun `$$$`() { - val textValue = "$$$" - assertThrows { - Text(textValue) - } - } - @Test - fun `%%%`() { - val textValue = "%%%" - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName(":") - fun ShouldNotAcceptColonsAsText() { - val textValue = ":" - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName(";") - fun `ShouldNotAcceptSemicolonsAsText`() { - val textValue = ";" - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName("[") - fun `ShouldNotAcceptBracesText`() { - val textValue = "[[" - assertThrows { - Text(textValue) - } - } - @Test - @DisplayName("]") - fun `ShouldNotAcceptCloseBracesText`() { - val textValue = "]]" - assertThrows { - Text(textValue) - } - } - - @Test - @DisplayName("#\$%!;") - fun `ShouldNotAcceptJustSymbolsText`() { - val textValue = "#$%!;" - assertThrows { - Text(textValue) - } - } - - @Test - @DisplayName("[]") - fun ShouldNotAcceptBraceText() { - val textValue = "[[[]]]" - assertThrows { - Text(textValue) - } - } - - } -} diff --git a/shared/src/main/kotlin/br/all/domain/shared/user/Name.kt b/shared/src/main/kotlin/br/all/domain/shared/user/Name.kt new file mode 100644 index 000000000..7eb2f20af --- /dev/null +++ b/shared/src/main/kotlin/br/all/domain/shared/user/Name.kt @@ -0,0 +1,30 @@ +package br.all.domain.shared.user + +import br.all.domain.shared.ddd.Notification +import br.all.domain.shared.ddd.ValueObject + +data class Name(val value: String) : ValueObject() { + + init { + val notification = validate() + require(notification.hasNoErrors()) { notification.message() } + } + + override fun validate(): Notification { + val notification = Notification() + + if (value.isBlank()) { + notification.addError("The name must not be blank!") + } else { + if (value.startsWith(" ") || value.endsWith(" ")) { + notification.addError("The name must not start or end with blank spaces!") + } + + if (!value.matches(Regex("[\\p{L}.' ]+"))) { + notification.addError("The name must contain only letters, dots, apostrophes, and blank spaces!") + } + } + + return notification + } +} diff --git a/account/src/main/kotlin/br/all/domain/user/Text.kt b/shared/src/main/kotlin/br/all/domain/shared/user/Text.kt similarity index 95% rename from account/src/main/kotlin/br/all/domain/user/Text.kt rename to shared/src/main/kotlin/br/all/domain/shared/user/Text.kt index eb81a04ba..89144d6b4 100644 --- a/account/src/main/kotlin/br/all/domain/user/Text.kt +++ b/shared/src/main/kotlin/br/all/domain/shared/user/Text.kt @@ -1,4 +1,4 @@ -package br.all.domain.user +package br.all.domain.shared.user import br.all.domain.shared.ddd.Notification import br.all.domain.shared.ddd.ValueObject diff --git a/account/src/main/kotlin/br/all/domain/user/Username.kt b/shared/src/main/kotlin/br/all/domain/shared/user/Username.kt similarity index 94% rename from account/src/main/kotlin/br/all/domain/user/Username.kt rename to shared/src/main/kotlin/br/all/domain/shared/user/Username.kt index 95c604a79..2979e335c 100644 --- a/account/src/main/kotlin/br/all/domain/user/Username.kt +++ b/shared/src/main/kotlin/br/all/domain/shared/user/Username.kt @@ -1,4 +1,4 @@ -package br.all.domain.user +package br.all.domain.shared.user import br.all.domain.shared.ddd.Notification import br.all.domain.shared.ddd.ValueObject @@ -18,4 +18,4 @@ data class Username(val value: String) : ValueObject(){ notification.addError("Username must contain only letters and numbers, dashes and underscores!") return notification } -} +} \ No newline at end of file diff --git a/shared/src/test/kotlin/br/all/domain/shared/user/NameTest.kt b/shared/src/test/kotlin/br/all/domain/shared/user/NameTest.kt new file mode 100644 index 000000000..b40ea1617 --- /dev/null +++ b/shared/src/test/kotlin/br/all/domain/shared/user/NameTest.kt @@ -0,0 +1,102 @@ +package br.all.domain.shared.user + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +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.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@Tag("UnitTest") +class NameTest { + + @Nested + @DisplayName("when name is valid") + inner class ValidNameTest { + @Test + fun `should create Name for simple name`() { + assertDoesNotThrow { Name("John Doe") } + } + + @Test + fun `should create Name with accented characters`() { + assertDoesNotThrow { Name("João Pacífico") } + } + + @Test + fun `should create Name with dots`() { + assertDoesNotThrow { Name("M. John Doe") } + } + + @Test + fun `should create Name with apostrophes`() { + assertDoesNotThrow { Name("O'Connell") } + } + + @Test + fun `should create Name with a mix of valid characters`() { + assertDoesNotThrow { Name("M. O'Connell Pacífico") } + } + } + + @Nested + @DisplayName("when name is invalid") + inner class InvalidNameTest { + @Test + fun `should throw exception for empty name`() { + val exception = assertThrows { Name("") } + assertTrue(exception.message!!.contains("The name must not be blank!")) + } + + @Test + fun `should throw exception for blank name`() { + val exception = assertThrows { Name(" ") } + assertTrue(exception.message!!.contains("The name must not be blank!")) + } + + @Test + fun `should throw exception for name starting with a space`() { + val exception = assertThrows { Name(" John Doe") } + assertEquals("The name must not start or end with blank spaces!", exception.message) + } + + @Test + fun `should throw exception for name ending with a space`() { + val exception = assertThrows { Name("John Doe ") } + assertEquals("The name must not start or end with blank spaces!", exception.message) + } + + @Test + fun `should throw exception for name with numbers`() { + val exception = assertThrows { Name("John Doe 123") } + assertEquals("The name must contain only letters, dots, apostrophes, and blank spaces!", exception.message) + } + + @Test + fun `should throw exception for name with invalid symbols`() { + val exception = assertThrows { Name("John-Doe@") } + assertEquals("The name must contain only letters, dots, apostrophes, and blank spaces!", exception.message) + } + } + + @Nested + @DisplayName("equality checks") + inner class EqualityTest { + @Test + fun `should be equal when two Name objects have the same value`() { + val name1 = Name("M. Pacífico") + val name2 = Name("M. Pacífico") + assertEquals(name1, name2) + } + + @Test + fun `should not be equal when two Name objects have different values`() { + val name1 = Name("John Doe") + val name2 = Name("Jane Doe") + assertNotEquals(name1, name2) + } + } +} diff --git a/shared/src/test/kotlin/br/all/domain/shared/user/TextTest.kt b/shared/src/test/kotlin/br/all/domain/shared/user/TextTest.kt new file mode 100644 index 000000000..365045397 --- /dev/null +++ b/shared/src/test/kotlin/br/all/domain/shared/user/TextTest.kt @@ -0,0 +1,165 @@ +package br.all.domain.shared.user + +import org.junit.jupiter.api.Assertions +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.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@Tag("UnitTest") +class TextTest { + + @Test + fun `should create Text when value is valid`() { + val textValue = "Mussum Ipsum cacilds vidis litro abertis" + Assertions.assertDoesNotThrow { + Text(textValue) + } + } + + @Test + fun `should be equal when two Text objects have the same value`() { + val textValue1 = "Mussum Ipsum cacilds vidis litro abertis Delegadis gente finis" + val textValue2 = "Mussum Ipsum cacilds vidis litro abertis Delegadis gente finis" + + val text1 = Text(textValue1) + val text2 = Text(textValue2) + + assertEquals(text1, text2) + } + + @Test + fun `should not be equal when two Text objects have different values`() { + val textValue1 = "Mussum Ipsum cacilds vidis litro abertis" + val textValue2 = "Per aumento de cachacis eu reclamis" + + val text1 = Text(textValue1) + val text2 = Text(textValue2) + + assertNotEquals(text1, text2) + } + + @Test + fun `should throw exception for empty TEXT`() { + val textValue = "" + assertThrows { + Text(textValue) + } + } + + @Test + fun `should throw exception for blank TEXT`() { + val textValue = " " + assertThrows { + Text(textValue) + } + } + + @Test + fun `should throw exception for text starting with a space`() { + val textValue = " leading space" + val exception = assertThrows { + Text(textValue) + } + assertEquals("The text must not start or end with blank spaces!", exception.message) + } + + @Test + fun `should throw exception for text ending with a space`() { + val textValue = "trailing space " + val exception = assertThrows { + Text(textValue) + } + assertEquals("The text must not start or end with blank spaces!", exception.message) + } + + @Nested + @DisplayName("should throw exception for text with invalid characters") + inner class InvalidTextTest { + + @Test + fun `like numbers`() { + val textValue = "Text with 123" + val exception = assertThrows { + Text(textValue) + } + assertEquals("The text must contain only letters and blank spaces!", exception.message) + } + + @Test + fun `like @@@`() { + val textValue = "@@@" + assertThrows { Text(textValue) } + } + + @Test + @DisplayName("like . (dots)") + fun shouldNotAcceptDotsAsText() { + val textValue = "." + assertThrows { Text(textValue) } + } + + @Test + @DisplayName("like , (commas)") + fun shouldNotAcceptCommasAsText() { + val textValue = "," + assertThrows { Text(textValue) } + } + + @Test + fun `like !!!`() { + val textValue = "!!!" + assertThrows { Text(textValue) } + } + + @Test + fun `like ###`() { + val textValue = "###" + assertThrows { Text(textValue) } + } + + @Test + fun `like $$$`() { + val textValue = "$$$" + assertThrows { Text(textValue) } + } + +// // This test can cause problems on Windows, but it passes. +// @Test +// fun `like %%%`() { +// val textValue = "%%%" +// assertThrows { Text(textValue) } +// } + + @Test + @DisplayName("like : (colons)") + fun shouldNotAcceptColonsAsText() { + val textValue = ":" + assertThrows { Text(textValue) } + } + + @Test + @DisplayName("like ; (semicolons)") + fun shouldNotAcceptSemicolonsAsText() { + val textValue = ";" + assertThrows { Text(textValue) } + } + + @Test + @DisplayName("like [ ] (brackets)") + fun shouldNotAcceptBracketsText() { + val textValue = "[[]]" + assertThrows { Text(textValue) } + } + + @Test + @DisplayName("like a mix of symbols (#$%!;)") + fun shouldNotAcceptJustSymbolsText() { + val textValue = "#$%!;" + assertThrows { Text(textValue) } + } + } +} \ No newline at end of file diff --git a/shared/src/test/kotlin/br/all/domain/shared/user/UsernameTest.kt b/shared/src/test/kotlin/br/all/domain/shared/user/UsernameTest.kt new file mode 100644 index 000000000..683d6e6b1 --- /dev/null +++ b/shared/src/test/kotlin/br/all/domain/shared/user/UsernameTest.kt @@ -0,0 +1,92 @@ +package br.all.domain.shared.user + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +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.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + + +@Tag("UnitTest") +class UsernameTest { + + @Nested + @DisplayName("when username is valid") + inner class ValidUsernameTest { + @Test + fun `should create Username with letters and numbers`() { + assertDoesNotThrow { Username("user123") } + } + + @Test + fun `should create Username with dashes`() { + assertDoesNotThrow { Username("user-name") } + } + + @Test + fun `should create Username with underscores`() { + assertDoesNotThrow { Username("user_name") } + } + + @Test + fun `should create Username with a mix of valid characters`() { + assertDoesNotThrow { Username("user-123_name") } + } + } + + @Nested + @DisplayName("when username is invalid") + inner class InvalidUsernameTest { + @Test + fun `should throw exception for empty username`() { + val exception = assertThrows { Username("") } + assertTrue(exception.message!!.contains("Username must not be blank!")) + } + + @Test + fun `should throw exception for blank username`() { + val exception = assertThrows { Username(" ") } + assertTrue(exception.message!!.contains("Username must not be blank!")) + } + + @Test + fun `should throw exception for username with spaces`() { + val exception = assertThrows { Username("user name") } + assertEquals("Username must contain only letters and numbers, dashes and underscores!", exception.message) + } + + @Test + fun `should throw exception for username with invalid symbols`() { + val exception = assertThrows { Username("user@name!") } + assertEquals("Username must contain only letters and numbers, dashes and underscores!", exception.message) + } + + @Test + fun `should throw exception for username with dots`() { + val exception = assertThrows { Username("user.name") } + assertEquals("Username must contain only letters and numbers, dashes and underscores!", exception.message) + } + } + + @Nested + @DisplayName("equality checks") + inner class EqualityTest { + @Test + fun `should be equal when two Username objects have the same value`() { + val user1 = Username("test-user_1") + val user2 = Username("test-user_1") + assertEquals(user1, user2) + } + + @Test + fun `should not be equal when two Username objects have different values`() { + val user1 = Username("user1") + val user2 = Username("user2") + assertNotEquals(user1, user2) + } + } +} \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt index e423f1232..69ee16bbb 100644 --- a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt @@ -37,6 +37,7 @@ class ExtractionQuestionController( ) { data class TextualRequest(val code: String, val description: String) data class PickListRequest(val code: String, val description: String, val options: List) + data class PickManyRequest(val code: String, val description: String, val options: List) data class LabeledScaleRequest(val code: String, val description: String, val scales: Map) data class NumberScaleRequest(val code: String, val description: String, val lower: Int, val higher: Int) val questionContext = "EXTRACTION" @@ -119,6 +120,42 @@ class ExtractionQuestionController( ) ) + @PostMapping("/pick-many") + @Operation(summary = "Create a extraction pick-many question in the protocol") + @ApiResponses( + value = [ + ApiResponse(responseCode = "201", description = "Success creating a pick-many question in the protocol", + content = [Content(schema = Schema(hidden = true))]), + ApiResponse( + responseCode = "400", + description = "Fail creating a pick-many question in the protocol - invalid input", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail creating a pick-many question in the protocol - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ),ApiResponse( + responseCode = "403", + description = "Fail creating a pick-many question in the protocol - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun createPickManyQuestion( + @PathVariable systematicStudyId: UUID, @RequestBody request: PickManyRequest, + ): ResponseEntity<*> = createQuestion( + RequestModel( + authenticationInfoService.getAuthenticatedUserId(), + systematicStudyId, + questionContext, + PICK_MANY, + request.code, + request.description, + options = request.options + ) + ) + @PostMapping("/labeled-scale") @Operation(summary = "Create a extraction labeled-scale question in the protocol") @ApiResponses( diff --git a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt index a58824bc6..41499029c 100644 --- a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt @@ -36,6 +36,7 @@ class RiskOfBiasQuestionController( ) { data class TextualRequest(val code: String, val description: String) data class PickListRequest(val code: String, val description: String, val options: List) + data class PickManyRequest(val code: String, val description: String, val options: List) data class LabeledScaleRequest(val code: String, val description: String, val scales: Map) data class NumberScaleRequest(val code: String, val description: String, val lower: Int, val higher: Int) val questionContext = "ROB" @@ -119,6 +120,43 @@ class RiskOfBiasQuestionController( ) ) + @PostMapping("/pick-many") + @Operation(summary = "Create a risk of bias pick-many question in the protocol") + @ApiResponses( + value = [ + ApiResponse(responseCode = "201", description = "Success creating a pick-many question in the protocol", + content = [Content(schema = Schema(hidden = true))]), + ApiResponse( + responseCode = "400", + description = "Fail creating a pick-many question in the protocol - invalid input", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail creating a pick-many question in the protocol - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ),ApiResponse( + responseCode = "403", + description = "Fail creating a pick-many question in the protocol - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + + ] + ) + fun createPickManyQuestion( + @PathVariable systematicStudyId: UUID, @RequestBody request: PickManyRequest, + ): ResponseEntity<*> = createQuestion( + RequestModel( + authenticationInfoService.getAuthenticatedUserId(), + systematicStudyId, + questionContext, + PICK_MANY, + request.code, + request.description, + options = request.options + ) + ) + @PostMapping("/labeled-scale") @Operation(summary = "Create a risk of bias labeled-scale question in the protocol") @ApiResponses( diff --git a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulCreateExtractionQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulCreateExtractionQuestionPresenter.kt index 1293091b9..2f54827cf 100644 --- a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulCreateExtractionQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulCreateExtractionQuestionPresenter.kt @@ -24,11 +24,12 @@ class RestfulCreateExtractionQuestionPresenter( val selfRef = linksFactory.findExtractionQuestion(response.systematicStudyId, response.questionId) val pickList = linksFactory.createPickListExtractionQuestion(response.systematicStudyId) + val pickMany = linksFactory.createPickManyExtractionQuestion(response.systematicStudyId) val labeledScale = linksFactory.createLabeledScaleExtractionQuestion(response.systematicStudyId) val numberScale = linksFactory.createNumberScaleExtractionQuestion(response.systematicStudyId) val findAll = linksFactory.findAllReviewExtractionQuestions(response.systematicStudyId) - viewModel.add(selfRef, pickList, labeledScale, numberScale, findAll) + viewModel.add(selfRef, pickList, pickMany, labeledScale, numberScale, findAll) responseEntity = status(HttpStatus.CREATED).body(viewModel) } diff --git a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindAllExtractionQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindAllExtractionQuestionPresenter.kt index b78e80658..603e45cec 100644 --- a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindAllExtractionQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindAllExtractionQuestionPresenter.kt @@ -22,10 +22,11 @@ class RestfulFindAllExtractionQuestionPresenter( val selfRef = linksFactory.findAllReviewExtractionQuestions(response.systematicStudyId) val createQuestion = linksFactory.createTextualExtractionQuestion(response.systematicStudyId) val createPickList = linksFactory.createPickListExtractionQuestion(response.systematicStudyId) + val createPickMany = linksFactory.createPickManyExtractionQuestion(response.systematicStudyId) val createLabeledScale = linksFactory.createLabeledScaleExtractionQuestion(response.systematicStudyId) val createNumberScale = linksFactory.createNumberScaleExtractionQuestion(response.systematicStudyId) - viewModel.add(selfRef, createQuestion, createPickList, createLabeledScale, createNumberScale) + viewModel.add(selfRef, createQuestion, createPickList, createPickMany, createLabeledScale, createNumberScale) responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel) } diff --git a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindExtractionQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindExtractionQuestionPresenter.kt index 75552dbdd..7105af7b5 100644 --- a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindExtractionQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulFindExtractionQuestionPresenter.kt @@ -34,7 +34,7 @@ class RestfulFindExtractionQuestionPresenter( val questionId = content.questionId val systematicStudyId = content.systematicStudyId val code = content.code - val description = content.code + val description = content.description val questionType = content.questionType val scales = content.scales val higher = content.higher diff --git a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulCreateRoBQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulCreateRoBQuestionPresenter.kt index 9be521156..c003991d7 100644 --- a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulCreateRoBQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulCreateRoBQuestionPresenter.kt @@ -23,11 +23,12 @@ class RestfulCreateRoBQuestionPresenter( val selfRef = linksFactory.findRobQuestion(response.systematicStudyId, response.questionId) val pickList = linksFactory.createPickListRobQuestion(response.systematicStudyId) + val pickMany = linksFactory.createPickManyRobQuestion(response.systematicStudyId) val labeledScale = linksFactory.createLabeledScaleRobQuestion(response.systematicStudyId) val numberScale = linksFactory.createNumberScaleRobQuestion(response.systematicStudyId) val findAll = linksFactory.findAllReviewRobQuestions(response.systematicStudyId) - restfulResponse.add(selfRef, pickList, labeledScale, numberScale, findAll) + restfulResponse.add(selfRef, pickList, pickMany, labeledScale, numberScale, findAll) responseEntity = status(HttpStatus.CREATED).body(restfulResponse) } diff --git a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindAllRoBQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindAllRoBQuestionPresenter.kt index dc356d90f..41bd77f66 100644 --- a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindAllRoBQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindAllRoBQuestionPresenter.kt @@ -22,10 +22,11 @@ class RestfulFindAllRoBQuestionPresenter( val selfRef = linksFactory.findAllReviewRobQuestions(response.systematicStudyId) val createQuestion = linksFactory.createTextualRobQuestion(response.systematicStudyId) val createPickList = linksFactory.createPickListRobQuestion(response.systematicStudyId) + val createPickMany = linksFactory.createPickManyRobQuestion(response.systematicStudyId) val createLabeledScale = linksFactory.createLabeledScaleRobQuestion(response.systematicStudyId) val createNumberScale = linksFactory.createNumberScaleRobQuestion(response.systematicStudyId) - restfulResponse.add(selfRef, createQuestion ,createPickList,createLabeledScale,createNumberScale) + restfulResponse.add(selfRef, createQuestion, createPickList, createPickMany, createLabeledScale, createNumberScale) responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) } diff --git a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindRoBQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindRoBQuestionPresenter.kt index 6a817378a..4409fbb97 100644 --- a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindRoBQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulFindRoBQuestionPresenter.kt @@ -35,7 +35,7 @@ class RestfulFindRoBQuestionPresenter( val questionId = content.questionId val systematicStudyId = content.systematicStudyId val code = content.code - val description = content.code + val description = content.description val questionType = content.questionType val scales = content.scales val higher = content.higher diff --git a/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt b/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt index 9e3fc3156..9884fe50f 100644 --- a/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt +++ b/web/src/main/kotlin/br/all/user/presenter/RestfulRetrieveUserProfilePresenter.kt @@ -15,6 +15,7 @@ class RestfulRetrieveUserProfilePresenter : RetrieveUserProfilePresenter { override fun prepareSuccessView(response: ResponseModel) { val restfulResponse = ViewModel( response.userId, + response.name, response.username, response.email, response.affiliation, @@ -30,6 +31,7 @@ class RestfulRetrieveUserProfilePresenter : RetrieveUserProfilePresenter { private data class ViewModel( val userId: UUID, + val name: String, val username: String, val email: String, val affiliation: String, diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt index e2ed2eb30..dd2151c24 100644 --- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt +++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt @@ -81,6 +81,15 @@ class LinksFactory { ) }.withRel("create-pick-list-extraction-question").withType("POST") + fun createPickManyExtractionQuestion(systematicStudyId: UUID): Link = linkTo { + createPickManyQuestion( + systematicStudyId, + request = ExtractionQuestionController.PickManyRequest( + "code", "description", listOf("option1") + ) + ) + }.withRel("create-pick-many-extraction-question").withType("POST") + fun createLabeledScaleExtractionQuestion(systematicStudyId: UUID): Link = linkTo { createLabeledScaleQuestion( systematicStudyId, @@ -125,6 +134,15 @@ class LinksFactory { ) }.withRel("create-pick-list-rob-question").withType("POST") + fun createPickManyRobQuestion(systematicStudyId: UUID): Link = linkTo { + createPickManyQuestion( + systematicStudyId, + request = RiskOfBiasQuestionController.PickManyRequest( + "code", "description", listOf("option1") + ) + ) + }.withRel("create-pick-many-rob-question").withType("POST") + fun createLabeledScaleRobQuestion(systematicStudyId: UUID): Link = linkTo { createLabeledScaleQuestion( systematicStudyId, diff --git a/web/src/main/kotlin/br/all/utils/example/CreateSystematicReviewExampleService.kt b/web/src/main/kotlin/br/all/utils/example/CreateSystematicReviewExampleService.kt index addd68c30..0fa98c348 100644 --- a/web/src/main/kotlin/br/all/utils/example/CreateSystematicReviewExampleService.kt +++ b/web/src/main/kotlin/br/all/utils/example/CreateSystematicReviewExampleService.kt @@ -85,6 +85,22 @@ class CreateSystematicReviewExampleService( ) ) + val eq5 = question.createQuestion( + reviewId = systematicId, + code = "EQ5", + title = "Which of the following service-oriented architectures (SOAs) have been applied to robotic systems in the reviewed studies?", + type = "PICK_MANY", + context = QuestionContextEnum.EXTRACTION, + options = listOf( + "ROS (Robot Operating System)", + "DDS (Data Distribution Service)", + "REST (Representational State Transfer)", + "SOAP (Simple Object Access Protocol)", + "Corba (Common Object Request Broker Architecture)", + "MQTT (Message Queuing Telemetry Transport)" + ) + ) + val rbq1 = question.createQuestion( reviewId = systematicId, code = "RBQ1", @@ -133,6 +149,21 @@ class CreateSystematicReviewExampleService( lower = 1 ) + val rbq5 = question.createQuestion( + reviewId = systematicId, + code = "RBQ5", + title = "Select all potential sources of bias that are present in the primary study.", + type = "PICK_MANY", + context = QuestionContextEnum.ROB, + options = listOf( + "Selection bias (e.g., non-randomized sampling)", + "Performance bias (e.g., lack of blinding of participants)", + "Detection bias (e.g., lack of blinding of outcome assessors)", + "Attrition bias (e.g., incomplete outcome data)", + "Reporting bias (e.g., selective reporting of outcomes)" + ) + ) + protocolRepository.saveOrUpdate( ProtocolDto( id = systematicId, @@ -175,8 +206,8 @@ class CreateSystematicReviewExampleService( ), dataCollectionProcess = "Data extraction will be performed using pre-defined extraction tables corresponding to each research question.", analysisAndSynthesisProcess = "Data were synthesized using statistical methods and meta-analysis to draw conclusions about the research area.", - extractionQuestions = setOf(eq1.questionId, eq2.questionId, eq3.questionId, eq4.questionId), - robQuestions = setOf(rbq1.questionId, rbq2.questionId, rbq3.questionId, rbq4.questionId), + extractionQuestions = setOf(eq1.questionId, eq2.questionId, eq3.questionId, eq4.questionId, eq5.questionId), + robQuestions = setOf(rbq1.questionId, rbq2.questionId, rbq3.questionId, rbq4.questionId, rbq5.questionId), picoc = PicocDto( population = "Researchers and developers of robotic systems interested in employing SOA.", intervention = "The development and use of service-oriented robotic systems.", diff --git a/web/src/main/kotlin/br/all/utils/example/RegisterUserExampleService.kt b/web/src/main/kotlin/br/all/utils/example/RegisterUserExampleService.kt index 8c61d6b1c..794c52426 100644 --- a/web/src/main/kotlin/br/all/utils/example/RegisterUserExampleService.kt +++ b/web/src/main/kotlin/br/all/utils/example/RegisterUserExampleService.kt @@ -3,6 +3,9 @@ package br.all.utils.example import br.all.application.user.repository.UserAccountRepository import br.all.application.user.repository.toDto import br.all.domain.shared.user.Email +import br.all.domain.shared.user.Name +import br.all.domain.shared.user.Text +import br.all.domain.shared.user.Username import br.all.domain.user.* import org.springframework.stereotype.Service import java.util.* @@ -14,9 +17,10 @@ class RegisterUserExampleService ( fun registerUserAccount( username: String, password: String, - email: String = "email@email.com", - country: String = "Country", - affiliation: String = "affiliation", + email: String = "lucas@gmail.com", + country: String = "Brazil", + affiliation: String = "IFSP", + name: String = "Lucas" ): UserAccount { val newUserAccount = UserAccount( @@ -26,7 +30,8 @@ class RegisterUserExampleService ( email = Email(email), country = Text(country), affiliation = affiliation, - authorities = setOf(Authority.USER) + authorities = setOf(Authority.USER), + name = Name(name), ) repo.save(newUserAccount.toDto()) diff --git a/web/src/main/resources/Springer.bib b/web/src/main/resources/Springer.bib index e4fbccbba..ba6860636 100644 --- a/web/src/main/resources/Springer.bib +++ b/web/src/main/resources/Springer.bib @@ -27,7 +27,7 @@ @INCOLLECTION{Awaad2008 affiliation = {Bonn-Rhein-Sieg University of Applied Science Grantham-Allee 20 53757 Sankt Augustin Germany}, isbn = {978-3-540-89075-1}, - keyword = {Computer Science}, + keywords = {Computer Science,Robotic Learning}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-540-89076-8_13} } @@ -52,7 +52,7 @@ @INCOLLECTION{Borangiu2009 affiliation = {Department of Automatic Control and Industrial Informatics, University Politehnica of Bucharest, Bucharest, Romania}, isbn = {978-90-481-3522-6}, - keyword = {Engineering}, + keywords = {Engineering;SOA}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-90-481-3522-6_49} } @@ -77,7 +77,7 @@ @INCOLLECTION{Borangiu2009 affiliation = {Department of Automatic Control and Industrial Informatics, University Politehnica of Bucharest, Bucharest, Romania}, isbn = {978-90-481-3522-6}, - keyword = {Engineering}, + keywords = {Engineering}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-90-481-3522-6_49} } @@ -102,7 +102,7 @@ @INCOLLECTION{Borangiu2009 affiliation = {Department of Automatic Control and Industrial Informatics, University Politehnica of Bucharest, Bucharest, Romania}, isbn = {978-90-481-3522-6}, - keyword = {Engineering}, + keywords = {Engineering}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-90-481-3522-6_49} } @@ -127,7 +127,7 @@ @INCOLLECTION{Borangiu2009 affiliation = {Department of Automatic Control and Industrial Informatics, University Politehnica of Bucharest, Bucharest, Romania}, isbn = {978-90-481-3522-6}, - keyword = {Engineering}, + keywords = {Engineering}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-90-481-3522-6_49} } @@ -158,7 +158,7 @@ @INCOLLECTION{DeCarolis2009 socially oriented situations.}, affiliation = {University of Bari Dipartimento di Informatica}, isbn = {978-3-642-05407-5}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-05408-2_20} } @@ -195,7 +195,7 @@ @INCOLLECTION{Helal2008 affiliation = {University of Florida Department of Computer & Information Science & Engineering Gainesville FL 32611 USA}, isbn = {978-3-540-70584-0}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-540-70585-7_1} } @@ -229,7 +229,7 @@ @INCOLLECTION{Ko2009 affiliation = {Korea Advanced Institute of Science and Technology (KAIST) Computer Science Department Daejeon Korea}, isbn = {978-3-642-04594-3}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-04595-0_46} } @@ -259,7 +259,7 @@ @INCOLLECTION{Kononchuk2011a comparison to other robot control systems.}, affiliation = {Ural State University, Lenina str. 51, Yekaterinburg, Russia}, isbn = {978-3-642-21975-7}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-21975-7_15} } @@ -290,7 +290,7 @@ @INCOLLECTION{Mikulski2011 affiliation = {Institute of Automatic Control, Silesian University of Technology, Akademicka 16, 44-100 Gliwice, Poland}, isbn = {978-3-642-23168-1}, - keyword = {Books}, + keywords = {Books}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-23169-8_9} } @@ -335,7 +335,7 @@ @INCOLLECTION{Pop2009 affiliation = {Telecommunications and IT Technical University of Cluj-Napoca, Faculty of Electronics Cluj-Napoca Romania}, isbn = {978-3-642-04292-8}, - keyword = {Engineering}, + keywords = {Engineering}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-04292-8_13} } @@ -367,7 +367,7 @@ @INCOLLECTION{Weigand2011 a robot cleaner.}, affiliation = {Tilburg University, P.O.Box 90153, 5000 LE Tilburg, The Netherlands}, isbn = {978-3-642-21639-8}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-21640-4_43} } @@ -399,7 +399,7 @@ @INCOLLECTION{Weigand2011 a robot cleaner.}, affiliation = {Tilburg University, P.O.Box 90153, 5000 LE Tilburg, The Netherlands}, isbn = {978-3-642-21639-8}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-21640-4_43} } @@ -431,7 +431,7 @@ @INCOLLECTION{Weigand2011 a robot cleaner.}, affiliation = {Tilburg University, P.O.Box 90153, 5000 LE Tilburg, The Netherlands}, isbn = {978-3-642-21639-8}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-21640-4_43} } @@ -463,7 +463,7 @@ @INCOLLECTION{Weigand2011 a robot cleaner.}, affiliation = {Tilburg University, P.O.Box 90153, 5000 LE Tilburg, The Netherlands}, isbn = {978-3-642-21639-8}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-21640-4_43} } @@ -493,7 +493,7 @@ @INCOLLECTION{Yeom2007a affiliation = {School of Computer Science, The University of Seoul, Jeonnong-dong, Dongdaemun-gu, Seoul, 130-743 Korea}, isbn = {978-3-540-73548-9}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-540-73549-6_8} } @@ -525,7 +525,7 @@ @INCOLLECTION{Zhu2011 affiliation = {School of Computer Science and Engineering, South China University of Technology, Guangzhou, China}, isbn = {978-3-642-23147-6}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-23147-6_2} } @@ -557,7 +557,7 @@ @INCOLLECTION{Zhu2011 affiliation = {School of Computer Science and Engineering, South China University of Technology, Guangzhou, China}, isbn = {978-3-642-23147-6}, - keyword = {Computer Science}, + keywords = {Computer Science}, source = {Springer}, url = {http://dx.doi.org/10.1007/978-3-642-23147-6_2} } diff --git a/web/src/main/resources/banner.txt b/web/src/main/resources/banner.txt new file mode 100644 index 000000000..caa41d7af --- /dev/null +++ b/web/src/main/resources/banner.txt @@ -0,0 +1,10 @@ + █████████ █████ █████████ █████ █████████ ███████████ █████ + ███░░░░░███ ░░███ ███░░░░░███ ░░███ ███░░░░░███ ░░███░░░░░███░░███ +░███ ░░░ ███████ ░███ ░███ ████████ ███████ ░███ ░███ ░███ ░███ ░███ +░░█████████ ░░░███░ ░███████████ ░░███░░███░░░███░ ░███████████ ░██████████ ░███ + ░░░░░░░░███ ░███ ░███░░░░░███ ░███ ░░░ ░███ ░███░░░░░███ ░███░░░░░░ ░███ + ███ ░███ ░███ ███ ░███ ░███ ░███ ░███ ███ ░███ ░███ ░███ ░███ +░░█████████ ░░█████ █████ █████ █████ ░░█████ █████ █████ █████ █████ + ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ + + 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 f9b441db8..c05d71810 100644 --- a/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt +++ b/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt @@ -93,6 +93,19 @@ class ExtractionQuestionControllerTest( .andExpect(jsonPath("$._links").exists()) } + @Test + fun `should create pickmany question and return 201`() { + val json = factory.validCreatePickManyRequest() + mockMvc.perform( + post(postUrl() + "/pick-many").contentType(MediaType.APPLICATION_JSON).content(json) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ) + .andDo(print()) + .andExpect(status().isCreated) + .andExpect(jsonPath("$.systematicStudyId").value(systematicStudyId.toString())) + .andExpect(jsonPath("$._links").exists()) + } + @Test fun `should create labeledscale question and return 201`() { val json = factory.validCreateLabeledScaleRequest() @@ -155,6 +168,22 @@ class ExtractionQuestionControllerTest( } + @Test + fun `should find pickmany question and return 200`() { + val question = factory.validCreatePickManyQuestionDocument(questionId, systematicStudyId) + + repository.insert(question) + + val questionIdUrl = "/${questionId}" + mockMvc.perform(get(getUrl(questionIdUrl)).contentType(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ) + + .andExpect(status().isOk) + .andExpect(jsonPath("$.systematicStudyId").value(question.systematicStudyId.toString())) + .andExpect(jsonPath("$._links").exists()) + } + @Test fun `should find labeled scale question and return 200`() { val question = factory.validCreateLabeledScaleQuestionDocument(questionId, systematicStudyId) @@ -245,6 +274,17 @@ class ExtractionQuestionControllerTest( ).andExpect(status().isBadRequest) } + @Test + fun `should not create pickmany question with invalid input and return 400`() { + val json = factory.invalidCreatePickManyRequest() + mockMvc.perform( + post(postUrl() + "/pick-many") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ).andExpect(status().isBadRequest) + } + @Test fun `should not create labeled scale question with invalid input and return 400`() { val json = factory.invalidCreateLabeledScaleRequest() 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 2ea33c5e5..50b4591eb 100644 --- a/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt +++ b/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt @@ -102,6 +102,20 @@ class RiskOfBiasQuestionControllerTest( .andExpect(jsonPath("$._links").exists()) } + @Test + fun `should create pickmany question and return 201`() { + val json = factory.validCreatePickManyRequest() + mockMvc.perform( + post(postUrl() + "/pick-many").contentType(MediaType.APPLICATION_JSON).content(json) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ) + + .andDo(print()) + .andExpect(status().isCreated) + .andExpect(jsonPath("$.systematicStudyId").value(systematicStudyId.toString())) + .andExpect(jsonPath("$._links").exists()) + } + @Test fun `should create labeled scale question and return 201`() { val json = factory.validCreateLabeledScaleRequest() @@ -163,6 +177,21 @@ class RiskOfBiasQuestionControllerTest( } + @Test + fun `should find pickmany question and return 200`() { + val question = factory.validCreatePickManyQuestionDocument(questionId, systematicStudyId) + + repository.insert(question) + + val questionIdUrl = "/${questionId}" + mockMvc.perform(get(getUrl(questionIdUrl)).contentType(MediaType.APPLICATION_JSON) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.systematicStudyId").value(question.systematicStudyId.toString())) + .andExpect(jsonPath("$._links").exists()) + } + @Test fun `should find labeled scale question and return 200`() { val question = factory.validCreateLabeledScaleQuestionDocument(questionId, systematicStudyId) @@ -255,6 +284,17 @@ class RiskOfBiasQuestionControllerTest( ).andExpect(status().isBadRequest) } + @Test + fun `should not create pickmany question with invalid input and return 400`() { + val json = factory.invalidCreatePickManyRequest() + mockMvc.perform( + post(postUrl() + "/pick-many") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ).andExpect(status().isBadRequest) + } + @Test fun `should not create labeled scale question with invalid input and return 400`() { val json = factory.invalidCreateLabeledScaleRequest() diff --git a/web/src/test/kotlin/br/all/question/utils/TestDataFactory.kt b/web/src/test/kotlin/br/all/question/utils/TestDataFactory.kt index 2caa8698f..09a783414 100644 --- a/web/src/test/kotlin/br/all/question/utils/TestDataFactory.kt +++ b/web/src/test/kotlin/br/all/question/utils/TestDataFactory.kt @@ -42,6 +42,22 @@ class TestDataFactory { } """ + fun validCreatePickManyRequest(researcher: UUID = researcherId, systematicStudyId: UUID = this.systematicStudyId) = + """ + { + "researcherId": "$researcher", + "systematicStudyId": "$systematicStudyId", + "type": "PICK_MANY", + "code": "${faker.lorem.words()}", + "description": "${faker.paragraph(8)}", + "options": [ + "${faker.lorem.words()}", + "${faker.lorem.words()}", + "${faker.lorem.words()}" + ] + } + """ + fun validCreateLabeledScaleRequest(researcher: UUID = researcherId, systematicStudyId: UUID = this.systematicStudyId) = """ { @@ -94,6 +110,21 @@ class TestDataFactory { } """ + fun invalidCreatePickManyRequest(researcher: UUID = researcherId, systematicStudyId: UUID = this.systematicStudyId) = + """ + { + "researcherId": "$researcher", + "systematicStudyId": "$systematicStudyId", + "type": "PICK_MANY", + "code": "${faker.lorem.words()}", + "description": "${faker.paragraph(8)}", + "options": [ + "${faker.lorem.words()}", + "" + ] + } + """ + fun invalidCreateLabeledScaleRequest(researcher: UUID = researcherId, systematicStudyId: UUID = this.systematicStudyId) = """ { @@ -158,6 +189,25 @@ class TestDataFactory { questionType ) + fun validCreatePickManyQuestionDocument( + questionId: UUID, + systematicStudyId: UUID, + code: String = faker.lorem.words(), + description: String = faker.lorem.words(), + questionType: QuestionContextEnum = QuestionContextEnum.ROB + ) = QuestionDocument( + questionId, + systematicStudyId, + code, + description, + "PICK_MANY", + null, + null, + null, + listOf(faker.lorem.words(), faker.lorem.words()), + questionType + ) + fun validCreateLabeledScaleQuestionDocument( questionId: UUID, systematicStudyId: UUID, diff --git a/web/src/test/kotlin/br/all/shared/TestHelperService.kt b/web/src/test/kotlin/br/all/shared/TestHelperService.kt index bf6d40fac..ba9b85ec7 100644 --- a/web/src/test/kotlin/br/all/shared/TestHelperService.kt +++ b/web/src/test/kotlin/br/all/shared/TestHelperService.kt @@ -25,6 +25,7 @@ class TestHelperService( fun createApplicationUser(): ApplicationUser { val userDto = UserAccountDto( id = UUID.randomUUID(), + name = faker.name.neutralFirstName(), username = faker.name.firstName() + "_" + UUID.randomUUID().toString().take(8), password = faker.fallout.locations(), email = faker.internet.email(), @@ -46,6 +47,7 @@ class TestHelperService( fun createUnauthorizedApplicationUser(): ApplicationUser { val userDto = UserAccountDto( id = UUID.randomUUID(), + name = faker.clashOfClans.troops(), username = faker.name.firstName() + "_" + UUID.randomUUID().toString().take(8), password = faker.fallout.locations(), email = faker.internet.email(),