From 603d361d4b08cb6a3ba4860fe07e86165efe5eb5 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Tue, 3 Jun 2025 11:47:59 -0300 Subject: [PATCH 01/74] build: fix kotlin version and update executions tag usage --- account/pom.xml | 18 ++---------------- pom.xml | 29 +++++++++++++++-------------- web/pom.xml | 2 +- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/account/pom.xml b/account/pom.xml index 34e1db50c..5ae3d627f 100644 --- a/account/pom.xml +++ b/account/pom.xml @@ -30,22 +30,7 @@ org.jetbrains.kotlin kotlin-maven-plugin - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - + true maven-surefire-plugin @@ -72,6 +57,7 @@ org.postgresql postgresql + 42.7.2 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0fba64dbb..4585c5d8a 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ UTF-8 official 1.8 + 1.9.0 @@ -46,18 +47,19 @@ org.jetbrains.kotlin kotlin-reflect + ${kotlin.version} compile org.jetbrains.kotlin kotlin-stdlib - 1.9.0 + ${kotlin.version} compile org.jetbrains.kotlin kotlin-test-junit5 - 1.9.0 + ${kotlin.version} test @@ -80,7 +82,7 @@ org.jetbrains.kotlin kotlin-test-junit - 1.9.0 + ${kotlin.version} test @@ -108,24 +110,20 @@ org.jetbrains.kotlin kotlin-maven-plugin - 1.9.0 + ${kotlin.version} - - org.mapstruct - mapstruct-processor - 1.4.2.Final - org.jetbrains.kotlin kotlin-maven-allopen - 2.0.0-RC1 + ${kotlin.version} org.jetbrains.kotlin kotlin-maven-noarg - 2.0.0-RC1 + ${kotlin.version} + + true spring jpa - - spring - diff --git a/web/pom.xml b/web/pom.xml index 4d14b3ac6..f8203f80e 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -18,7 +18,7 @@ 17 - 1.8.22 + 1.9.0 From 33ae8821b5d30bdb93817e255288c553ab07c86b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 3 Jun 2025 12:50:55 -0300 Subject: [PATCH 02/74] build: enable actions file --- .github/workflows/{ci.yml.disabled => ci.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{ci.yml.disabled => ci.yml} (100%) diff --git a/.github/workflows/ci.yml.disabled b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml.disabled rename to .github/workflows/ci.yml From 97e2c8ed60f695aa811c5170e264e2d1db1feb5c Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 3 Jun 2025 12:51:39 -0300 Subject: [PATCH 03/74] test: use fixed data instead of faker generation --- .../br/all/protocol/controller/ProtocolControllerTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt index 317efff83..79d9a3e01 100644 --- a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt +++ b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt @@ -1,5 +1,6 @@ package br.all.protocol.controller +import br.all.application.protocol.repository.CriterionDto import br.all.infrastructure.protocol.MongoProtocolRepository import br.all.infrastructure.review.MongoSystematicStudyRepository import br.all.infrastructure.review.SystematicStudyDocument @@ -153,7 +154,11 @@ class ProtocolControllerTest( @Test fun `should update an existing protocol without deleting existing collection-type variables`() { - val document = factory.createProtocolDocument() + val document = factory.createProtocolDocument( + keywords = setOf("keyword1", "keyword2"), + robQuestions = setOf(UUID.randomUUID(), UUID.randomUUID()), + selectionCriteria = setOf(CriterionDto("desc", "INCLUSION")) + ) val json = factory.validPutRequest() protocolRepository.save(document) From 3799557a2fe7166aa8b89f01cd461f7737a5d430 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Tue, 3 Jun 2025 15:22:30 -0300 Subject: [PATCH 04/74] =?UTF-8?q?build:=20git=20r=C3=A3bi=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b96aca1da..733eec78e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI with Maven 2025 on: push: - branches: [ "main" ] + branches: [ "fix-pom" ] pull_request: branches: [ "main" ] From b2231c903e8fa0cfcbdcde6f65f6d08b404d1213 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Tue, 3 Jun 2025 15:25:42 -0300 Subject: [PATCH 05/74] =?UTF-8?q?commit=20jo=C3=A3o=20txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test-actions.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-actions.txt b/test-actions.txt index af8ea0e99..0d133db18 100644 --- a/test-actions.txt +++ b/test-actions.txt @@ -1,2 +1 @@ -this is test push -testing push 2 \ No newline at end of file +joão \ No newline at end of file From 4e05114e6d01192b05e363f61834607b5ccbe359 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Tue, 3 Jun 2025 15:54:56 -0300 Subject: [PATCH 06/74] build: test gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..03085867b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.bib linguist-generated From dfe945861b82d0e4bf62a6ddeeae2ed77ab013cf Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 4 Jun 2025 12:28:50 -0300 Subject: [PATCH 07/74] test: remove unused internal keyword from class --- .../src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt | 2 +- review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt index 29153d030..c216b0c64 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.* -internal class ProtocolIdTest{ +class ProtocolIdTest{ @Test fun `valid ProtocolId`() { val uuid = UUID.randomUUID() diff --git a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt index ac3109ab1..32234cf89 100644 --- a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue -internal class DOITest { +class DOITest { @Test fun `valid DOI should not throw an exception`() { assertDoesNotThrow { DOI("10.1590/1089-6891v16i428131") } From 9257c7c8059304b30abc6fe0005773f871599a8d Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 4 Jun 2025 13:15:19 -0300 Subject: [PATCH 08/74] test: add tags to tests that were missing --- .../src/test/kotlin/br/all/domain/user/UserAccountTest.kt | 2 ++ .../search/create/CreateSearchSessionServiceImplTest.kt | 6 ++++-- .../find/FindAllSearchSessionsBySourceServiceImplTest.kt | 3 +++ .../search/find/FindAllSearchSessionsServiceImplTest.kt | 3 +++ .../search/find/FindSearchSessionServiceImplTest.kt | 3 +++ .../search/update/UpdateSearchSessionServiceImplTest.kt | 3 +++ .../application/shared/presenter/PreconditionCheckerTest.kt | 2 ++ .../src/test/kotlin/br/all/architecture/DomainModuleTest.kt | 3 ++- .../kotlin/br/all/domain/model/search/SearchSessionTest.kt | 2 ++ review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt | 2 ++ .../kotlin/br/all/domain/model/study/StudyReviewTest.kt | 2 ++ .../br/all/domain/services/BibtexConverterServiceTest.kt | 2 ++ .../br/all/domain/services/ConverterFactoryServiceTest.kt | 3 +++ .../br/all/domain/services/ReviewSimilarityServiceTest.kt | 1 + .../br/all/domain/services/RisConverterServiceTest.kt | 2 ++ .../br/all/domain/services/ScoreCalculatorServiceTest.kt | 1 + .../src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt | 2 ++ .../src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt | 3 +++ .../test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt | 2 ++ .../kotlin/br/all/domain/shared/ddd/NotificationTest.kt | 2 ++ .../test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt | 2 ++ review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt | 1 + .../src/test/kotlin/br/all/domain/shared/utils/DOITest.kt | 2 ++ .../test/kotlin/br/all/domain/shared/utils/PhraseTest.kt | 2 ++ .../br/all/infrastructure/ProtocolRepositoryImplTest.kt | 4 ++-- .../br/all/protocol/controller/ProtocolControllerTest.kt | 1 + .../all/protocol/persistence/MongoProtocolRepositoryTest.kt | 1 + .../question/controller/ExtractionQuestionControllerTest.kt | 1 + .../question/controller/RiskOfBiasQuestionControllerTest.kt | 1 + .../all/question/persistence/MongoQuestionRepositoryTest.kt | 2 ++ .../kotlin/br/all/report/controller/ReportControllerTest.kt | 1 + .../all/review/controller/SystematicStudyControllerTest.kt | 1 + .../persistence/MongoSystematicStudyRepositoryTest.kt | 1 + .../br/all/search/controller/SearchSessionControllerTest.kt | 2 ++ .../search/persistence/MongoSearchSessionRepositoryTest.kt | 2 ++ web/src/test/kotlin/br/all/shared/TestHelperService.kt | 2 ++ .../br/all/study/controller/StudyReviewControllerTest.kt | 2 ++ .../all/study/persistence/MongoStudyReviewRepositoryTest.kt | 3 +++ 38 files changed, 75 insertions(+), 5 deletions(-) diff --git a/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt b/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt index fdb27d3ee..25a1e4b8e 100644 --- a/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt +++ b/account/src/test/kotlin/br/all/domain/user/UserAccountTest.kt @@ -2,11 +2,13 @@ package br.all.domain.user import io.github.serpro69.kfaker.Faker import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Tag import java.time.LocalDateTime import java.util.UUID import kotlin.test.Test import kotlin.test.assertFailsWith +@Tag("UnitTest") class UserAccountTest{ private val faker = Faker() diff --git a/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt index cc4a200c3..42a8eec8a 100644 --- a/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/search/create/CreateSearchSessionServiceImplTest.kt @@ -1,8 +1,7 @@ +package br.all.application.search.create import br.all.application.protocol.repository.ProtocolRepository import br.all.application.review.repository.SystematicStudyRepository -import br.all.application.search.create.CreateSearchSessionServiceImpl -import br.all.application.search.create.CreateSearchSessionPresenter import br.all.application.search.util.TestDataFactory import br.all.application.search.repository.SearchSessionRepository import br.all.application.shared.exceptions.EntityNotFoundException @@ -23,9 +22,12 @@ import io.mockk.junit5.MockKExtension import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.extension.ExtendWith import kotlin.test.Test +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class CreateSearchSessionServiceImplTest { diff --git a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt index 43fc2453b..3ed9029cf 100644 --- a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsBySourceServiceImplTest.kt @@ -17,9 +17,12 @@ import io.mockk.verify import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class FindAllSearchSessionsBySourceServiceImplTest { diff --git a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt index 6efa7829d..2ebb67cba 100644 --- a/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/search/find/FindAllSearchSessionsServiceImplTest.kt @@ -16,9 +16,12 @@ import io.mockk.verify import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class FindAllSearchSessionsServiceImplTest { diff --git a/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt index 19ce02703..fae5ee1dc 100644 --- a/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/search/find/FindSearchSessionServiceImplTest.kt @@ -17,9 +17,12 @@ import io.mockk.verify import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class FindSearchSessionServiceImplTest { @MockK(relaxUnitFun = true) diff --git a/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt index 43a701154..217307515 100644 --- a/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/search/update/UpdateSearchSessionServiceImplTest.kt @@ -16,9 +16,12 @@ import io.mockk.junit5.MockKExtension import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class UpdateSearchSessionServiceImplTest { diff --git a/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt b/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt index 9bb78ed4f..c69609d0a 100644 --- a/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt +++ b/review/src/test/kotlin/br/all/application/shared/presenter/PreconditionCheckerTest.kt @@ -11,9 +11,11 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import java.util.* +@Tag("UnitTest") class PreconditionCheckerTest { private lateinit var presenter: GenericPresenter<*> diff --git a/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt b/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt index f985969ae..1d41e1d00 100644 --- a/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt +++ b/review/src/test/kotlin/br/all/architecture/DomainModuleTest.kt @@ -6,8 +6,9 @@ import com.tngtech.archunit.junit.AnalyzeClasses import com.tngtech.archunit.junit.ArchTest import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes +import org.junit.jupiter.api.Tag - +@Tag("ArchitectureTest") @AnalyzeClasses(packages = ["br.all"], importOptions = [ImportOption.DoNotIncludeTests::class]) class DomainModuleTest { diff --git a/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt b/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt index edb3eada2..dcb7de383 100644 --- a/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt +++ b/review/src/test/kotlin/br/all/domain/model/search/SearchSessionTest.kt @@ -3,6 +3,7 @@ package br.all.domain.model.search import br.all.domain.model.protocol.SearchSource import br.all.domain.model.user.ResearcherId import br.all.domain.model.review.SystematicStudyId +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.time.LocalDateTime @@ -10,6 +11,7 @@ import java.util.* import kotlin.test.assertEquals +@Tag("UnitTest") class SearchSessionTest { diff --git a/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt b/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt index d0845449c..6655ecc1c 100644 --- a/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt +++ b/review/src/test/kotlin/br/all/domain/model/study/DoiTest.kt @@ -1,8 +1,10 @@ package br.all.domain.model.study import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Tag import kotlin.test.Test +@Tag("UnitTest") class DoiTest { @Test diff --git a/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt b/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt index 1a6b27d17..1cfacf2e8 100644 --- a/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt +++ b/review/src/test/kotlin/br/all/domain/model/study/StudyReviewTest.kt @@ -7,10 +7,12 @@ import br.all.domain.model.search.SearchSessionID import br.all.domain.shared.utils.paragraph import br.all.domain.shared.utils.year import io.github.serpro69.kfaker.Faker +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.assertAll import java.util.* import kotlin.test.* +@Tag("UnitTest") class StudyReviewTest { private val faker = Faker() diff --git a/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt index 7cce5feba..7b226fa5d 100644 --- a/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt +++ b/review/src/test/kotlin/br/all/domain/services/BibtexConverterServiceTest.kt @@ -8,6 +8,8 @@ import java.util.* import kotlin.test.assertEquals import kotlin.test.assertTrue +@Tag("UnitTest") +@Tag("ServiceTest") class BibtexConverterServiceTest { private lateinit var sut: BibtexConverterService diff --git a/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt index 3964710f2..0242ecd11 100644 --- a/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt +++ b/review/src/test/kotlin/br/all/domain/services/ConverterFactoryServiceTest.kt @@ -5,10 +5,13 @@ import br.all.domain.model.search.SearchSessionID import br.all.domain.services.RisTestData.testInput as risInput import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import java.util.* import kotlin.test.assertEquals +@Tag("IntegrationTest") +@Tag("ServiceTest") class ConverterFactoryServiceTest { private lateinit var sut: ConverterFactoryService private lateinit var bibtexConverterService: BibtexConverterService diff --git a/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt index d008b73ab..72993f0fd 100644 --- a/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt +++ b/review/src/test/kotlin/br/all/domain/services/ReviewSimilarityServiceTest.kt @@ -16,6 +16,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @Tag("IntegrationTest") +@Tag("ServiceTest") class ReviewSimilarityServiceTest { private lateinit var sut: ReviewSimilarityService diff --git a/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt index e3ecaa20d..dc1eda98d 100644 --- a/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt +++ b/review/src/test/kotlin/br/all/domain/services/RisConverterServiceTest.kt @@ -9,6 +9,8 @@ import java.util.* import kotlin.test.assertEquals import kotlin.test.assertTrue +@Tag("UnitTest") +@Tag("ServiceTest") class RisConverterServiceTest { private lateinit var sut: RisConverterService private lateinit var idGeneratorService: IdGeneratorService diff --git a/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt b/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt index ef35805fa..228c2f31d 100644 --- a/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt +++ b/review/src/test/kotlin/br/all/domain/services/ScoreCalculatorServiceTest.kt @@ -11,6 +11,7 @@ import java.util.* import kotlin.test.assertEquals @Tag("IntegrationTest") +@Tag("ServiceTest") class ScoreCalculatorServiceTest { private lateinit var sut: ScoreCalculatorService diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt index 05f63ee8b..1f3348c04 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt @@ -1,6 +1,7 @@ package br.all.domain.shared.ddd import br.all.domain.shared.valueobject.Email +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -8,6 +9,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue +@Tag("UnitTest") class EmailTest { @Test diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt index cc3e586c9..bbf2ae3b7 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/EntityTest.kt @@ -1,8 +1,11 @@ package br.all.domain.shared.ddd import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test +@Tag("UnitTest") class EntityTest{ @JvmInline diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt index aa1ae7d8b..cb82af57a 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/LanguageTest.kt @@ -1,11 +1,13 @@ package br.all.domain.shared.ddd import br.all.domain.shared.valueobject.Language +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals +@Tag("UnitTest") class LanguageTest { @Test diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt index 404ac1d06..37ed02b9b 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/NotificationTest.kt @@ -1,8 +1,10 @@ package br.all.domain.shared.ddd import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test +@Tag("UnitTest") class NotificationTest { @Test fun `Should notification with no errors present no error message`(){ diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt index c216b0c64..facc535a6 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/ProtocolIdTest.kt @@ -3,10 +3,12 @@ package br.all.domain.shared.ddd import br.all.domain.model.protocol.ProtocolId import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.util.* +@Tag("UnitTest") class ProtocolIdTest{ @Test fun `valid ProtocolId`() { diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt index e77cfb994..29df084e3 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/TextTest.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.* import kotlin.test.assertEquals import kotlin.test.assertNotEquals +@Tag("UnitTest") class TextTest { @Test diff --git a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt index 32234cf89..ec2a9d83c 100644 --- a/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/utils/DOITest.kt @@ -2,6 +2,7 @@ package br.all.domain.shared.utils import br.all.domain.shared.valueobject.DOI +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -9,6 +10,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertTrue +@Tag("UnitTest") class DOITest { @Test fun `valid DOI should not throw an exception`() { diff --git a/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt b/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt index 8e242cacd..6b3878563 100644 --- a/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/utils/PhraseTest.kt @@ -1,10 +1,12 @@ package br.all.domain.shared.utils +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals +@Tag("UnitTest") class PhraseTest { @Test fun `Should accept words with only lowercase letters`() { diff --git a/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt b/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt index 5c1269c24..b281a2cc8 100644 --- a/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt +++ b/review/src/test/kotlin/br/all/infrastructure/ProtocolRepositoryImplTest.kt @@ -18,8 +18,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.util.* -@Tag("UnitTests") -@Tag("ServiceTests") +@Tag("UnitTest") +@Tag("ServiceTest") @ExtendWith(MockKExtension::class) class ProtocolRepositoryImplTest { @MockK diff --git a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt index 79d9a3e01..e305b0a7e 100644 --- a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt +++ b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt @@ -24,6 +24,7 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") +@Tag("ControllerTest") @DisplayName("Protocol Controller Integration Tests") class ProtocolControllerTest( @Autowired private val protocolRepository: MongoProtocolRepository, diff --git a/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt b/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt index fee36a63e..1c8f03b11 100644 --- a/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt +++ b/web/src/test/kotlin/br/all/protocol/persistence/MongoProtocolRepositoryTest.kt @@ -10,6 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest @Tag("IntegrationTest") +@Tag("RepositoryTest") class MongoProtocolRepositoryTest( @Autowired private val sut: MongoProtocolRepository, ) { diff --git a/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt b/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt index 87fb4da8a..f9b441db8 100644 --- a/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt +++ b/web/src/test/kotlin/br/all/question/controller/ExtractionQuestionControllerTest.kt @@ -22,6 +22,7 @@ import java.util.* @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") +@Tag("ControllerTest") class ExtractionQuestionControllerTest( @Autowired val repository: MongoQuestionRepository, @Autowired val systematicStudyRepository: MongoSystematicStudyRepository, diff --git a/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt b/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt index 71f5f4ff0..2ea33c5e5 100644 --- a/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt +++ b/web/src/test/kotlin/br/all/question/controller/RiskOfBiasQuestionControllerTest.kt @@ -22,6 +22,7 @@ import java.util.* @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") +@Tag("ControllerTest") class RiskOfBiasQuestionControllerTest( @Autowired val repository: MongoQuestionRepository, @Autowired val systematicStudyRepository: MongoSystematicStudyRepository, diff --git a/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt b/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt index 53c818e05..78c7ddb5c 100644 --- a/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt +++ b/web/src/test/kotlin/br/all/question/persistence/MongoQuestionRepositoryTest.kt @@ -13,6 +13,8 @@ import org.springframework.boot.test.context.SpringBootTest import java.util.UUID @SpringBootTest +@Tag("IntegrationTest") +@Tag("RepositoryTest") class MongoQuestionRepositoryTest( @Autowired private val sut: MongoQuestionRepository, ) { diff --git a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt index 382b3dbed..295b76c0d 100644 --- a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt +++ b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt @@ -31,6 +31,7 @@ import br.all.protocol.shared.TestDataFactory as ProtocolTestDataFactory @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") +@Tag("ControllerTest") @DisplayName("Report Controller Integration Tests") class ReportControllerTest( @Autowired private val studyReviewRepository: MongoStudyReviewRepository, diff --git a/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt b/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt index facd73d72..43224dc85 100644 --- a/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt +++ b/web/src/test/kotlin/br/all/review/controller/SystematicStudyControllerTest.kt @@ -23,6 +23,7 @@ import java.util.* @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") +@Tag("ControllerTest") class SystematicStudyControllerTest( @Autowired private val repository: MongoSystematicStudyRepository, @Autowired private val testHelperService: TestHelperService, diff --git a/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt b/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt index 1a81d5f4a..c8c8cf4a6 100644 --- a/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt +++ b/web/src/test/kotlin/br/all/review/persistence/MongoSystematicStudyRepositoryTest.kt @@ -11,6 +11,7 @@ import java.util.* @SpringBootTest @Tag("IntegrationTest") +@Tag("RepositoryTest") class MongoSystematicStudyRepositoryTest( @Autowired private val sut: MongoSystematicStudyRepository, ) { diff --git a/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt b/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt index 2aef8a289..1d3fe1a22 100644 --- a/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt +++ b/web/src/test/kotlin/br/all/search/controller/SearchSessionControllerTest.kt @@ -29,6 +29,8 @@ import java.util.* @SpringBootTest @AutoConfigureMockMvc +@Tag("IntegrationTest") +@Tag("ControllerTest") class SearchSessionControllerTest( @Autowired val repository: MongoSearchSessionRepository, @Autowired val systematicStudyRepository: MongoSystematicStudyRepository, diff --git a/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt b/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt index fbeba7400..2f62a0fe3 100644 --- a/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt +++ b/web/src/test/kotlin/br/all/search/persistence/MongoSearchSessionRepositoryTest.kt @@ -8,6 +8,8 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import java.util.* @SpringBootTest +@Tag("IntegrationTest") +@Tag("RepositoryTest") class MongoSearchSessionRepositoryTest( @Autowired private val sut: MongoSearchSessionRepository, ) { diff --git a/web/src/test/kotlin/br/all/shared/TestHelperService.kt b/web/src/test/kotlin/br/all/shared/TestHelperService.kt index fd86578cb..bf6d40fac 100644 --- a/web/src/test/kotlin/br/all/shared/TestHelperService.kt +++ b/web/src/test/kotlin/br/all/shared/TestHelperService.kt @@ -4,6 +4,7 @@ import br.all.application.user.repository.UserAccountDto import br.all.application.user.repository.UserAccountRepository import br.all.security.service.ApplicationUser import io.github.serpro69.kfaker.Faker +import org.junit.jupiter.api.Tag import org.springframework.http.MediaType import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors import org.springframework.stereotype.Service @@ -14,6 +15,7 @@ import java.time.LocalDateTime import java.util.* @Service +@Tag("UnitTest") class TestHelperService( private val repository: UserAccountRepository, ) { diff --git a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt index aab3d409f..4735a7e39 100644 --- a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt +++ b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt @@ -27,6 +27,8 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory @SpringBootTest @AutoConfigureMockMvc +@Tag("IntegrationTest") +@Tag("ControllerTest") class StudyReviewControllerTest( @Autowired val repository: MongoStudyReviewRepository, @Autowired val systematicStudyRepository: MongoSystematicStudyRepository, diff --git a/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt b/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt index 1278542e3..1ebed0a45 100644 --- a/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt +++ b/web/src/test/kotlin/br/all/study/persistence/MongoStudyReviewRepositoryTest.kt @@ -8,12 +8,15 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import java.util.UUID @SpringBootTest +@Tag("IntegrationTest") +@Tag("RepositoryTest") class MongoStudyReviewRepositoryTest ( @Autowired private val sut: MongoStudyReviewRepository, @Autowired private val idService: StudyReviewIdGeneratorService From 06800f1015cb18931bf0c880ee81ad8a30fcd910 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 18:07:08 -0300 Subject: [PATCH 09/74] build: update spring-boot-starter-parent version --- pom.xml | 2 +- review/pom.xml | 1 - web/pom.xml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 4585c5d8a..da369b719 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.2 + 3.4.4 diff --git a/review/pom.xml b/review/pom.xml index 6c9336ec0..5d782b870 100644 --- a/review/pom.xml +++ b/review/pom.xml @@ -21,7 +21,6 @@ org.springframework.boot spring-boot-starter-data-mongodb - 3.1.2 org.springframework diff --git a/web/pom.xml b/web/pom.xml index f8203f80e..588d0e033 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.2 + 3.4.4 From 48737449e2a70ea12f8ab55ca3a6b7da3f0e6bdc Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 18:59:00 -0300 Subject: [PATCH 10/74] chore: add config to ignore tex files --- .gitattributes | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 03085867b..5cbdc074c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -*.bib linguist-generated +*.tex linguist-detectable=false +*.bib linguist-detectable=false +*.bibtex linguist-detectable=false From bc7e6c38cca24acf0b0d4252e9d9857f279ccaeb Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:14:46 -0300 Subject: [PATCH 11/74] feat: add find criteria by study endpoint --- .../all/report/controller/ReportController.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/web/src/main/kotlin/br/all/report/controller/ReportController.kt b/web/src/main/kotlin/br/all/report/controller/ReportController.kt index 7168c0f06..be2394a3a 100644 --- a/web/src/main/kotlin/br/all/report/controller/ReportController.kt +++ b/web/src/main/kotlin/br/all/report/controller/ReportController.kt @@ -3,6 +3,7 @@ package br.all.report.controller import br.all.application.report.find.service.* import br.all.report.presenter.* import br.all.security.service.AuthenticationInfoService +import br.all.report.presenter.RestfulFindStudyReviewCriteriaPresenter import br.all.utils.LinksFactory import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content @@ -27,6 +28,7 @@ class ReportController( private val authorNetworkService: AuthorNetworkService, private val findKeywordsService: FindKeywordsService, private val findStudiesByStageService: FindStudiesByStageService, + private val findStudyReviewCriteriaService: FindStudyReviewCriteriaService, private val exportProtocolService: ExportProtocolService, private val findAnswerService: FindAnswerServiceImpl, private val studiesFunnelService: StudiesFunnelService, @@ -350,4 +352,32 @@ class ReportController( findAnswerService.find(presenter, request) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @GetMapping("/study-review/{studyReview}/criteria") + @Operation(summary = "Return inclusion and exclusion criteria of study review") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", description = "Success getting criteria of a review", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = FindStudyReviewCriteriaService.ResponseModel::class) + )] + ), + ApiResponse(responseCode = "401", description = "Fail getting criteria of a review - unauthenticated user", + content = [Content(schema = Schema(hidden = true))]), + ApiResponse(responseCode = "403", description = "Fail getting criteria of a review - unauthorized user", + content = [Content(schema = Schema(hidden = true))]), + ] + ) + fun findCriteria( + @PathVariable systematicStudyId: UUID, + @PathVariable studyReview: Long + ): ResponseEntity<*> { + val presenter = RestfulFindStudyReviewCriteriaPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = FindStudyReviewCriteriaService.RequestModel(userId, systematicStudyId, studyReview) + findStudyReviewCriteriaService.findCriteria(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } \ No newline at end of file From b868df20856f6c10e7c64e5e4711061888afebd4 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:15:17 -0300 Subject: [PATCH 12/74] feat: find criteria by study endpoint presenter --- .../find/presenter/FindStudyReviewCriteriaPresenter.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt diff --git a/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt b/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt new file mode 100644 index 000000000..90e2ad4cf --- /dev/null +++ b/review/src/main/kotlin/br/all/application/report/find/presenter/FindStudyReviewCriteriaPresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.report.find.presenter + +import br.all.application.shared.presenter.GenericPresenter +import br.all.application.report.find.service.FindStudyReviewCriteriaService + +interface FindStudyReviewCriteriaPresenter: GenericPresenter \ No newline at end of file From 46e0f28aa5aa99ebb8d84d3fcd9f20cd1f17db67 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:15:25 -0300 Subject: [PATCH 13/74] feat: find criteria by study endpoint service --- .../service/FindStudyReviewCriteriaService.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt diff --git a/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt new file mode 100644 index 000000000..e97df28f4 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaService.kt @@ -0,0 +1,22 @@ +package br.all.application.report.find.service + +import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter +import java.util.* + +interface FindStudyReviewCriteriaService { + fun findCriteria(presenter: FindStudyReviewCriteriaPresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyReviewId: Long + ) + + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyReviewId: Long, + val inclusionCriteria: Set, + val exclusionCriteria: Set + ) +} \ No newline at end of file From 73b7a68f47540fe0df5fa6919c6044a66cdfd9b5 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:15:29 -0300 Subject: [PATCH 14/74] feat: find criteria by study endpoint impl --- .../FindStudyReviewCriteriaServiceImpl.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt new file mode 100644 index 000000000..f1b8bc5ac --- /dev/null +++ b/review/src/main/kotlin/br/all/application/report/find/service/FindStudyReviewCriteriaServiceImpl.kt @@ -0,0 +1,60 @@ +package br.all.application.report.find.service + +import br.all.application.protocol.repository.CriterionDto +import br.all.application.protocol.repository.ProtocolRepository +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter +import br.all.application.study.repository.StudyReviewDto +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.user.CredentialsService +import br.all.domain.model.review.SystematicStudy + +class FindStudyReviewCriteriaServiceImpl( + private val systematicStudyRepository: SystematicStudyRepository, + private val studyReviewRepository: StudyReviewRepository, + private val credentialsService: CredentialsService, + private val protocolRepository: ProtocolRepository, +): FindStudyReviewCriteriaService { + override fun findCriteria( + presenter: FindStudyReviewCriteriaPresenter, + request: FindStudyReviewCriteriaService.RequestModel + ) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + + if(presenter.isDone()) return + + val criteriaSet = protocolRepository.findById(request.systematicStudyId)?.eligibilityCriteria?: emptyList() + + val studyReview = studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId)!! + + val inclusionCriteria = getCriteriaByType(criteriaSet, studyReview, "INCLUSION") + val exclusionCriteria = getCriteriaByType(criteriaSet, studyReview, "EXCLUSION") + + val response = FindStudyReviewCriteriaService.ResponseModel( + userId = request.userId, + systematicStudyId = request.systematicStudyId, + studyReviewId = request.studyReviewId, + inclusionCriteria = inclusionCriteria, + exclusionCriteria = exclusionCriteria, + ) + + presenter.prepareSuccessView(response) + } + + private fun getCriteriaByType( + criteriaSet: Collection, + studyReview: StudyReviewDto, + type: String + ): Set { + return criteriaSet + .filter { it.type == type && it.description in studyReview.criteria } + .map { it.description }.toSet() + } +} \ No newline at end of file From c9c5835ed846fb2ac7094c2577d42aa8ef381615 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:15:36 -0300 Subject: [PATCH 15/74] feat: find criteria by study endpoint link factory --- web/src/main/kotlin/br/all/utils/LinksFactory.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt index ce4765c8b..dcea9b1fe 100644 --- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt +++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt @@ -316,5 +316,12 @@ class LinksFactory { questionId ) }.withRel("find-answer").withType("GET") -} + fun findStudyReviewCriteria(systematicStudyId: UUID, studyReviewId: Long): Link = + linkTo { + findCriteria( + systematicStudyId, + studyReviewId + ) + }.withRel("find-study-criteria").withType("GET") +} From 787d7e89826184b392751ec2c0533b8a7723b520 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:15:42 -0300 Subject: [PATCH 16/74] feat: find criteria by study endpoint bean --- .../controller/ReportControllerConfiguration.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt b/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt index 5712b44cf..2edc0fc1c 100644 --- a/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt +++ b/web/src/main/kotlin/br/all/report/controller/ReportControllerConfiguration.kt @@ -137,4 +137,17 @@ class ReportControllerConfiguration { systematicStudyRepository, questionRepository ) + + @Bean + fun findStudyReviewCriteriaService( + systematicStudyRepository: SystematicStudyRepository, + studyReviewRepository: StudyReviewRepository, + credentialsService: CredentialsService, + protocolRepository: ProtocolRepository, + ) = FindStudyReviewCriteriaServiceImpl( + systematicStudyRepository, + studyReviewRepository, + credentialsService, + protocolRepository + ) } \ No newline at end of file From b0c7be440a857d1f88c7710f0fda20e05db523f1 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:16:02 -0300 Subject: [PATCH 17/74] test: find criteria by study endpoint happy path test --- .../report/controller/ReportControllerTest.kt | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt index 295b76c0d..bd3d94aac 100644 --- a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt +++ b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt @@ -27,7 +27,6 @@ import br.all.review.shared.TestDataFactory as SystematicStudyTestDataFactory import br.all.study.utils.TestDataFactory as StudyReviewTestDataFactory import br.all.protocol.shared.TestDataFactory as ProtocolTestDataFactory - @SpringBootTest @AutoConfigureMockMvc @Tag("IntegrationTest") @@ -119,6 +118,9 @@ class ReportControllerTest( private fun studiesFunnelUrl() = "$baseReportUrl/studies-funnel" + private fun findStudyReviewCriteria(studyReviewId: Long) = + "$baseReportUrl/study-review/$studyReviewId/criteria" + private fun findKeywordsUrl(filter: String?) = if (filter.isNullOrBlank()) "$baseReportUrl/keywords" @@ -695,4 +697,37 @@ class ReportControllerTest( } } } + + @Nested + @DisplayName("When finding criteria of a study review") + inner class WhenFindingCriteriaOfAStudyReview { + @Test + fun `should return 200 and find correctly the criteria`() { + val systematicStudyId = systematicStudy.id + val studyReviewId = 1_000L + + val studyReview = studyReviewDataFactory.reviewDocument( + criteria = setOf("criteria 1", "criteria 2"), + systematicStudyId = systematicStudyId, + studyReviewId = studyReviewId, + ) + studyReviewRepository.save(studyReview) + + val criteria1 = protocolDataFactory.createCriteria("INCLUSION", "criteria 1") + + val criteria2 = protocolDataFactory.createCriteria("EXCLUSION", "criteria 2") + + val protocol = protocolDataFactory.createProtocolDocument( + id = systematicStudyId, + selectionCriteria = setOf(criteria1, criteria2) + ) + protocolRepository.save(protocol) + + mockMvc.perform( + get(findStudyReviewCriteria(studyReviewId = studyReviewId)) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + ) + .andExpect(status().isOk) + } + } } From bfdfd07f975e13737a2b0d110085cd496bf4a1f3 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:16:22 -0300 Subject: [PATCH 18/74] test: create function to generate criteria --- .../test/kotlin/br/all/protocol/shared/TestDataFactory.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt b/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt index a0da57f75..0a47a7b1e 100644 --- a/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt +++ b/web/src/test/kotlin/br/all/protocol/shared/TestDataFactory.kt @@ -62,6 +62,14 @@ class TestDataFactory { picoc, ) + fun createCriteria( + type: String, + description: String, + ) = CriterionDto( + type = type, + description = description, + ) + private fun eligibilityCriteria() = List(5) { CriterionDto(faker.paragraph(5), faker.random.randomValue(listOf("INCLUSION", "EXCLUSION"))) }.toSet() From e6dabd92bc7c55c8562d5cb8af332afcfef29368 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:16:39 -0300 Subject: [PATCH 19/74] test: remove unused imports and make variable private --- web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt b/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt index 0f7fe576d..bf5ded6e2 100644 --- a/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt +++ b/web/src/test/kotlin/br/all/review/shared/TestDataFactory.kt @@ -1,14 +1,12 @@ package br.all.review.shared -import br.all.application.user.repository.UserAccountDto import br.all.infrastructure.review.SystematicStudyDocument import io.github.serpro69.kfaker.Faker -import java.time.LocalDateTime import java.util.* class TestDataFactory { private val faker = Faker() - val researcherId: UUID = UUID.randomUUID() + private val researcherId: UUID = UUID.randomUUID() val systematicStudyId: UUID = UUID.randomUUID() val ownerId: UUID = UUID.randomUUID() From 907410996c10f53527735d4a77ba8dbcdfffd628 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:19:07 -0300 Subject: [PATCH 20/74] feat: restful implementation of presenter --- ...RestfulFindStudyReviewCriteriaPresenter.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt diff --git a/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt b/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt new file mode 100644 index 000000000..017ed9d37 --- /dev/null +++ b/web/src/main/kotlin/br/all/report/presenter/RestfulFindStudyReviewCriteriaPresenter.kt @@ -0,0 +1,37 @@ +package br.all.report.presenter + +import br.all.application.report.find.presenter.FindStudyReviewCriteriaPresenter +import br.all.application.report.find.service.FindStudyReviewCriteriaService +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.http.ResponseEntity.status + +class RestfulFindStudyReviewCriteriaPresenter( + private val linksFactory: LinksFactory +): FindStudyReviewCriteriaPresenter { + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: FindStudyReviewCriteriaService.ResponseModel) { + val restfulResponse = ViewModel( + response.exclusionCriteria, + response.inclusionCriteria + ) + + val selfRef = linksFactory.findStudyReviewCriteria(response.systematicStudyId, response.studyReviewId) + + restfulResponse.add(selfRef) + responseEntity = status(HttpStatus.OK).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable)= run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val exclusionCriteria: Set, + val inclusionCriteria: Set, + ) : RepresentationModel() +} \ No newline at end of file From da75ba22a0885fb038740f0e40012583c4291c22 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 13 Jun 2025 23:21:33 -0300 Subject: [PATCH 21/74] refactor: studyReviewId value --- .../br/all/report/controller/ReportControllerTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt index bd3d94aac..8c3cb88c4 100644 --- a/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt +++ b/web/src/test/kotlin/br/all/report/controller/ReportControllerTest.kt @@ -704,7 +704,12 @@ class ReportControllerTest( @Test fun `should return 200 and find correctly the criteria`() { val systematicStudyId = systematicStudy.id - val studyReviewId = 1_000L + // o intelli j tava reclamando que o valor do id era sempre o mesmo + // então é por isso que tem esse negócio ai, qualquer coisa só + // coloca val studyReviewId = 1000L + val seed = System.currentTimeMillis() + val studyReviewId = (seed * 31) % Long.MAX_VALUE + val studyReview = studyReviewDataFactory.reviewDocument( criteria = setOf("criteria 1", "criteria 2"), From 0c8ae63c8bacc84977cafd2c95ae0dba0ab1e5b4 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 17 Jun 2025 12:28:36 -0300 Subject: [PATCH 22/74] build: add sonarqube to github actions --- .github/workflows/sonarqube.yml | 36 +++++++++++++++++++++++++++++++++ pom.xml | 2 ++ 2 files changed, 38 insertions(+) create mode 100644 .github/workflows/sonarqube.yml diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 000000000..d1c691708 --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,36 @@ +name: SonarQube +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + name: Build and analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'zulu' # Alternative distribution options are available. + - name: Cache SonarQube packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build and analyze + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=pet-ads_systematic \ No newline at end of file diff --git a/pom.xml b/pom.xml index da369b719..693dbddf5 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,8 @@ official 1.8 1.9.0 + pet-ads + https://sonarcloud.io From 889a36530a080ece269bf8e0adbdc4605ef4d79a Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sat, 21 Jun 2025 16:13:54 -0300 Subject: [PATCH 23/74] chore(jpa): remove explicit Hibernate dialect configuration --- web/src/main/resources/application.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/main/resources/application.yaml b/web/src/main/resources/application.yaml index d5956b6bc..6e9a2abe7 100644 --- a/web/src/main/resources/application.yaml +++ b/web/src/main/resources/application.yaml @@ -21,7 +21,6 @@ spring: properties: hibernate: format_sql: true - database-platform: org.hibernate.dialect.PostgreSQLDialect jwt: key: "dasdasdadadasdasdadadasdasdadadasdasdadadasdasdada" From a2b0078ab507e361b2859593f66735976d1213bb Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sat, 21 Jun 2025 16:14:45 -0300 Subject: [PATCH 24/74] chore(deps): bump springdoc-openapi-starter-webmvc-ui to 2.8.9 and swagger-annotations to 2.2.31 --- review/pom.xml | 4 ++-- web/pom.xml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/review/pom.xml b/review/pom.xml index 5d782b870..920d13193 100644 --- a/review/pom.xml +++ b/review/pom.xml @@ -32,11 +32,11 @@ 1.0-SNAPSHOT compile + io.swagger.core.v3 swagger-annotations-jakarta - 2.2.19 - compile + 2.2.31 org.apache.commons diff --git a/web/pom.xml b/web/pom.xml index 588d0e033..5a2c513f7 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -38,10 +38,11 @@ org.springframework.boot spring-boot-starter-web + org.springdoc springdoc-openapi-starter-webmvc-ui - 2.3.0 + 2.8.9 org.springframework.boot From 34be8564ff60c4d3ff5874e5ece90b73733a1a40 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sat, 21 Jun 2025 22:09:53 -0300 Subject: [PATCH 25/74] chore: delete bogus unused file --- test-actions.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test-actions.txt diff --git a/test-actions.txt b/test-actions.txt deleted file mode 100644 index 0d133db18..000000000 --- a/test-actions.txt +++ /dev/null @@ -1 +0,0 @@ -joão \ No newline at end of file From bc13df23a89b1b6ce9eedbc45dd04ce831f60b8a Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sat, 21 Jun 2025 22:10:12 -0300 Subject: [PATCH 26/74] feat: add links to each search source --- .../presenter/RestfulFindAllStudyReviewsPresenter.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt index 16b363b89..6aab812d7 100644 --- a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsPresenter.kt @@ -4,11 +4,8 @@ import br.all.application.study.find.presenter.FindAllStudyReviewsPresenter import br.all.application.study.find.service.FindAllStudyReviewsService.ResponseModel import br.all.application.study.repository.StudyReviewDto import br.all.shared.error.createErrorResponseFrom -import br.all.study.controller.StudyReviewController -import br.all.study.requests.PostStudyReviewRequest import br.all.utils.LinksFactory import org.springframework.hateoas.RepresentationModel -import org.springframework.hateoas.server.mvc.linkTo import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component @@ -25,10 +22,13 @@ class RestfulFindAllStudyReviewsPresenter( val restfulResponse = ViewModel(response.systematicStudyId, response.studyReviews.size, response.studyReviews) val selfRef = linksFactory.findAllStudies(response.systematicStudyId) - val allBySource = linksFactory.findAllStudiesBySource(response.systematicStudyId, "") + val sources = getSources(response.studyReviews) + for (source in sources) { + restfulResponse.add(linksFactory.findAllStudiesBySource(response.systematicStudyId, source)) + } val createStudyReview = linksFactory.createStudy(response.systematicStudyId) - restfulResponse.add(selfRef, allBySource, createStudyReview) + restfulResponse.add(selfRef, createStudyReview) responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) } @@ -36,6 +36,8 @@ class RestfulFindAllStudyReviewsPresenter( override fun isDone() = responseEntity != null + fun getSources(studies: List): List = studies.flatMap { it.searchSources }.distinct() + private data class ViewModel ( val systematicStudyId : UUID, val size: Int, From e19dcb76734e511284cb20a3f2fa59997e72048b Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:27:18 -0300 Subject: [PATCH 27/74] feat: add link to RemoveCriteriaService --- web/src/main/kotlin/br/all/utils/LinksFactory.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt index dcea9b1fe..205d590f6 100644 --- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt +++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt @@ -12,6 +12,7 @@ import br.all.study.controller.StudyReviewController import br.all.study.requests.PatchDuplicatedStudiesRequest import br.all.study.requests.PatchStatusStudyReviewRequest import br.all.study.requests.PostStudyReviewRequest +import br.all.study.requests.RemoveCriteriaRequest import org.springframework.hateoas.Link import org.springframework.hateoas.server.mvc.linkTo import org.springframework.stereotype.Component @@ -324,4 +325,13 @@ class LinksFactory { studyReviewId ) }.withRel("find-study-criteria").withType("GET") + + fun removeStudyReviewCriteria(systematicStudyId: UUID, studyReviewId: Long, request: RemoveCriteriaRequest): Link = + linkTo { + removeCriterion( + systematicStudyId, + studyReviewId, + request + ) + }.withRel("remove-criteria").withType("PATCH") } From b82d735626c6801320c89422fe11b9155078ef62 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:27:34 -0300 Subject: [PATCH 28/74] feat: create remove criteria presenter --- .../study/update/interfaces/RemoveCriteriaPresenter.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt new file mode 100644 index 000000000..ab24adad1 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaPresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.study.update.interfaces + +import br.all.application.shared.presenter.GenericPresenter + + +interface RemoveCriteriaPresenter: GenericPresenter \ No newline at end of file From b7b2b5339088bd4c23f89809a76c6258041792c1 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:27:41 -0300 Subject: [PATCH 29/74] feat: create remove criteria request --- .../all/study/requests/RemoveCriteriaRequest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt diff --git a/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt b/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt new file mode 100644 index 000000000..391995f94 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/requests/RemoveCriteriaRequest.kt @@ -0,0 +1,15 @@ +package br.all.study.requests + +import br.all.application.study.update.interfaces.RemoveCriteriaService +import java.util.* + +data class RemoveCriteriaRequest ( + val criteria: List, +) { + fun toRequestModel(userId: UUID, systematicStudyId: UUID, studyReviewId: Long) = RemoveCriteriaService.RequestModel ( + userId, + systematicStudyId, + studyReviewId, + criteria + ) +} From 267d5613f0544f3c7b31480cb22729867701c402 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:27:49 -0300 Subject: [PATCH 30/74] feat: create remove criteria service --- .../interfaces/RemoveCriteriaService.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt new file mode 100644 index 000000000..1f4923842 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/RemoveCriteriaService.kt @@ -0,0 +1,22 @@ +package br.all.application.study.update.interfaces + +import java.util.* + + +interface RemoveCriteriaService { + fun removeCriteria(presenter: RemoveCriteriaPresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyId: Long, + val criteria: List + ) + + data class ResponseModel( + val systematicStudyId: UUID, + val studyId: Long, + val inclusionCriteria: List, + val exclusionCriteria: List, + ) +} From 8f39a3ec2043fb03379585bcd5570156effe28ff Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:27:56 -0300 Subject: [PATCH 31/74] feat: create remove criteria service impl --- .../RemoveCriteriaServiceImpl.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt new file mode 100644 index 000000000..986bf2327 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/implementation/RemoveCriteriaServiceImpl.kt @@ -0,0 +1,70 @@ +package br.all.application.study.update.implementation + +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.exceptions.EntityNotFoundException +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.study.repository.fromDto +import br.all.application.study.repository.toDto +import br.all.application.study.update.interfaces.RemoveCriteriaPresenter +import br.all.application.study.update.interfaces.RemoveCriteriaService +import br.all.application.user.CredentialsService +import br.all.domain.model.protocol.Criterion +import br.all.domain.model.review.SystematicStudy +import br.all.domain.model.study.StudyReview + +class RemoveCriteriaServiceImpl( + private val studyReviewRepository: StudyReviewRepository, + private val systematicStudyRepository: SystematicStudyRepository, + private val credentialsService: CredentialsService, +): RemoveCriteriaService { + override fun removeCriteria(presenter: RemoveCriteriaPresenter, request: RemoveCriteriaService.RequestModel) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + if (presenter.isDone()) return + + val studyReviewDto = studyReviewRepository.findById(request.systematicStudyId, request.studyId) + if (studyReviewDto == null) { + presenter.prepareFailView( + EntityNotFoundException("Study review of id ${request.systematicStudyId} not found.") + ) + return + } + + val studyReview = StudyReview.fromDto(studyReviewDto) + + val inclusionCriteria: List = studyReview.criteria + .filter { it.type == Criterion.CriterionType.INCLUSION } + .map { it.description } + + val exclusionCriteria: List = studyReview.criteria + .filter { it.type == Criterion.CriterionType.EXCLUSION } + .map { it.description } + + request.criteria + .filter { it.isNotBlank() } + .forEach { criterionString -> + studyReview.criteria + .filter { it.description == criterionString } + .forEach { matching -> + studyReview.removeCriterion(matching) + } + } + + studyReviewRepository.saveOrUpdate(studyReview.toDto()) + + presenter.prepareSuccessView( + RemoveCriteriaService.ResponseModel( + request.systematicStudyId, + request.studyId, + inclusionCriteria, + exclusionCriteria, + ) + ) + } +} \ No newline at end of file From e15aea4f597d3ce4de32acff3eda9480ab544836 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:28:08 -0300 Subject: [PATCH 32/74] feat: create remove criteria restful presenter --- .../RestfulRemoveCriteriaPresenter.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt new file mode 100644 index 000000000..438284947 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulRemoveCriteriaPresenter.kt @@ -0,0 +1,60 @@ +package br.all.study.presenter + +import br.all.application.study.update.interfaces.RemoveCriteriaPresenter +import br.all.application.study.update.interfaces.RemoveCriteriaService +import br.all.shared.error.createErrorResponseFrom +import br.all.study.requests.RemoveCriteriaRequest +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity + +class RestfulRemoveCriteriaPresenter( + private val linksFactory: LinksFactory, +): RemoveCriteriaPresenter { + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: RemoveCriteriaService.ResponseModel) { + val restfulResponse = ViewModel( + response.inclusionCriteria, + response.exclusionCriteria, + ) + + response.inclusionCriteria.forEach { + restfulResponse.add(linksFactory.removeStudyReviewCriteria + ( + response.systematicStudyId, + response.studyId, + RemoveCriteriaRequest( + criteria = listOf(it) + ) + ) + ) + } + + response.exclusionCriteria.forEach { + restfulResponse.add(linksFactory.removeStudyReviewCriteria + ( + response.systematicStudyId, + response.studyId, + RemoveCriteriaRequest( + criteria = listOf(it) + ) + ) + ) + } + + val protocol = linksFactory.findProtocol(response.systematicStudyId) + restfulResponse.add(protocol) + responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) = run {responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val inclusionCriteria: List, + val exclusionCriteria: List, + ) : RepresentationModel() +} \ No newline at end of file From 6f7dd6a7c0649cec985c43e803fb4d780d06b504 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:28:30 -0300 Subject: [PATCH 33/74] feat: create remove criteria request mapping --- .../study/controller/StudyReviewController.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt index 11670b2ba..01bc6a7c8 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt @@ -35,6 +35,7 @@ class StudyReviewController( private val updateSelectionService: UpdateStudyReviewSelectionService, private val updateExtractionService: UpdateStudyReviewExtractionService, private val updateReadingPriorityService: UpdateStudyReviewPriorityService, + private val removeCriteriaService: RemoveCriteriaService, private val markAsDuplicatedService: MarkAsDuplicatedService, private val answerQuestionService: AnswerQuestionService, private val authenticationInfoService: AuthenticationInfoService, @@ -481,4 +482,39 @@ class StudyReviewController( markAsDuplicatedService.markAsDuplicated(presenter, request) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @PatchMapping("/study-review/remove-criteria/{studyReviewId}") + @Operation(summary = "Remove a criteria from study review") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Success removing criteria of study review", + content = [Content(schema = Schema(hidden = true))]), + ApiResponse( + responseCode = "400", + description = "Fail removing criteria of study review - invalid status", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail removing criteria of study review - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail removing criteria of study review - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun removeCriterion( + @PathVariable systematicStudy: UUID, + @PathVariable studyReviewId: Long, + @RequestBody patchRequest: RemoveCriteriaRequest + ): ResponseEntity<*> { + val presenter = RestfulRemoveCriteriaPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = patchRequest.toRequestModel(userId, systematicStudy, studyReviewId) + removeCriteriaService.removeCriteria(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } \ No newline at end of file From bcad4b2f43c4ebf7abb481f518f63ecd6800b507 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Sun, 22 Jun 2025 03:28:46 -0300 Subject: [PATCH 34/74] feat: add remove criteria service config --- .../controller/StudyReviewServicesConfiguration.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt index 4c89d9144..95ff740fd 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt @@ -138,4 +138,15 @@ class StudyReviewServicesConfiguration { systematicStudyRepository, credentialsService ) + + @Bean + fun removeCriteriaService( + studyReviewRepository: StudyReviewRepository, + systematicStudyRepository: SystematicStudyRepository, + credentialsService: CredentialsService, + ) = RemoveCriteriaServiceImpl( + studyReviewRepository, + systematicStudyRepository, + credentialsService + ) } \ No newline at end of file From 59646a38c34992e9c2c0feb530f2024abdd85f3f Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Mon, 23 Jun 2025 15:10:16 -0300 Subject: [PATCH 35/74] chore: remove junit dependency - let spring solve it automatically --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index 693dbddf5..b98537552 100644 --- a/pom.xml +++ b/pom.xml @@ -69,12 +69,6 @@ kotlin-faker 1.15.0 - - org.junit.jupiter - junit-jupiter-engine - 5.8.2 - test - com.tngtech.archunit archunit-junit5 From 8c6b4406979ec59067f9d5918c469aa96dadc85c Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 23 Jun 2025 15:32:20 -0300 Subject: [PATCH 36/74] build: normalise scas kotlin version --- scas/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scas/pom.xml b/scas/pom.xml index 14a1e5fb8..b8ddb39a2 100644 --- a/scas/pom.xml +++ b/scas/pom.xml @@ -81,7 +81,7 @@ org.jetbrains.kotlin kotlin-stdlib - 1.9.23 + 1.9.0 br.all From 974506dfec44150a4c53f5744bfc3481361d67e6 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Tue, 24 Jun 2025 14:22:35 -0300 Subject: [PATCH 37/74] chore: change text to be grammatically accurate. --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b98537552..a2365f4e1 100644 --- a/pom.xml +++ b/pom.xml @@ -155,9 +155,9 @@ - This was the old executions property, it was removed because, as indicated by the kotlin docs, - from version 1.8.2 you can replace all of it with: true. This is a faulty - executions property, it is here only for eventual future reference. + This was the old executions configuration; it has been removed because, as indicated by the Kotlin docs, from version 1.8.2 onward you can replace all of it with: + true + This executions block is faulty and is included here only for eventual future reference. --> true From a95e7558728d8dcaaea3d74f54df50f9c925d2ef Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 14:50:11 -0300 Subject: [PATCH 38/74] feat(batchAnswer): add new patch batch request model --- ...chBatchAnswerQuestionStudyReviewRequest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt diff --git a/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt b/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt new file mode 100644 index 000000000..110711493 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/requests/PatchBatchAnswerQuestionStudyReviewRequest.kt @@ -0,0 +1,34 @@ +package br.all.study.requests + +import br.all.application.study.update.interfaces.BatchAnswerQuestionService +import java.util.UUID + +class PatchBatchAnswerQuestionStudyReviewRequest( + val answers: List +) { + data class AnswerPayload( + val questionId: UUID, + val type: String, + val answer: Any + ) + + fun toRequestModel(userId: UUID, + systematicStudyId: UUID, + studyReviewId: Long + ): BatchAnswerQuestionService.RequestModel { + val serviceAnswers = this.answers.map { + BatchAnswerQuestionService.RequestModel.AnswerDetail( + questionId = it.questionId, + type = it.type, + answer = it.answer + ) + } + + return BatchAnswerQuestionService.RequestModel( + userId = userId, + systematicStudyId = systematicStudyId, + studyReviewId = studyReviewId, + answers = serviceAnswers + ) + } +} \ No newline at end of file From 5e1d297327bdc15594c1e776e50c680237267618 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 14:50:30 -0300 Subject: [PATCH 39/74] feat(batchAnswer): add new patch batch service and presenter interfaces --- .../BatchAnswerQuestionPresenter.kt | 6 ++++ .../interfaces/BatchAnswerQuestionService.kt | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt create mode 100644 review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt new file mode 100644 index 000000000..627d35bb8 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionPresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.study.update.interfaces + +import br.all.application.shared.presenter.GenericPresenter +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel + +interface BatchAnswerQuestionPresenter: GenericPresenter \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt new file mode 100644 index 000000000..1df16e4dc --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt @@ -0,0 +1,29 @@ +package br.all.application.study.update.interfaces + +import io.swagger.v3.oas.annotations.media.Schema +import java.util.UUID + +interface BatchAnswerQuestionService { + fun batchAnswerQuestion(presenter: BatchAnswerQuestionPresenter, request: RequestModel, context: String) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyReviewId: Long, + val answers: List + ) { + data class AnswerDetail( + val questionId: UUID, + val type: String, + val answer: Any + ) + } + + @Schema(name = "BatchAnswerQuestionServiceResponseModel") + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyReviewId: Long, + val answersProcessed: Int + ) +} \ No newline at end of file From 492e3b2b67bcf7e7be3bd8e4b557965ab1544ad8 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 14:50:37 -0300 Subject: [PATCH 40/74] feat(batchAnswer): add new patch batch service implementation --- .../BatchAnswerQuestionServiceImpl.kt | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt new file mode 100644 index 000000000..73f035848 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/update/implementation/BatchAnswerQuestionServiceImpl.kt @@ -0,0 +1,115 @@ +package br.all.application.study.update.implementation + +import br.all.application.question.repository.QuestionRepository +import br.all.application.question.repository.fromDto +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.exceptions.EntityNotFoundException +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.study.repository.fromDto +import br.all.application.study.repository.toDto +import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter +import br.all.application.study.update.interfaces.BatchAnswerQuestionService +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.RequestModel +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel +import br.all.application.user.CredentialsService +import br.all.domain.model.question.Label +import br.all.domain.model.question.LabeledScale +import br.all.domain.model.question.NumberScale +import br.all.domain.model.question.PickList +import br.all.domain.model.question.Question +import br.all.domain.model.question.QuestionContextEnum +import br.all.domain.model.question.Textual +import br.all.domain.model.review.SystematicStudy +import br.all.domain.model.study.Answer +import br.all.domain.model.study.StudyReview + +class BatchAnswerQuestionServiceImpl( + private val studyReviewRepository: StudyReviewRepository, + private val questionRepository: QuestionRepository, + private val systematicStudyRepository: SystematicStudyRepository, + private val credentialsService: CredentialsService, +): BatchAnswerQuestionService { + override fun batchAnswerQuestion( + presenter: BatchAnswerQuestionPresenter, + request: RequestModel, + context: String + ) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + if (presenter.isDone()) return + + val reviewDto = studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId) + if (reviewDto == null) { + val message = "Review with id ${request.studyReviewId} in systematic study ${request.systematicStudyId} does not exist!" + presenter.prepareFailView(EntityNotFoundException(message)) + return + } + + val review = StudyReview.fromDto(reviewDto) + var answersProcessed = 0 + + for (answerDetail in request.answers) { + val questionDto = questionRepository.findById(request.systematicStudyId, answerDetail.questionId) + + if (questionDto == null) continue + if (QuestionContextEnum.valueOf(context) != questionDto.context) continue + + try { + val question = Question.fromDto(questionDto) + val answer = convertAnswer(answerDetail.type, answerDetail.answer, question) + + if (questionDto.context == QuestionContextEnum.ROB) { + review.answerQualityQuestionOf(answer) + } else { + review.answerFormQuestionOf(answer) + } + + answersProcessed++ + } catch (e: Exception) { + continue + } + } + + studyReviewRepository.saveOrUpdate(review.toDto()) + + presenter.prepareSuccessView( + ResponseModel( + request.userId, + request.systematicStudyId, + request.studyReviewId, + answersProcessed + ) + ) + } + + private fun convertAnswer( + type: String, + rawAnswer: Any, + question: Question<*> + ): Answer<*> { + return when { + type == "TEXTUAL" && rawAnswer is String -> (question as Textual).answer(rawAnswer) + type == "PICK_LIST" && rawAnswer is String -> (question as PickList).answer(rawAnswer) + type == "NUMBERED_SCALE" && rawAnswer is Int -> (question as NumberScale).answer(rawAnswer) + type == "LABELED_SCALE" -> { + when (rawAnswer) { + is Map<*, *> -> { + val name = rawAnswer["name"] as? String + val value = rawAnswer["value"] as? Int + if (name != null && value != null) { + (question as LabeledScale).answer(Label(name, value)) + } else throw IllegalArgumentException("Invalid labeled scale answer") + } + else -> throw IllegalArgumentException("Unsupported LABELED_SCALE format") + } + } + else -> throw IllegalArgumentException("Unsupported answer type or mismatched value") + } + } +} \ No newline at end of file From ab43577fbf5f5872cb24b3130026b190a083689e Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:32:11 -0300 Subject: [PATCH 41/74] refactor(batchAnswer): add properly exceptions to sad paths --- .../BatchAnswerQuestionServiceImpl.kt | 81 ++++++++++++------- .../interfaces/BatchAnswerQuestionService.kt | 14 +++- 2 files changed, 65 insertions(+), 30 deletions(-) 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 73f035848..f4f1421ff 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 @@ -12,7 +12,10 @@ import br.all.application.study.repository.toDto import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter import br.all.application.study.update.interfaces.BatchAnswerQuestionService import br.all.application.study.update.interfaces.BatchAnswerQuestionService.RequestModel +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.RequestModel.AnswerDetail import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.FailedAnswer +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.LabelDto import br.all.application.user.CredentialsService import br.all.domain.model.question.Label import br.all.domain.model.question.LabeledScale @@ -24,6 +27,7 @@ import br.all.domain.model.question.Textual import br.all.domain.model.review.SystematicStudy import br.all.domain.model.study.Answer import br.all.domain.model.study.StudyReview +import java.util.UUID class BatchAnswerQuestionServiceImpl( private val studyReviewRepository: StudyReviewRepository, @@ -50,29 +54,38 @@ class BatchAnswerQuestionServiceImpl( presenter.prepareFailView(EntityNotFoundException(message)) return } - val review = StudyReview.fromDto(reviewDto) - var answersProcessed = 0 + + val questionContext = QuestionContextEnum.valueOf(context.uppercase()) + val successfulQuestionIds = mutableListOf() + val failedAnswers = mutableListOf() + var totalAnswered = 0 for (answerDetail in request.answers) { - val questionDto = questionRepository.findById(request.systematicStudyId, answerDetail.questionId) + try { + val questionDto = questionRepository.findById(request.systematicStudyId, answerDetail.questionId) - if (questionDto == null) continue - if (QuestionContextEnum.valueOf(context) != questionDto.context) continue + if (questionDto == null) throw EntityNotFoundException("Question with id ${answerDetail.questionId} in systematic study ${request.systematicStudyId} was not found!") + if (questionDto.context != questionContext) throw IllegalArgumentException("Should answer question with the context: $context, found: ${questionDto.context}") - try { val question = Question.fromDto(questionDto) - val answer = convertAnswer(answerDetail.type, answerDetail.answer, question) + val answer = convertAnswer(question, answerDetail, questionDto.context.name) - if (questionDto.context == QuestionContextEnum.ROB) { + if (questionContext == QuestionContextEnum.ROB) { review.answerQualityQuestionOf(answer) } else { review.answerFormQuestionOf(answer) } - answersProcessed++ + successfulQuestionIds.add(answerDetail.questionId) + totalAnswered++ } catch (e: Exception) { - continue + failedAnswers.add( + FailedAnswer( + questionId = answerDetail.questionId, + reason = e.message ?: "An unknown error occurred!" + ) + ) } } @@ -80,36 +93,46 @@ class BatchAnswerQuestionServiceImpl( presenter.prepareSuccessView( ResponseModel( - request.userId, - request.systematicStudyId, - request.studyReviewId, - answersProcessed + userId = request.userId, + systematicStudyId = request.systematicStudyId, + studyReviewId = request.studyReviewId, + succeededAnswers = successfulQuestionIds, + failedAnswers = failedAnswers, + totalAnswered = totalAnswered ) ) } private fun convertAnswer( - type: String, - rawAnswer: Any, - question: Question<*> + question: Question<*>, + detail: AnswerDetail, + type: String ): Answer<*> { + if (detail.type != type) { + throw IllegalArgumentException("Type mismatch: Request payload type is '${detail.type}', but question ${question.id} is of type '${type}'") + } return when { - type == "TEXTUAL" && rawAnswer is String -> (question as Textual).answer(rawAnswer) - type == "PICK_LIST" && rawAnswer is String -> (question as PickList).answer(rawAnswer) - type == "NUMBERED_SCALE" && rawAnswer is Int -> (question as NumberScale).answer(rawAnswer) + type == "TEXTUAL" && detail.answer is String -> (question as Textual).answer(detail.answer) + type == "PICK_LIST" && detail.answer is String -> (question as PickList).answer(detail.answer) + type == "NUMBERED_SCALE" && detail.answer is Int -> (question as NumberScale).answer(detail.answer) type == "LABELED_SCALE" -> { - when (rawAnswer) { - is Map<*, *> -> { - val name = rawAnswer["name"] as? String - val value = rawAnswer["value"] as? Int - if (name != null && value != null) { - (question as LabeledScale).answer(Label(name, value)) - } else throw IllegalArgumentException("Invalid labeled scale answer") + when (val answer = detail.answer) { + is LinkedHashMap<*, *> -> { + (answer["name"] as? String)?.let { name -> + (answer["value"] as? Int)?.let { value -> + (question as LabeledScale).answer(Label(name, value)) + } + } ?: throw IllegalArgumentException("Invalid labeled scale answer: missing 'name' or 'value'") + } + is LabelDto -> { + (question as LabeledScale).answer(Label(answer.name, answer.value)) + } + else -> { + throw IllegalArgumentException("Unsupported answer type for 'LABELED_SCALE'") } - else -> throw IllegalArgumentException("Unsupported LABELED_SCALE format") } } - else -> throw IllegalArgumentException("Unsupported answer type or mismatched value") + else -> throw IllegalArgumentException("Answer type of '${detail.answer.javaClass}' is not compatible with question type '${type}'") } } } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt index 1df16e4dc..36c6299d1 100644 --- a/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt +++ b/review/src/main/kotlin/br/all/application/study/update/interfaces/BatchAnswerQuestionService.kt @@ -19,11 +19,23 @@ interface BatchAnswerQuestionService { ) } + data class FailedAnswer( + val questionId: UUID, + val reason: String + ) + + data class LabelDto( + val name: String, + val value: Int + ) + @Schema(name = "BatchAnswerQuestionServiceResponseModel") data class ResponseModel( val userId: UUID, val systematicStudyId: UUID, val studyReviewId: Long, - val answersProcessed: Int + val succeededAnswers: List, + val failedAnswers: List, + val totalAnswered: Int ) } \ No newline at end of file From 29ecba19f31929ce8ca10c9a6fa35ae9d3a80cda Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:35:14 -0300 Subject: [PATCH 42/74] fix(batchAnswer): change convert answer wrong passing param --- .../BatchAnswerQuestionServiceImpl.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 f4f1421ff..1f971a240 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 @@ -69,9 +69,9 @@ class BatchAnswerQuestionServiceImpl( if (questionDto.context != questionContext) throw IllegalArgumentException("Should answer question with the context: $context, found: ${questionDto.context}") val question = Question.fromDto(questionDto) - val answer = convertAnswer(question, answerDetail, questionDto.context.name) + val answer = convertAnswer(question, answerDetail, questionDto.questionType) - if (questionContext == QuestionContextEnum.ROB) { + if (questionDto.context == QuestionContextEnum.ROB) { review.answerQualityQuestionOf(answer) } else { review.answerFormQuestionOf(answer) @@ -106,16 +106,16 @@ class BatchAnswerQuestionServiceImpl( private fun convertAnswer( question: Question<*>, detail: AnswerDetail, - type: String + questionType: String ): Answer<*> { - if (detail.type != type) { - throw IllegalArgumentException("Type mismatch: Request payload type is '${detail.type}', but question ${question.id} is of type '${type}'") + if (detail.type != questionType) { + throw IllegalArgumentException("Type mismatch: Request payload type is '${detail.type}', but question ${question.id} is of type '${questionType}'") } return when { - type == "TEXTUAL" && detail.answer is String -> (question as Textual).answer(detail.answer) - type == "PICK_LIST" && detail.answer is String -> (question as PickList).answer(detail.answer) - type == "NUMBERED_SCALE" && detail.answer is Int -> (question as NumberScale).answer(detail.answer) - type == "LABELED_SCALE" -> { + questionType == "TEXTUAL" && detail.answer is String -> (question as Textual).answer(detail.answer) + questionType == "PICK_LIST" && detail.answer is String -> (question as PickList).answer(detail.answer) + questionType == "NUMBERED_SCALE" && detail.answer is Int -> (question as NumberScale).answer(detail.answer) + questionType == "LABELED_SCALE" -> { when (val answer = detail.answer) { is LinkedHashMap<*, *> -> { (answer["name"] as? String)?.let { name -> @@ -132,7 +132,7 @@ class BatchAnswerQuestionServiceImpl( } } } - else -> throw IllegalArgumentException("Answer type of '${detail.answer.javaClass}' is not compatible with question type '${type}'") + else -> throw IllegalArgumentException("Answer type of '${detail.answer.javaClass}' is not compatible with question type '${questionType}'") } } } \ No newline at end of file From 2407d84d73e939ae3fc7e6e8f3975f54315e7f41 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:36:32 -0300 Subject: [PATCH 43/74] refactor(batchAnswer): change bloat count variable --- .../update/implementation/BatchAnswerQuestionServiceImpl.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1f971a240..22c2813c9 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 @@ -59,7 +59,6 @@ class BatchAnswerQuestionServiceImpl( val questionContext = QuestionContextEnum.valueOf(context.uppercase()) val successfulQuestionIds = mutableListOf() val failedAnswers = mutableListOf() - var totalAnswered = 0 for (answerDetail in request.answers) { try { @@ -78,7 +77,6 @@ class BatchAnswerQuestionServiceImpl( } successfulQuestionIds.add(answerDetail.questionId) - totalAnswered++ } catch (e: Exception) { failedAnswers.add( FailedAnswer( @@ -98,7 +96,7 @@ class BatchAnswerQuestionServiceImpl( studyReviewId = request.studyReviewId, succeededAnswers = successfulQuestionIds, failedAnswers = failedAnswers, - totalAnswered = totalAnswered + totalAnswered = successfulQuestionIds.size ) ) } From 13fba247854031bb39a68d9d90cf97220b8e897c Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:39:41 -0300 Subject: [PATCH 44/74] fix(batchAnswer): make service implementation transactional --- .../update/implementation/BatchAnswerQuestionServiceImpl.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 22c2813c9..724075ecd 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 @@ -27,14 +27,19 @@ import br.all.domain.model.question.Textual import br.all.domain.model.review.SystematicStudy import br.all.domain.model.study.Answer import br.all.domain.model.study.StudyReview +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.util.UUID +@Service class BatchAnswerQuestionServiceImpl( private val studyReviewRepository: StudyReviewRepository, private val questionRepository: QuestionRepository, private val systematicStudyRepository: SystematicStudyRepository, private val credentialsService: CredentialsService, ): BatchAnswerQuestionService { + + @Transactional override fun batchAnswerQuestion( presenter: BatchAnswerQuestionPresenter, request: RequestModel, From 2f35b2c3d170931eeae8f5afef5f519e372d9172 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:48:50 -0300 Subject: [PATCH 45/74] feat(batchAnswer): add restful batch answer question presenter --- .../RestfulBatchAnswerQuestionPresenter.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt new file mode 100644 index 000000000..7cc8f06b2 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulBatchAnswerQuestionPresenter.kt @@ -0,0 +1,48 @@ +package br.all.study.presenter + +import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.ResponseModel +import br.all.application.study.update.interfaces.BatchAnswerQuestionService.FailedAnswer +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.UUID + +class RestfulBatchAnswerQuestionPresenter( + private val linksFactory: LinksFactory +): BatchAnswerQuestionPresenter { + var responseEntity: ResponseEntity<*>?= null + + override fun prepareSuccessView(response: ResponseModel) { + val viewModel = ViewModel( + systematicStudyId = response.systematicStudyId, + studyReviewId = response.studyReviewId, + succeededAnswers = response.succeededAnswers, + failedAnswers = response.failedAnswers, + totalAnswered = response.totalAnswered + ) + + val link = linksFactory.findStudy(response.systematicStudyId, response.studyReviewId) + viewModel.add(link) + + responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel) + } + + override fun prepareFailView(throwable: Throwable) { + responseEntity = createErrorResponseFrom(throwable) + } + + override fun isDone(): Boolean { + return responseEntity != null + } + + data class ViewModel( + val systematicStudyId: UUID, + val studyReviewId: Long, + val succeededAnswers: List, + val failedAnswers: List, + val totalAnswered: Int + ) : RepresentationModel() +} \ No newline at end of file From 0fd1b316195ac1a409124022eb0f439820cf3e57 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 25 Jun 2025 15:53:12 -0300 Subject: [PATCH 46/74] feat(batchAnswer): implement two new endpoints for batch answering questions --- .../study/controller/StudyReviewController.kt | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt index 01bc6a7c8..f2005b0aa 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt @@ -21,6 +21,9 @@ import org.springframework.web.bind.annotation.* import java.util.* import br.all.application.study.find.service.FindAllStudyReviewsBySourceService.RequestModel as FindAllBySourceRequest import br.all.application.study.find.service.FindStudyReviewService.RequestModel as FindOneRequest +import br.all.application.study.update.interfaces.BatchAnswerQuestionService +import br.all.study.presenter.RestfulBatchAnswerQuestionPresenter +import br.all.study.requests.PatchBatchAnswerQuestionStudyReviewRequest @RestController @RequestMapping("/api/v1/systematic-study/{systematicStudy}") @@ -38,6 +41,7 @@ class StudyReviewController( private val removeCriteriaService: RemoveCriteriaService, private val markAsDuplicatedService: MarkAsDuplicatedService, private val answerQuestionService: AnswerQuestionService, + private val batchAnswerQuestionService: BatchAnswerQuestionService, private val authenticationInfoService: AuthenticationInfoService, private val linksFactory: LinksFactory @@ -442,6 +446,86 @@ class StudyReviewController( return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + @PatchMapping("/study-review/{studyReview}/batch-riskOfBias-answers") + @Operation(summary = "Update a batch of answers for risk of bias questions") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Success updating a batch of answers to risk of bias questions", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = BatchAnswerQuestionService.ResponseModel::class) + )] + ), + ApiResponse( + responseCode = "400", + description = "Fail updating answers - invalid payload", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail updating answers - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail updating answers - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun batchRiskOfBiasAnswers( + @PathVariable systematicStudy: UUID, + @PathVariable studyReview: Long, + @RequestBody requestBody: PatchBatchAnswerQuestionStudyReviewRequest + ): ResponseEntity<*> { + val presenter = RestfulBatchAnswerQuestionPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = requestBody.toRequestModel(userId, systematicStudy, studyReview) + + batchAnswerQuestionService.batchAnswerQuestion(presenter, request, "ROB") + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } + + @PatchMapping("/study-review/{studyReview}/batch-extraction-answers") + @Operation(summary = "Update a batch of answers for extraction questions") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "Success updating a batch of answers to extraction questions", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = BatchAnswerQuestionService.ResponseModel::class) + )] + ), + ApiResponse( + responseCode = "400", + description = "Fail updating answers - invalid payload", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail updating answers - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail updating answers - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun batchExtractionAnswers( + @PathVariable systematicStudy: UUID, + @PathVariable studyReview: Long, + @RequestBody requestBody: PatchBatchAnswerQuestionStudyReviewRequest + ): ResponseEntity<*> { + val presenter = RestfulBatchAnswerQuestionPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = requestBody.toRequestModel(userId, systematicStudy, studyReview) + + batchAnswerQuestionService.batchAnswerQuestion(presenter, request, "EXTRACTION") + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } + @PatchMapping("/study-review/{referenceStudyId}/duplicated") @Operation(summary = "Mark multiple existing studies as duplicated in the systematic study") @ApiResponses( From de1285ddfaec899fde8c8f5fd2bd5a638293ea9b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 13:55:52 -0300 Subject: [PATCH 47/74] refactor(email): revamp email validation logic --- .../main/kotlin/br/all/domain/user/Email.kt | 59 +++++++++++-------- .../br/all/domain/shared/valueobject/Email.kt | 55 +++++++++-------- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/account/src/main/kotlin/br/all/domain/user/Email.kt b/account/src/main/kotlin/br/all/domain/user/Email.kt index caf6dce7b..965415718 100644 --- a/account/src/main/kotlin/br/all/domain/user/Email.kt +++ b/account/src/main/kotlin/br/all/domain/user/Email.kt @@ -7,51 +7,60 @@ data class Email(val value: String) : ValueObject() { init { val notification = validate() - require(notification.hasNoErrors()) {notification.message()} + require(notification.hasNoErrors()) { notification.message() } } override fun validate(): Notification { val notification = Notification() + if (value.isEmpty()) { + notification.addError("Email must not be empty.") + return notification + } + if (value.isBlank()) { notification.addError("Email must not be blank.") return notification } + if (!isValidEmailFormat(value)) notification.addError("Wrong Email format.") + return notification } private fun isValidEmailFormat(email: String): Boolean { - if (!isValidEmailAddress(email)) return false - if (!hasLengthBelowMaximum(email)) return false - if (hasRepeatedSubdomains(email)) return false - if (email.contains("..")) return false - if (email.contains("@.")) return false - if (email.contains(".@")) return false - return true - } - - private fun isValidEmailAddress(email: String): Boolean { - val regex = Regex("^[A-Za-z0-9+_.-]+@[a-z.]+$") - return regex.matches(email) - } + if (email.contains("..") || email.contains(".@") || email.contains("@.")) return false + if (email.startsWith(".") || email.endsWith(".")) return false + if (email.startsWith("@") || email.endsWith("@")) return false - fun hasRepeatedSubdomains(email: String): Boolean { val parts = email.split("@") + if (parts.size != 2) return false - if (parts.size == 2) { - val subdomains = parts[1].split(".") - val verifyedSubdomains = HashSet() + val localPart = parts[0] + val domainPart = parts[1] - for (subdomain in subdomains){ - if (!verifyedSubdomains.add(subdomain)) return true - } + if (!hasValidLength(localPart, domainPart)) return false + if (!isValidStructure(localPart, domainPart)) return false + + return true + } + + private fun isValidStructure(localPart: String, domainPart: String): Boolean { + val localRegex = Regex("^[A-Za-z0-9_!#$%&'*+/=?`{|}~^.-]+$") + val domainRegex = Regex("^[A-Za-z0-9.-]+$") + + val domainLabels = domainPart.split(".") + if (domainLabels.last().length < 2 || domainLabels.any { it.startsWith("-") || it.endsWith("-") }) { + return false } - return false + + return localRegex.matches(localPart) && domainRegex.matches(domainPart) } - fun hasLengthBelowMaximum(email: String): Boolean { - val parts = email.split("@") - return !(parts[0].length > 64 || parts[1].length > 255) + private fun hasValidLength(localPart: String, domainPart: String): Boolean { + if (localPart.length > 64) return false + if (domainPart.length > 255) return false + if ((localPart.length + 1 + domainPart.length) > 254) return false + return true } } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt b/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt index fa3f91af0..aa332b362 100644 --- a/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt +++ b/review/src/main/kotlin/br/all/domain/shared/valueobject/Email.kt @@ -7,7 +7,7 @@ data class Email(val email: String) : ValueObject() { init { val notification = validate() - require(notification.hasNoErrors()) {notification.message()} + require(notification.hasNoErrors()) { notification.message() } } override fun validate(): Notification { @@ -17,45 +17,50 @@ data class Email(val email: String) : ValueObject() { notification.addError("Email must not be empty.") return notification } + if (email.isBlank()) { notification.addError("Email must not be blank.") return notification } + if (!isValidEmailFormat(email)) notification.addError("Wrong Email format.") + return notification } private fun isValidEmailFormat(email: String): Boolean { - if (!isValidEmailAddress(email)) return false - if (!HasLengthBelowMaximum(email)) return false - if (hasRepeatedSubdomains(email)) return false - if (email.contains("..")) return false - if (email.contains("@.")) return false - if (email.contains(".@")) return false - return true - } - - private fun isValidEmailAddress(email: String): Boolean { - val regex = Regex("^[A-Za-z0-9+_.-]+@[a-z.]+$") - return regex.matches(email) - } + if (email.contains("..") || email.contains(".@") || email.contains("@.")) return false + if (email.startsWith(".") || email.endsWith(".")) return false + if (email.startsWith("@") || email.endsWith("@")) return false - fun hasRepeatedSubdomains(email: String): Boolean { val parts = email.split("@") + if (parts.size != 2) return false + + val localPart = parts[0] + val domainPart = parts[1] - if (parts.size == 2) { - val subdomains = parts[1].split(".") - val verifyedSubdomains = HashSet() + if (!hasValidLength(localPart, domainPart)) return false + if (!isValidStructure(localPart, domainPart)) return false - for (subdomain in subdomains){ - if (!verifyedSubdomains.add(subdomain)) return true - } + return true + } + + private fun isValidStructure(localPart: String, domainPart: String): Boolean { + val localRegex = Regex("^[A-Za-z0-9_!#$%&'*+/=?`{|}~^.-]+$") + val domainRegex = Regex("^[A-Za-z0-9.-]+$") + + val domainLabels = domainPart.split(".") + if (domainLabels.last().length < 2 || domainLabels.any { it.startsWith("-") || it.endsWith("-") }) { + return false } - return false + + return localRegex.matches(localPart) && domainRegex.matches(domainPart) } - fun HasLengthBelowMaximum(email: String): Boolean { - val parts = email.split("@") - return !(parts[0].length > 64 || parts[1].length > 255) + private fun hasValidLength(localPart: String, domainPart: String): Boolean { + if (localPart.length > 64) return false + if (domainPart.length > 255) return false + if ((localPart.length + 1 + domainPart.length) > 254) return false + return true } } \ No newline at end of file From 878e84f802373d955c0c1c083f3c901cdf2aa160 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 13:56:30 -0300 Subject: [PATCH 48/74] test(email): modify tests with false premises --- .../br/all/domain/shared/ddd/EmailTest.kt | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt index 1f3348c04..55959a529 100644 --- a/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt +++ b/review/src/test/kotlin/br/all/domain/shared/ddd/EmailTest.kt @@ -61,19 +61,6 @@ class EmailTest { exception.message?.contains("Wrong Email format")?.let { assertTrue(it) } } - - @Test - fun `should not accept email with two equal TLDs`() { - val exception = assertThrows { Email("email@ifsp.com.com") } - exception.message?.contains("Wrong Email format")?.let { assertTrue(it) } - } - - @Test - fun `should not accept email with two equal subdomains`() { - val exception = assertThrows { Email("email@ifsp.ifsp.com") } - exception.message?.contains("Wrong Email format")?.let { assertTrue(it) } - } - @Test fun `email with incorrect format should include error message`() { val exception = assertThrows { Email("invalid-email-format") } @@ -104,7 +91,7 @@ class EmailTest { @Test fun `invalid email with special characters should throw an exception`() { - val exception = assertThrows { Email("user!name@example.com") } + val exception = assertThrows { Email("user name@example.com") } exception.message?.contains("Wrong Email format")?.let { assertTrue(it) } } @@ -151,7 +138,7 @@ class EmailTest { @Test fun `valid email with domain under maximum length should not throw an exception`() { - assertDoesNotThrow { Email("user@" + "a".repeat(251) + ".com") } + assertDoesNotThrow { Email("user@" + "a".repeat(240) + ".com") } } @Test @@ -159,6 +146,4 @@ class EmailTest { val exception = assertThrows { Email("user@domain@com") } exception.message?.contains("Wrong Email format")?.let { assertTrue(it) } } - - } From e6d69157d052116467affe728c154db6fed4d2f5 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 13:56:43 -0300 Subject: [PATCH 49/74] test(email): remove hardcoded email --- .../user/create/RegisterUserAccountServiceImplTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt b/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt index c5d6795b9..10f7cca27 100644 --- a/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt +++ b/account/src/test/kotlin/br/all/application/user/create/RegisterUserAccountServiceImplTest.kt @@ -33,7 +33,7 @@ class RegisterUserAccountServiceImplTest { inner class WhenSuccessfullyRegisteringUser { @Test fun `should register a new user`() { - val request = factory.registerRequest().copy(email = "user@example.com") + val request = factory.registerRequest() every { userAccountRepository.existsByEmail(request.email) } returns false every { userAccountRepository.existsByUsername(request.username) } returns false @@ -53,7 +53,7 @@ class RegisterUserAccountServiceImplTest { inner class WhenFailingToRegisterUser { @Test fun `should not register user with existing email`() { - val request = factory.registerRequest().copy(email = "user@example.com") + val request = factory.registerRequest() every { userAccountRepository.existsByEmail(request.email) } returns true every { userAccountRepository.existsByUsername(request.username) } returns false @@ -68,7 +68,7 @@ class RegisterUserAccountServiceImplTest { @Test fun `should not register user with existing username`() { - val request = factory.registerRequest().copy(email = "user@example.com") + val request = factory.registerRequest() every { userAccountRepository.existsByEmail(request.email) } returns false every { userAccountRepository.existsByUsername(request.username) } returns true From 4f6b43d531ab0663821b1d09a20e91a72c769f6c Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 15:50:28 -0300 Subject: [PATCH 50/74] test(studyController): add number and label question generations in data factory --- .../br/all/study/utils/TestDataFactory.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt b/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt index e98f932f5..d72a0dc5c 100644 --- a/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt +++ b/web/src/test/kotlin/br/all/study/utils/TestDataFactory.kt @@ -164,6 +164,27 @@ class TestDataFactory { QuestionContextEnum.ROB ) + fun generateRobQuestionNumberScaleDto( + questionId: UUID, + systematicStudyId: UUID = this.systematicStudyId, + code: String = faker.lorem.words(), + description: String = faker.lorem.words(), + lower: Int = 1, + higher: Int = 5 + ) = + QuestionDocument( + questionId, + systematicStudyId, + code, + description, + "NUMBERED_SCALE", + null, + higher, + lower, + null, + QuestionContextEnum.ROB + ) + fun generateExtractionQuestionTextualDto( questionId: UUID, systematicStudyId: UUID = this.systematicStudyId, @@ -182,4 +203,24 @@ class TestDataFactory { null, QuestionContextEnum.EXTRACTION ) + + fun generateExtractionQuestionPickListDto( + questionId: UUID, + systematicStudyId: UUID = this.systematicStudyId, + code: String = faker.lorem.words(), + description: String = faker.lorem.words(), + options: List = listOf("Option A", "Option B", "Option C") + ) = + QuestionDocument( + questionId, + systematicStudyId, + code, + description, + "PICK_LIST", + null, + null, + null, + options, + QuestionContextEnum.EXTRACTION + ) } \ No newline at end of file From e4ce13eafde5712357e5ccd8c4e5db8ed58300e9 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 15:50:43 -0300 Subject: [PATCH 51/74] test(studyController): add new tests for both batch endpoints --- .../controller/StudyReviewControllerTest.kt | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt index 4735a7e39..7a2233ff9 100644 --- a/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt +++ b/web/src/test/kotlin/br/all/study/controller/StudyReviewControllerTest.kt @@ -9,8 +9,12 @@ import br.all.infrastructure.study.StudyReviewIdGeneratorService import br.all.security.service.ApplicationUser import br.all.shared.TestHelperService import br.all.study.utils.TestDataFactory +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.hasSize import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc @@ -71,6 +75,12 @@ class StudyReviewControllerTest( fun answerExtractionQuestion(studyReviewId: Long) = "/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/extraction-answer" + fun batchAnswerRiskOfBiasQuestion(studyReviewId: Long) = + "/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/batch-riskOfBias-answers" + + fun batchAnswerExtractionQuestion(studyReviewId: Long) = + "/api/v1/systematic-study/$systematicStudyId/study-review/${studyReviewId}/batch-extraction-answers" + @BeforeEach fun setUp() { repository.deleteAll() @@ -639,6 +649,167 @@ class StudyReviewControllerTest( } } + @Nested + @DisplayName("When batch answering ROB questions in a review") + inner class BatchAnswerRobQuestionsTests( + @Autowired val questionRepository: MongoQuestionRepository + ) { + @Test + @DisplayName("should save valid answers, ignore invalid ones, and return 200 with a detailed report") + fun `should handle partial success correctly`() { + val studyId = idService.next() + val studyReview = factory.reviewDocument(systematicStudyId, studyId) + repository.insert(studyReview) + + val validQId1 = UUID.randomUUID() + val validQId2 = UUID.randomUUID() + val invalidQId = UUID.randomUUID() + + val question1 = factory.generateRobQuestionTextualDto(validQId1, systematicStudyId) + val question2 = factory.generateRobQuestionTextualDto(validQId2, systematicStudyId) + val question3 = factory.generateRobQuestionNumberScaleDto(invalidQId, systematicStudyId) + + questionRepository.insert(question1) + questionRepository.insert(question2) + questionRepository.insert(question3) + + val jsonPayload = """ + { + "answers": [ + { "questionId": "$validQId1", "type": "TEXTUAL", "answer": "First valid answer" }, + { "questionId": "$validQId2", "type": "TEXTUAL", "answer": "Second valid answer" }, + { "questionId": "$invalidQId", "type": "NUMBERED_SCALE", "answer": "This shouldn't be a string" } + ] + } + """.trimIndent() + + mockMvc.perform( + patch(batchAnswerRiskOfBiasQuestion(studyId)) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + .contentType(MediaType.APPLICATION_JSON).content(jsonPayload) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.totalAnswered").value(2)) + .andExpect(jsonPath("$.succeededAnswers", hasSize(2))) + .andExpect(jsonPath("$.succeededAnswers", hasItem(validQId1.toString()))) + .andExpect(jsonPath("$.succeededAnswers", hasItem(validQId2.toString()))) + .andExpect(jsonPath("$.failedAnswers", hasSize(1))) + .andExpect(jsonPath("$.failedAnswers[0].questionId").value(invalidQId.toString())) + .andExpect(jsonPath("$.failedAnswers[0].reason", containsString("not compatible with question type 'NUMBERED_SCALE'"))) + + val updatedReview = repository.findById(StudyReviewId(systematicStudyId, studyId)).get() + + assertEquals(3, updatedReview.qualityAnswers.size) + assertEquals("First valid answer", updatedReview.qualityAnswers[validQId1]) + assertEquals("Second valid answer", updatedReview.qualityAnswers[validQId2]) + assertNull(updatedReview.qualityAnswers[invalidQId]) + } + + @Test + @DisplayName("should return 401 for unauthenticated users") + fun `should not update if user is unauthenticated`() { + testHelperService.testForUnauthenticatedUser(mockMvc, patch(batchAnswerRiskOfBiasQuestion(1L))) + } + + @Test + @DisplayName("should return 403 for unauthorized users") + fun `should not update if user is unauthorized`() { + val jsonPayload = """{ "answers": [] }""" + testHelperService.testForUnauthorizedUser( + mockMvc, + patch(batchAnswerRiskOfBiasQuestion(1L)).content(jsonPayload) + ) + } + } + + @Nested + @DisplayName("When batch answering Extraction questions in a review") + inner class BatchAnswerExtractionQuestionsTests( + @Autowired val questionRepository: MongoQuestionRepository + ) { + @Test + @DisplayName("should save all valid answers and return 200 with no failures") + fun `should handle full success correctly`() { + val studyId = idService.next() + val studyReview = factory.reviewDocument(systematicStudyId, studyId) + repository.insert(studyReview) + + val textQId = UUID.randomUUID() + val pickListQId = UUID.randomUUID() + + val question1 = factory.generateExtractionQuestionTextualDto(textQId, systematicStudyId = systematicStudyId) + val question2 = factory.generateExtractionQuestionPickListDto(pickListQId, systematicStudyId = systematicStudyId) + + questionRepository.insert(question1) + questionRepository.insert(question2) + + val jsonPayload = """ + { + "answers": [ + { "questionId": "$textQId", "type": "TEXTUAL", "answer": "Another valid answer" }, + { "questionId": "$pickListQId", "type": "PICK_LIST", "answer": "Option A" } + ] + } + """.trimIndent() + + mockMvc.perform( + patch(batchAnswerExtractionQuestion(studyId)) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + .contentType(MediaType.APPLICATION_JSON).content(jsonPayload) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.totalAnswered").value(2)) + .andExpect(jsonPath("$.succeededAnswers", hasSize(2))) + .andExpect(jsonPath("$.failedAnswers", hasSize(0))) + + val updatedReview = repository.findById(StudyReviewId(systematicStudyId, studyId)).get() + assertEquals(3, updatedReview.formAnswers.size) + assertEquals("Another valid answer", updatedReview.formAnswers[textQId]) + assertEquals("Option A", updatedReview.formAnswers[pickListQId]) + } + + @Test + @DisplayName("should not save answers if the question context is wrong") + fun `should fail on context mismatch`() { + val studyId = idService.next() + repository.insert(factory.reviewDocument(systematicStudyId, studyId)) + + val extractionQId = UUID.randomUUID() + questionRepository.insert(factory.generateExtractionQuestionTextualDto(extractionQId, systematicStudyId = systematicStudyId)) + + val jsonPayload = """ + { "answers": [{ "questionId": "$extractionQId", "type": "TEXTUAL", "answer": "some answer" }] } + """ + + mockMvc.perform( + patch(batchAnswerRiskOfBiasQuestion(studyId)) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + .contentType(MediaType.APPLICATION_JSON).content(jsonPayload) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.totalAnswered").value(0)) + .andExpect(jsonPath("$.failedAnswers", hasSize(1))) + .andExpect(jsonPath("$.failedAnswers[0].questionId").value(extractionQId.toString())) + .andExpect(jsonPath("$.failedAnswers[0].reason", containsString("Should answer question with the context: ROB"))) + } + + @Test + @DisplayName("should return 401 for unauthenticated users") + fun `should not update if user is unauthenticated`() { + testHelperService.testForUnauthenticatedUser(mockMvc, patch(batchAnswerExtractionQuestion(1L))) + } + + @Test + @DisplayName("should return 403 for unauthorized users") + fun `should not update if user is unauthorized`() { + val jsonPayload = """{ "answers": [] }""" + testHelperService.testForUnauthorizedUser( + mockMvc, + patch(batchAnswerExtractionQuestion(1L)).content(jsonPayload) + ) + } + } + @Nested @DisplayName("When marking a study review as duplicated") inner class MarkingAsDuplicatedTests { From 976408b449b35f7a370122a9c80f7ef42ee7b6ec Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 26 Jun 2025 16:03:14 -0300 Subject: [PATCH 52/74] test(batchAnswer): WIP add service test setup --- .../BatchAnswerQuestionServiceImplTest.kt | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt diff --git a/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt b/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt new file mode 100644 index 000000000..a8e12f75d --- /dev/null +++ b/review/src/test/kotlin/br/all/application/study/update/BatchAnswerQuestionServiceImplTest.kt @@ -0,0 +1,65 @@ +package br.all.application.study.update + +import br.all.application.question.repository.QuestionRepository +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.study.update.implementation.BatchAnswerQuestionServiceImpl +import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter +import br.all.application.study.util.TestDataFactory +import br.all.application.user.CredentialsService +import br.all.application.util.PreconditionCheckerMockingNew +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.extension.ExtendWith +import java.util.UUID + +@Tag("UnitTest") +@Tag("ServiceTest") +@ExtendWith(MockKExtension::class) +class BatchAnswerQuestionServiceImplTest { + + @MockK(relaxed = true) + private lateinit var studyReviewRepository: StudyReviewRepository + + @MockK + private lateinit var systematicStudyRepository: SystematicStudyRepository + + @MockK + private lateinit var questionRepository: QuestionRepository + + @MockK + private lateinit var credentialsService: CredentialsService + + @MockK(relaxed = true) + private lateinit var presenter: BatchAnswerQuestionPresenter + + private lateinit var sut: BatchAnswerQuestionServiceImpl + private lateinit var factory: TestDataFactory + private lateinit var preconditionCheckerMocking: PreconditionCheckerMockingNew + private lateinit var questionId: UUID + + @BeforeEach + fun setUp() { + factory = TestDataFactory() + preconditionCheckerMocking = PreconditionCheckerMockingNew( + presenter, + credentialsService, + systematicStudyRepository, + factory.researcherId, + factory.systematicStudyId + ) + sut = BatchAnswerQuestionServiceImpl( + studyReviewRepository, + questionRepository, + systematicStudyRepository, + credentialsService + ) + questionId = UUID.randomUUID() + } + + + + +} \ No newline at end of file From 5b09b436f829d11b6adf21a9bec5c6e2c3f455b1 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 28 Jun 2025 17:05:41 -0300 Subject: [PATCH 53/74] test(batchAnswer): add request model generation in data factory --- .../application/study/util/TestDataFactory.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt b/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt index ad2554d20..63640d153 100644 --- a/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt +++ b/review/src/test/kotlin/br/all/application/study/util/TestDataFactory.kt @@ -220,6 +220,27 @@ class TestDataFactory { QuestionContextEnum.valueOf(questionContext), ) + fun batchAnswerRequest( + answers: List, + researcherId: UUID = this.researcherId, + systematicStudyId: UUID = this.systematicStudyId, + studyReviewId: Long = this.studyReviewId, + ) = BatchAnswerQuestionService.RequestModel( + userId = researcherId, + systematicStudyId = systematicStudyId, + studyReviewId = studyReviewId, + answers = answers + ) + + fun answerDetail( + questionId: UUID, + type: String, + answer: Any, + ) = BatchAnswerQuestionService.RequestModel.AnswerDetail( + questionId = questionId, + type = type, + answer = answer + ) operator fun component1() = researcherId operator fun component2() = studyReviewId From 3a0ad4052d1a7d4aa96a8217912a224accdaed00 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 28 Jun 2025 17:06:04 -0300 Subject: [PATCH 54/74] test(batchAnswer): add tests for batch answer service impl --- .../BatchAnswerQuestionServiceImplTest.kt | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) 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 a8e12f75d..829b8f8b1 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 @@ -4,16 +4,24 @@ import br.all.application.question.repository.QuestionRepository import br.all.application.review.repository.SystematicStudyRepository import br.all.application.study.repository.StudyReviewRepository import br.all.application.study.update.implementation.BatchAnswerQuestionServiceImpl +import br.all.application.study.update.interfaces.AnswerQuestionService import br.all.application.study.update.interfaces.BatchAnswerQuestionPresenter import br.all.application.study.util.TestDataFactory import br.all.application.user.CredentialsService import br.all.application.util.PreconditionCheckerMockingNew +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension +import io.mockk.verify import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Tag import org.junit.jupiter.api.extension.ExtendWith import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue @Tag("UnitTest") @Tag("ServiceTest") @@ -59,7 +67,174 @@ class BatchAnswerQuestionServiceImplTest { questionId = UUID.randomUUID() } + @Nested + @DisplayName("When successfully answering questions") + inner class WhenSuccessfullyAnsweringQuestions { + @Test + fun `should successfully answer multiple questions of different types`() { + val reviewDto = factory.generateDto() + val context = "EXTRACTION" + val textualQId = UUID.randomUUID() + val textualQDto = factory.generateQuestionTextualDto(textualQId, factory.systematicStudyId, questionContext = context) + val textualAnswer = factory.answerDetail(questionId = textualQId, type = "TEXTUAL", answer = "Valid textual answer") + val numberedQId = UUID.randomUUID() + val numberedQDto = factory.generateQuestionNumberedScaleDto(numberedQId, factory.systematicStudyId, questionContext = context, higher = 5, lower = 1) + val numberedAnswer = factory.answerDetail(questionId = numberedQId, type = "NUMBERED_SCALE", answer = 5) + + val pickListQId = UUID.randomUUID() + val pickListQDto = factory.generateQuestionPickListDto(pickListQId, factory.systematicStudyId, options = listOf("A", "B"), questionContext = context) + val pickListAnswer = factory.answerDetail(questionId = pickListQId, type = "PICK_LIST", answer = "A") + + val request = factory.batchAnswerRequest(listOf(textualAnswer, numberedAnswer, pickListAnswer)) + + preconditionCheckerMocking.makeEverythingWork() + + every { studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId) } returns reviewDto + every { questionRepository.findById(request.systematicStudyId, textualQId) } returns textualQDto + every { questionRepository.findById(request.systematicStudyId, numberedQId) } returns numberedQDto + every { questionRepository.findById(request.systematicStudyId, pickListQId) } returns pickListQDto + + sut.batchAnswerQuestion(presenter, request, context) + + verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) } + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(3, response.succeededAnswers.size) + assertTrue(response.failedAnswers.isEmpty()) + assertEquals(3, response.totalAnswered) + assertTrue(response.succeededAnswers.containsAll(listOf(textualQId, numberedQId, pickListQId))) + }) + } + } + + @Test + fun `should handle a mix of successful and failed answers`() { + val reviewDto = factory.generateDto() + val context = "ROB" + + val successQId = UUID.randomUUID() + val successQDto = factory.generateQuestionTextualDto(successQId, factory.systematicStudyId, questionContext = context) + val successAnswer = factory.answerDetail(questionId = successQId, type = "TEXTUAL", answer = "This will work") + + val notFoundQId = UUID.randomUUID() + val notFoundAnswer = factory.answerDetail(questionId = notFoundQId, type = "TEXTUAL", answer = "This will fail") + + val wrongContextQId = UUID.randomUUID() + val wrongContextQDto = factory.generateQuestionTextualDto(wrongContextQId, factory.systematicStudyId, questionContext = "EXTRACTION") + val wrongContextAnswer = factory.answerDetail(questionId = wrongContextQId, type = "TEXTUAL", answer = "Wrong context") + + val request = factory.batchAnswerRequest(listOf(successAnswer, notFoundAnswer, wrongContextAnswer)) + + preconditionCheckerMocking.makeEverythingWork() + + every { studyReviewRepository.findById(request.systematicStudyId, request.studyReviewId) } returns reviewDto + every { questionRepository.findById(request.systematicStudyId, successQId) } returns successQDto + every { questionRepository.findById(request.systematicStudyId, notFoundQId) } returns null + every { questionRepository.findById(request.systematicStudyId, wrongContextQId) } returns wrongContextQDto + + sut.batchAnswerQuestion(presenter, request, context) + + verify(exactly = 1) { studyReviewRepository.saveOrUpdate(any()) } + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertEquals(1, response.succeededAnswers.size) + assertEquals(2, response.failedAnswers.size) + assertEquals(1, response.totalAnswered) + assertTrue(response.succeededAnswers.contains(successQId)) + assertTrue(response.failedAnswers.any { it.questionId == notFoundQId }) + assertTrue(response.failedAnswers.any { it.questionId == wrongContextQId }) + }) + } + } + } + + @Nested + @DisplayName("When failing to answer questions") + inner class WhenFailingToAnswerQuestions { + @Test + fun `should create a failed answer entry for a question with conflicting types`() { + val reviewDto = factory.generateDto() + val context = "ROB" + val questionId = UUID.randomUUID() + val questionDto = factory.generateQuestionTextualDto(questionId, factory.systematicStudyId, questionContext = context) + + val answerDetail = factory.answerDetail(questionId, "PICK_LIST", "Some answer") + val request = factory.batchAnswerRequest(listOf(answerDetail)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), questionId) } returns questionDto + + sut.batchAnswerQuestion(presenter, request, context) + + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertTrue(response.succeededAnswers.isEmpty()) + assertEquals(1, response.failedAnswers.size) + assertEquals(questionId, response.failedAnswers.first().questionId) + assertTrue("Type mismatch" in response.failedAnswers.first().reason) + }) + } + } + + @Test + fun `should create a failed answer entry when answer value type is incompatible`() { + val reviewDto = factory.generateDto() + val context = "EXTRACTION" + val questionId = UUID.randomUUID() + val questionDto = factory.generateQuestionTextualDto(questionId, factory.systematicStudyId, questionContext = context) + + val answerDetail = factory.answerDetail(questionId, "TEXTUAL", 12345) + val request = factory.batchAnswerRequest(listOf(answerDetail)) + + preconditionCheckerMocking.makeEverythingWork() + + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), questionId) } returns questionDto + + sut.batchAnswerQuestion(presenter, request, context) + + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertTrue(response.succeededAnswers.isEmpty()) + assertEquals(1, response.failedAnswers.size) + assertEquals(questionId, response.failedAnswers.first().questionId) + assertTrue("is not compatible with question type" in response.failedAnswers.first().reason) + }) + } + } + + @Test + fun `should create a failed answer entry for unsupported labeled scale answer type`() { + val reviewDto = factory.generateDto() + val context = "ROB" + val questionId = UUID.randomUUID() + val labelDto = AnswerQuestionService.LabelDto( + "LabelTest", + 1 + ) + val questionDto = factory.generateQuestionLabeledScaleDto(questionId, factory.systematicStudyId, questionContext = context, labelDto = labelDto) + + val answerDetail = factory.answerDetail(questionId, "LABELED_SCALE", "invalid answer format") + val request = factory.batchAnswerRequest(listOf(answerDetail)) + + preconditionCheckerMocking.makeEverythingWork() + every { studyReviewRepository.findById(any(), any()) } returns reviewDto + every { questionRepository.findById(any(), questionId) } returns questionDto + + sut.batchAnswerQuestion(presenter, request, context) + + verify(exactly = 1) { + presenter.prepareSuccessView(withArg { response -> + assertTrue(response.succeededAnswers.isEmpty()) + assertEquals(1, response.failedAnswers.size) + assertEquals(questionId, response.failedAnswers.first().questionId) + assertTrue("Unsupported answer type for 'LABELED_SCALE'" in response.failedAnswers.first().reason) + }) + } + } + } } \ No newline at end of file From 719da3d4bd0bf6f4e1c7d7f645baa9be2f92ca5b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:02:47 -0300 Subject: [PATCH 55/74] chore: add picoc word to the dictionary --- .idea/dictionaries/project.xml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .idea/dictionaries/project.xml diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 000000000..a7265e4c8 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + picoc + + + \ No newline at end of file From c671e08eb897c8e913c92a32a7e4fe15eb77c8ad Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:03:03 -0300 Subject: [PATCH 56/74] feat(protocolStage): add protocol stage presenter --- .../application/protocol/find/GetProtocolStagePresenter.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt new file mode 100644 index 000000000..8ba8c5d9b --- /dev/null +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStagePresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.protocol.find + +import br.all.application.shared.presenter.GenericPresenter +import br.all.application.protocol.find.GetProtocolStageService.ResponseModel + +interface GetProtocolStagePresenter : GenericPresenter \ No newline at end of file From 2fcc5d94a36ecac2df36f1e89e849e3faf34ec8a Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:03:09 -0300 Subject: [PATCH 57/74] feat(protocolStage): add protocol stage service --- .../protocol/find/GetProtocolStageService.kt | 30 ++++++ .../find/GetProtocolStageServiceImpl.kt | 98 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt create mode 100644 review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt new file mode 100644 index 000000000..b4a9b2ccf --- /dev/null +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt @@ -0,0 +1,30 @@ +package br.all.application.protocol.find + +import java.util.UUID + +interface GetProtocolStageService { + fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID + ) + + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val stage: ProtocolStage + ) + + enum class ProtocolStage { + PROTOCOL_PART_I, + PICOC, + PROTOCOL_PART_II, + PROTOCOL_PART_III, + IDENTIFICATION, + SELECTION, + EXTRACTION, + GRAPHICS, + FINALIZATION + } +} \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt new file mode 100644 index 000000000..c242cce42 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -0,0 +1,98 @@ +package br.all.application.protocol.find + +import br.all.application.protocol.repository.ProtocolRepository +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.user.CredentialsService +import br.all.application.protocol.find.GetProtocolStageService.RequestModel +import br.all.application.protocol.find.GetProtocolStageService.ResponseModel +import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage +import br.all.application.protocol.repository.ProtocolDto +import br.all.application.review.repository.fromDto +import br.all.application.shared.exceptions.EntityNotFoundException +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.repository.StudyReviewRepository +import br.all.domain.model.review.SystematicStudy +import org.springframework.stereotype.Service + +@Service +class GetProtocolStageServiceImpl( + private val protocolRepository: ProtocolRepository, + private val systematicStudyRepository: SystematicStudyRepository, + private val studyReviewRepository: StudyReviewRepository, + private val credentialsService: CredentialsService +) : GetProtocolStageService { + override fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + if (presenter.isDone()) return + + val protocolDto = protocolRepository.findById(request.systematicStudyId) + if (protocolDto == null) { + val message = "Protocol not found for systematic study ${request.systematicStudyId}" + presenter.prepareFailView(EntityNotFoundException(message)) + return + } + + val allStudies = studyReviewRepository.findAllFromReview(request.systematicStudyId) + + val totalStudiesCount = allStudies.size + val includedStudiesCount = allStudies.count { it.selectionStatus == "INCLUDED" } + val extractedStudiesCount = allStudies.count { it.extractionStatus == "INCLUDED" } + + val stage = evaluateStage(protocolDto, totalStudiesCount, includedStudiesCount, extractedStudiesCount) + + presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) + } + + + private fun evaluateStage(dto: ProtocolDto, totalStudiesCount: Int, includedStudiesCount: Int, extractedStudiesCount: Int) : ProtocolStage { + if (dto.goal.isNullOrBlank() && dto.justification.isNullOrBlank()) { + return ProtocolStage.PROTOCOL_PART_I + } + + val picoc = dto.picoc + if (picoc == null || (picoc.population.isNullOrBlank() && picoc.intervention.isNullOrBlank() && + picoc.control.isNullOrBlank() && picoc.outcome.isNullOrBlank() && picoc.context.isNullOrBlank())) { + return ProtocolStage.PICOC + } + + if (dto.studiesLanguages.isEmpty() && dto.eligibilityCriteria.isEmpty() && + dto.informationSources.isEmpty() && dto.keywords.isEmpty() && + dto.sourcesSelectionCriteria.isNullOrBlank() && dto.searchMethod.isNullOrBlank() && + dto.selectionProcess.isNullOrBlank()) { + return ProtocolStage.PROTOCOL_PART_II + } + + val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } + val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } + + if (dto.extractionQuestions.isEmpty() || dto.robQuestions.isEmpty() || + !hasInclusionCriteria || !hasExclusionCriteria || + dto.informationSources.isEmpty() || + !dto.researchQuestions.isEmpty() || !dto.analysisAndSynthesisProcess.isNullOrBlank()) { + return ProtocolStage.PROTOCOL_PART_III + } + + if (totalStudiesCount == 0) { + return ProtocolStage.IDENTIFICATION + } + + if (includedStudiesCount == 0) { + return ProtocolStage.SELECTION + } + + if (extractedStudiesCount == 0) { + return ProtocolStage.EXTRACTION + } + + // This stage is reached when extraction is complete, but finalization has not yet begun. + // As Finalization criteria are not yet defined, this is the default final step. + return ProtocolStage.GRAPHICS + + // Finalization would be returned here once its criteria are defined. + } +} \ No newline at end of file From cb7a7bda38b88843ea7d620108e919e96113c429 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:03:22 -0300 Subject: [PATCH 58/74] feat(protocolStage): add protocol stage restful presenter --- .../RestfulGetProtocolStagePresenter.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt diff --git a/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt new file mode 100644 index 000000000..f65b36851 --- /dev/null +++ b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt @@ -0,0 +1,32 @@ +package br.all.protocol.presenter + +import br.all.application.protocol.find.GetProtocolStagePresenter +import br.all.application.protocol.find.GetProtocolStageService.ResponseModel +import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.UUID + +class RestfulGetProtocolStagePresenter( + private val linksFactory: LinksFactory +) : GetProtocolStagePresenter { + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: ResponseModel) { + val viewModel = ViewModel(response.userId, response.systematicStudyId, response.stage) + responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel) + } + + override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val researcherId: UUID, + val systematicStudyId: UUID, + val currentStage: ProtocolStage, + ) : RepresentationModel() +} \ No newline at end of file From 93e537a81aef8c3a5dbd36d0d72ddb3357211c40 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:03:38 -0300 Subject: [PATCH 59/74] feat(protocolStage): add protocol stage endpoint to protocol controller --- .../protocol/controller/ProtocolController.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt b/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt index 4bf028a1e..8ef4aef3a 100644 --- a/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt +++ b/web/src/main/kotlin/br/all/protocol/controller/ProtocolController.kt @@ -1,8 +1,10 @@ package br.all.protocol.controller import br.all.application.protocol.find.FindProtocolService +import br.all.application.protocol.find.GetProtocolStageService import br.all.application.protocol.update.UpdateProtocolService import br.all.protocol.presenter.RestfulFindProtocolPresenter +import br.all.protocol.presenter.RestfulGetProtocolStagePresenter import br.all.protocol.presenter.RestfulUpdateProtocolPresenter import br.all.protocol.requests.PutRequest import br.all.security.service.AuthenticationInfoService @@ -17,12 +19,14 @@ import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import java.util.* import br.all.application.protocol.find.FindProtocolService.RequestModel as FindOneRequestModel +import br.all.application.protocol.find.GetProtocolStageService.RequestModel as FindStageRequestModel @RestController @RequestMapping("/systematic-study/{systematicStudyId}/protocol") class ProtocolController( private val findProtocolService: FindProtocolService, private val updateProtocolService: UpdateProtocolService, + private val getProtocolStageService: GetProtocolStageService, private val authenticationInfoService: AuthenticationInfoService, private val linksFactory: LinksFactory ) { @@ -86,4 +90,38 @@ class ProtocolController( return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + @GetMapping("/stage") + @Operation(summary = "Get the current stage of the systematic review protocol") + @ApiResponses(value = [ + ApiResponse(responseCode = "200", description = "Success getting the protocol stage", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = GetProtocolStageService.ResponseModel::class) + )]), + ApiResponse( + responseCode = "401", + description = "Fail getting the protocol stage - unauthenticated collaborator", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail getting the protocol stage - unauthorized collaborator", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "404", + description = "Fail getting the protocol stage - nonexistent protocol or systematic study", + content = [Content(schema = Schema(hidden = true))] + ), + ]) + fun getStage( + @PathVariable systematicStudyId: UUID + ) : ResponseEntity<*> { + val presenter = RestfulGetProtocolStagePresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = FindStageRequestModel(userId, systematicStudyId) + + getProtocolStageService.getStage(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } From df81378314bc45875b25c43e98fc9cd7f3eaa5dc Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 30 Jun 2025 15:16:01 -0300 Subject: [PATCH 60/74] docs(protocolStage): fix swagger response model for protocol stage endpoint --- .../all/application/protocol/find/GetProtocolStageService.kt | 4 +++- .../protocol/presenter/RestfulGetProtocolStagePresenter.kt | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt index b4a9b2ccf..31ba40773 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageService.kt @@ -1,5 +1,6 @@ package br.all.application.protocol.find +import io.swagger.v3.oas.annotations.media.Schema import java.util.UUID interface GetProtocolStageService { @@ -10,10 +11,11 @@ interface GetProtocolStageService { val systematicStudyId: UUID ) + @Schema(name = "GetProtocolStageServiceResponseModel", description = "Response model for Get Protocol Stage Service") data class ResponseModel( val userId: UUID, val systematicStudyId: UUID, - val stage: ProtocolStage + val currentStage: ProtocolStage ) enum class ProtocolStage { diff --git a/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt index f65b36851..760cb5f3f 100644 --- a/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt +++ b/web/src/main/kotlin/br/all/protocol/presenter/RestfulGetProtocolStagePresenter.kt @@ -16,7 +16,7 @@ class RestfulGetProtocolStagePresenter( var responseEntity: ResponseEntity<*>? = null override fun prepareSuccessView(response: ResponseModel) { - val viewModel = ViewModel(response.userId, response.systematicStudyId, response.stage) + val viewModel = ViewModel(response.userId, response.systematicStudyId, response.currentStage) responseEntity = ResponseEntity.status(HttpStatus.OK).body(viewModel) } @@ -24,7 +24,7 @@ class RestfulGetProtocolStagePresenter( override fun isDone() = responseEntity != null - private data class ViewModel( + data class ViewModel( val researcherId: UUID, val systematicStudyId: UUID, val currentStage: ProtocolStage, From 0eb6c2082d3d868e6db77824f9ffe689f28df201 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 1 Jul 2025 14:31:52 -0300 Subject: [PATCH 61/74] test(protocolStage): add test setup and first test --- .../find/GetProtocolStageServiceImplTest.kt | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt diff --git a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt new file mode 100644 index 000000000..02e81308b --- /dev/null +++ b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt @@ -0,0 +1,104 @@ +package br.all.application.protocol.find + +import br.all.application.protocol.repository.ProtocolRepository +import br.all.application.protocol.util.TestDataFactory +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.user.CredentialsService +import br.all.application.util.PreconditionCheckerMockingNew +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.extension.ExtendWith +import java.util.UUID +import kotlin.test.Test +import br.all.application.protocol.find.GetProtocolStageService.RequestModel +import br.all.application.protocol.find.GetProtocolStageService.ResponseModel +import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage +import io.mockk.verify + +@Tag("UnitTest") +@Tag("ServiceTest") +@ExtendWith(MockKExtension::class) +class GetProtocolStageServiceImplTest { + + @MockK(relaxUnitFun = true) + private lateinit var protocolRepository: ProtocolRepository + + @MockK(relaxUnitFun = true) + private lateinit var systematicStudyRepository: SystematicStudyRepository + + @MockK(relaxUnitFun = true) + private lateinit var studyReviewRepository: StudyReviewRepository + + @MockK + private lateinit var credentialsService: CredentialsService + + @MockK(relaxUnitFun = true) + private lateinit var presenter: GetProtocolStagePresenter + + @InjectMockKs + private lateinit var sut: GetProtocolStageServiceImpl + + private lateinit var precondition: PreconditionCheckerMockingNew + private lateinit var factory: TestDataFactory + + private lateinit var researcherId: UUID + private lateinit var systematicStudyId: UUID + + @BeforeEach + fun setup() { + factory = TestDataFactory() + + researcherId = factory.researcher + systematicStudyId = factory.systematicStudy + + precondition = PreconditionCheckerMockingNew( + presenter, + credentialsService, + systematicStudyRepository, + researcherId, + systematicStudyId + ) + + precondition.makeEverythingWork() + } + + @Nested + @DisplayName("When successfully getting protocol's current stage") + inner class SuccessfullyGettingProtocolStage { + @Test + fun `should return protocol's first stage`() { + val protocolDto = factory.protocolDto( + systematicStudy = systematicStudyId, + goal = null, + justification = null + ) + + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + + val request = RequestModel( + userId = researcherId, + systematicStudyId = systematicStudyId + ) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel( + userId = researcherId, + systematicStudyId = systematicStudyId, + currentStage = ProtocolStage.PROTOCOL_PART_I + ) + + verify(exactly = 1) { + presenter.prepareSuccessView(expectedResponse) + } + } + } +} \ No newline at end of file From 4eded51c5cca37be662293da47f9e3dcc2ca3737 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 13:17:41 -0300 Subject: [PATCH 62/74] fix(protocolStage): change protocol part iii condition to properly catch the stage --- .../protocol/find/GetProtocolStageServiceImpl.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index c242cce42..f95e9c40d 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -70,10 +70,16 @@ class GetProtocolStageServiceImpl( val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } - if (dto.extractionQuestions.isEmpty() || dto.robQuestions.isEmpty() || - !hasInclusionCriteria || !hasExclusionCriteria || - dto.informationSources.isEmpty() || - !dto.researchQuestions.isEmpty() || !dto.analysisAndSynthesisProcess.isNullOrBlank()) { + val isSetupComplete = dto.extractionQuestions.isNotEmpty() && + dto.robQuestions.isNotEmpty() && + hasInclusionCriteria && + hasExclusionCriteria && + dto.informationSources.isNotEmpty() + + val areFinalFieldsFilled = dto.researchQuestions.isNotEmpty() && + !dto.analysisAndSynthesisProcess.isNullOrBlank() + + if (!isSetupComplete || !areFinalFieldsFilled) { return ProtocolStage.PROTOCOL_PART_III } From 5880f10d02b6f928210fade8d0d72abc5502f9ba Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 13:18:10 -0300 Subject: [PATCH 63/74] test(protocolStage): add all missing happy path tests --- .../find/GetProtocolStageServiceImplTest.kt | 210 ++++++++++++++++-- 1 file changed, 192 insertions(+), 18 deletions(-) diff --git a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt index 02e81308b..3f5ee9566 100644 --- a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt @@ -1,7 +1,8 @@ package br.all.application.protocol.find import br.all.application.protocol.repository.ProtocolRepository -import br.all.application.protocol.util.TestDataFactory +import br.all.application.protocol.util.TestDataFactory as ProtocolFactory +import br.all.application.study.util.TestDataFactory as StudyReviewFactory import br.all.application.review.repository.SystematicStudyRepository import br.all.application.study.repository.StudyReviewRepository import br.all.application.user.CredentialsService @@ -20,6 +21,8 @@ import kotlin.test.Test import br.all.application.protocol.find.GetProtocolStageService.RequestModel import br.all.application.protocol.find.GetProtocolStageService.ResponseModel import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage +import br.all.application.protocol.repository.CriterionDto +import br.all.application.protocol.repository.PicocDto import io.mockk.verify @Tag("UnitTest") @@ -46,17 +49,19 @@ class GetProtocolStageServiceImplTest { private lateinit var sut: GetProtocolStageServiceImpl private lateinit var precondition: PreconditionCheckerMockingNew - private lateinit var factory: TestDataFactory + private lateinit var protocolFactory: ProtocolFactory + private lateinit var studyReviewFactory: StudyReviewFactory private lateinit var researcherId: UUID private lateinit var systematicStudyId: UUID @BeforeEach fun setup() { - factory = TestDataFactory() + protocolFactory = ProtocolFactory() + studyReviewFactory = StudyReviewFactory() - researcherId = factory.researcher - systematicStudyId = factory.systematicStudy + researcherId = protocolFactory.researcher + systematicStudyId = protocolFactory.systematicStudy precondition = PreconditionCheckerMockingNew( presenter, @@ -73,32 +78,201 @@ class GetProtocolStageServiceImplTest { @DisplayName("When successfully getting protocol's current stage") inner class SuccessfullyGettingProtocolStage { @Test - fun `should return protocol's first stage`() { - val protocolDto = factory.protocolDto( + fun `should return PROTOCOL_PART_I stage when goal and justification are empty`() { + val protocolDto = protocolFactory.protocolDto( systematicStudy = systematicStudyId, goal = null, justification = null ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_I) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return PICOC stage when it is not filled`() { + val protocolDto = protocolFactory.protocolDto( + systematicStudy = systematicStudyId, + goal = "A goal", + justification = "A justification", + picoc = null + ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PICOC) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return PROTOCOL_PART_II stage when its fields are empty`() { + val protocolDto = protocolFactory.protocolDto( + systematicStudy = systematicStudyId, + goal = "A goal", + justification = "A justification", + picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"), + studiesLanguages = emptySet(), + eligibilityCriteria = emptySet(), + informationSources = emptySet(), + keywords = emptySet() + ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_II) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return PROTOCOL_PART_III stage when its conditions are not met`() { + val protocolDto = protocolFactory.protocolDto( + systematicStudy = systematicStudyId, + goal = "A goal", + justification = "A justification", + picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"), + keywords = setOf("test"), + extractionQuestions = emptySet() + ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.PROTOCOL_PART_III) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return IDENTIFICATION stage when no studies have been submitted`() { + val protocolDto = createFullProtocolDto() every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() - val request = RequestModel( - userId = researcherId, - systematicStudyId = systematicStudyId + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.IDENTIFICATION) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return SELECTION stage when studies exist but none are included`() { + val protocolDto = createFullProtocolDto() + + val studies = listOf( + createFullStudyReviewDto(selectionStatus = "REJECTED", extractionStatus = "PENDING") ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + + val request = RequestModel(researcherId, systematicStudyId) + sut.getStage(presenter, request) - val expectedResponse = ResponseModel( - userId = researcherId, - systematicStudyId = systematicStudyId, - currentStage = ProtocolStage.PROTOCOL_PART_I + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.SELECTION) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return EXTRACTION stage when studies are included but none are extracted`() { + val protocolDto = createFullProtocolDto() + + val studies = listOf( + createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "PENDING") ) - verify(exactly = 1) { - presenter.prepareSuccessView(expectedResponse) - } + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.EXTRACTION) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } + } + + @Test + fun `should return GRAPHICS stage when studies have been extracted`() { + val protocolDto = createFullProtocolDto() + + val studies = listOf( + createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "INCLUDED") + ) + + every { protocolRepository.findById(systematicStudyId) } returns protocolDto + every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + + val request = RequestModel(researcherId, systematicStudyId) + + sut.getStage(presenter, request) + + val expectedResponse = ResponseModel(researcherId, systematicStudyId, ProtocolStage.GRAPHICS) + verify(exactly = 1) { presenter.prepareSuccessView(expectedResponse) } } } -} \ No newline at end of file + + private fun createFullProtocolDto() = protocolFactory.protocolDto( + systematicStudy = systematicStudyId, + goal = "A goal", + justification = "A justification", + picoc = PicocDto(population = "P", intervention = "I", control = "C", outcome = "O", context = "C"), + studiesLanguages = setOf("English"), + eligibilityCriteria = setOf( + CriterionDto("test description", "INCLUSION"), + CriterionDto("test description", "EXCLUSION") + ), + informationSources = setOf("IEEE"), + keywords = setOf("test"), + sourcesSelectionCriteria = "criteria", + searchMethod = "method", + selectionProcess = "process", + extractionQuestions = setOf(UUID.randomUUID()), + robQuestions = setOf(UUID.randomUUID()), + researchQuestions = setOf("RQ1?"), + analysisAndSynthesisProcess = "process" + ) + + private fun createFullStudyReviewDto(selectionStatus: String, extractionStatus: String) = studyReviewFactory.generateDto( + studyReviewId = 1L, + systematicStudyId = systematicStudyId, + searchSessionId = UUID.randomUUID(), + type = "type", + title = "title", + year = 2025, + authors = "authors", + venue = "venue", + abstract = "abstract", + keywords = emptySet(), + references = emptyList(), + doi = "doi", + sources = emptySet(), + criteria = emptySet(), + formAnswers = emptyMap(), + robAnswers = emptyMap(), + comments = "comments", + readingPriority = "HIGH", + extractionStatus = extractionStatus, + selectionStatus = selectionStatus, + score = 10 + ) +} From fe3edb3a43b7f9568fa86efcd3c86547baa0000b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 14:34:58 -0300 Subject: [PATCH 64/74] fix(protocolStage): change picoc validation --- .../find/GetProtocolStageServiceImpl.kt | 19 ++++++++++++++----- .../find/GetProtocolStageServiceImplTest.kt | 8 +++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index f95e9c40d..cf1ccb02c 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -44,6 +44,7 @@ class GetProtocolStageServiceImpl( val extractedStudiesCount = allStudies.count { it.extractionStatus == "INCLUDED" } val stage = evaluateStage(protocolDto, totalStudiesCount, includedStudiesCount, extractedStudiesCount) + println(stage) presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) } @@ -55,9 +56,18 @@ class GetProtocolStageServiceImpl( } val picoc = dto.picoc - if (picoc == null || (picoc.population.isNullOrBlank() && picoc.intervention.isNullOrBlank() && - picoc.control.isNullOrBlank() && picoc.outcome.isNullOrBlank() && picoc.context.isNullOrBlank())) { - return ProtocolStage.PICOC + val picocIsStarted = picoc != null && ( + !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() || + !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank() + ) + + if (picocIsStarted) { + val picocIsCompleted = !picoc!!.population.isNullOrBlank() && !picoc.intervention.isNullOrBlank() && + !picoc.control.isNullOrBlank() && !picoc.outcome.isNullOrBlank() && !picoc.context.isNullOrBlank() + + if (!picocIsCompleted) { + return ProtocolStage.PICOC + } } if (dto.studiesLanguages.isEmpty() && dto.eligibilityCriteria.isEmpty() && @@ -76,8 +86,7 @@ class GetProtocolStageServiceImpl( hasExclusionCriteria && dto.informationSources.isNotEmpty() - val areFinalFieldsFilled = dto.researchQuestions.isNotEmpty() && - !dto.analysisAndSynthesisProcess.isNullOrBlank() + val areFinalFieldsFilled = !dto.analysisAndSynthesisProcess.isNullOrBlank() if (!isSetupComplete || !areFinalFieldsFilled) { return ProtocolStage.PROTOCOL_PART_III diff --git a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt index 3f5ee9566..bee856a40 100644 --- a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt @@ -101,7 +101,13 @@ class GetProtocolStageServiceImplTest { systematicStudy = systematicStudyId, goal = "A goal", justification = "A justification", - picoc = null + picoc = PicocDto( + population = "P", + intervention = null, + control = null, + outcome = null, + context = null + ) ) every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() From a5cf5e231525e3c1cba0dfc05c2ad6c5a859eaac Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 15:03:58 -0300 Subject: [PATCH 65/74] fix(protocolStage): invert protocol part iii condition check --- .../application/protocol/find/GetProtocolStageServiceImpl.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index cf1ccb02c..4a5c5e2aa 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -44,7 +44,6 @@ class GetProtocolStageServiceImpl( val extractedStudiesCount = allStudies.count { it.extractionStatus == "INCLUDED" } val stage = evaluateStage(protocolDto, totalStudiesCount, includedStudiesCount, extractedStudiesCount) - println(stage) presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) } @@ -86,7 +85,7 @@ class GetProtocolStageServiceImpl( hasExclusionCriteria && dto.informationSources.isNotEmpty() - val areFinalFieldsFilled = !dto.analysisAndSynthesisProcess.isNullOrBlank() + val areFinalFieldsFilled = dto.researchQuestions.isNotEmpty() && dto.analysisAndSynthesisProcess.isNullOrBlank() if (!isSetupComplete || !areFinalFieldsFilled) { return ProtocolStage.PROTOCOL_PART_III From 49995d3c590620663e0ea2db6da383ac30c76e3e Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 15:48:21 -0300 Subject: [PATCH 66/74] fix(protocolStage): separate protocol part iii condition into two different conditions --- .../protocol/find/GetProtocolStageServiceImpl.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index 4a5c5e2aa..273356714 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -85,9 +85,14 @@ class GetProtocolStageServiceImpl( hasExclusionCriteria && dto.informationSources.isNotEmpty() - val areFinalFieldsFilled = dto.researchQuestions.isNotEmpty() && dto.analysisAndSynthesisProcess.isNullOrBlank() + if (!isSetupComplete) { + return ProtocolStage.PROTOCOL_PART_III + } + + val areFinalFieldsEmpty = dto.researchQuestions.isEmpty() && + dto.analysisAndSynthesisProcess.isNullOrBlank() - if (!isSetupComplete || !areFinalFieldsFilled) { + if (areFinalFieldsEmpty) { return ProtocolStage.PROTOCOL_PART_III } From 10f1a3f1b069ed600302517cb35b738af0fbcdee Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Wed, 2 Jul 2025 16:06:47 -0300 Subject: [PATCH 67/74] refactor(protocolStage): separate protocol part iii into two different condition gates --- .../find/GetProtocolStageServiceImpl.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index 273356714..b287a8ec9 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -79,20 +79,13 @@ class GetProtocolStageServiceImpl( val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } - val isSetupComplete = dto.extractionQuestions.isNotEmpty() && - dto.robQuestions.isNotEmpty() && - hasInclusionCriteria && + val isProtocolTextComplete = hasInclusionCriteria && hasExclusionCriteria && - dto.informationSources.isNotEmpty() + dto.informationSources.isNotEmpty() && + dto.researchQuestions.isNotEmpty() && + !dto.analysisAndSynthesisProcess.isNullOrBlank() - if (!isSetupComplete) { - return ProtocolStage.PROTOCOL_PART_III - } - - val areFinalFieldsEmpty = dto.researchQuestions.isEmpty() && - dto.analysisAndSynthesisProcess.isNullOrBlank() - - if (areFinalFieldsEmpty) { + if (!isProtocolTextComplete) { return ProtocolStage.PROTOCOL_PART_III } @@ -104,6 +97,13 @@ class GetProtocolStageServiceImpl( return ProtocolStage.SELECTION } + val areExtractionFormsDefined = dto.extractionQuestions.isNotEmpty() && + dto.robQuestions.isNotEmpty() + + if (!areExtractionFormsDefined) { + return ProtocolStage.PROTOCOL_PART_III + } + if (extractedStudiesCount == 0) { return ProtocolStage.EXTRACTION } From b355f68d8749bea4c82c76f1a59b6e8741641c1a Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 3 Jul 2025 14:56:49 -0300 Subject: [PATCH 68/74] refactor(protocolStage): change protocol part iii conditional logic --- .../protocol/find/GetProtocolStageServiceImpl.kt | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index b287a8ec9..39f17ff77 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -78,14 +78,15 @@ class GetProtocolStageServiceImpl( val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } + val extractionAndRobDefined = dto.extractionQuestions.isNotEmpty() && dto.robQuestions.isNotEmpty() + val hasDatabases = dto.informationSources.isNotEmpty() - val isProtocolTextComplete = hasInclusionCriteria && - hasExclusionCriteria && - dto.informationSources.isNotEmpty() && + val protocolPartIIICompleted = hasInclusionCriteria && hasExclusionCriteria && + extractionAndRobDefined && hasDatabases && dto.researchQuestions.isNotEmpty() && !dto.analysisAndSynthesisProcess.isNullOrBlank() - if (!isProtocolTextComplete) { + if (!protocolPartIIICompleted) { return ProtocolStage.PROTOCOL_PART_III } @@ -97,13 +98,6 @@ class GetProtocolStageServiceImpl( return ProtocolStage.SELECTION } - val areExtractionFormsDefined = dto.extractionQuestions.isNotEmpty() && - dto.robQuestions.isNotEmpty() - - if (!areExtractionFormsDefined) { - return ProtocolStage.PROTOCOL_PART_III - } - if (extractedStudiesCount == 0) { return ProtocolStage.EXTRACTION } From 2b8e6b6a018ee411e36dddb36b7703e0175cddda Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Thu, 3 Jul 2025 15:10:57 -0300 Subject: [PATCH 69/74] test(protocolStage): add controller tests for the stage endpoint --- .../controller/ProtocolControllerTest.kt | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt index e305b0a7e..b99006f85 100644 --- a/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt +++ b/web/src/test/kotlin/br/all/protocol/controller/ProtocolControllerTest.kt @@ -65,6 +65,12 @@ class ProtocolControllerTest( private fun getUrl(systematicStudy: UUID = factory.protocol) = "/systematic-study/$systematicStudy/protocol" + private fun putUrl(systematicStudyId: UUID = factory.protocol) = + "/systematic-study/$systematicStudyId/protocol" + + private fun getStage(systematicStudy: UUID = factory.protocol) = + "/systematic-study/$systematicStudy/protocol/stage" + @Nested @DisplayName("When getting protocols") inner class WhenGettingProtocols { @@ -126,9 +132,6 @@ class ProtocolControllerTest( } } - private fun putUrl(systematicStudyId: UUID = factory.protocol) = - "/systematic-study/$systematicStudyId/protocol" - @Nested @DisplayName("When putting protocols") inner class WhenPuttingProtocols { @@ -255,4 +258,43 @@ class ProtocolControllerTest( } } } + + @Nested + @DisplayName("When getting protocol stage") + inner class WhenGettingProtocolStage { + @Test + fun `should return protocol stage with 200 status when protocol exists`() { + val document = factory.createProtocolDocument() + protocolRepository.save(document) + + mockMvc.perform(get(getStage()) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.systematicStudyId").value(factory.protocol.toString())) + .andExpect(jsonPath("$.currentStage").exists()) + } + + @Test + fun `should return 404 when protocol does not exist`() { + mockMvc.perform(get(getStage()) + .with(SecurityMockMvcRequestPostProcessors.user(user)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound) + .andExpect(jsonPath("$.message").exists()) + .andExpect(jsonPath("$.detail").exists()) + } + + @Test + fun `should not authorize researchers that are not collaborators to get protocol stage`() { + testHelperService.testForUnauthorizedUser(mockMvc, get(getStage())) + } + + @Test + fun `should not allow unauthenticated users to get protocol stage`() { + testHelperService.testForUnauthenticatedUser(mockMvc, get(getStage())) + } + } } \ No newline at end of file From c1ac0978b6ef6b0de7dfec83115d100d5cf12ac1 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Fri, 4 Jul 2025 12:55:57 -0300 Subject: [PATCH 70/74] build: change actions to main branch --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 733eec78e..b96aca1da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI with Maven 2025 on: push: - branches: [ "fix-pom" ] + branches: [ "main" ] pull_request: branches: [ "main" ] From 99b801c9db90ce9657766e0cedd11918cf001870 Mon Sep 17 00:00:00 2001 From: ericksgmes Date: Fri, 4 Jul 2025 13:52:13 -0300 Subject: [PATCH 71/74] fix: identification bug and add configuration --- .../find/GetProtocolStageServiceImpl.kt | 32 ++++++++++++++++--- .../ProtocolServicesConfiguration.kt | 11 +++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index 39f17ff77..4ee1e3cfc 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -7,10 +7,12 @@ import br.all.application.protocol.find.GetProtocolStageService.RequestModel import br.all.application.protocol.find.GetProtocolStageService.ResponseModel import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage import br.all.application.protocol.repository.ProtocolDto +import br.all.application.question.repository.QuestionRepository import br.all.application.review.repository.fromDto import br.all.application.shared.exceptions.EntityNotFoundException import br.all.application.shared.presenter.prepareIfFailsPreconditions import br.all.application.study.repository.StudyReviewRepository +import br.all.domain.model.question.QuestionContextEnum import br.all.domain.model.review.SystematicStudy import org.springframework.stereotype.Service @@ -19,7 +21,8 @@ class GetProtocolStageServiceImpl( private val protocolRepository: ProtocolRepository, private val systematicStudyRepository: SystematicStudyRepository, private val studyReviewRepository: StudyReviewRepository, - private val credentialsService: CredentialsService + private val credentialsService: CredentialsService, + private val questionRepository: QuestionRepository ) : GetProtocolStageService { override fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel) { val user = credentialsService.loadCredentials(request.userId)?.toUser() @@ -38,18 +41,33 @@ class GetProtocolStageServiceImpl( } val allStudies = studyReviewRepository.findAllFromReview(request.systematicStudyId) - + val robQuestions = questionRepository.findAllBySystematicStudyId(systematicStudyDto!!.id, QuestionContextEnum.ROB).size + val extractionQuestions = questionRepository.findAllBySystematicStudyId(systematicStudyDto.id, QuestionContextEnum.EXTRACTION).size val totalStudiesCount = allStudies.size val includedStudiesCount = allStudies.count { it.selectionStatus == "INCLUDED" } val extractedStudiesCount = allStudies.count { it.extractionStatus == "INCLUDED" } - val stage = evaluateStage(protocolDto, totalStudiesCount, includedStudiesCount, extractedStudiesCount) + val stage = evaluateStage( + protocolDto, + totalStudiesCount, + includedStudiesCount, + extractedStudiesCount, + robQuestions, + extractionQuestions, + ) presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) } - private fun evaluateStage(dto: ProtocolDto, totalStudiesCount: Int, includedStudiesCount: Int, extractedStudiesCount: Int) : ProtocolStage { + private fun evaluateStage( + dto: ProtocolDto, + totalStudiesCount: Int, + includedStudiesCount: Int, + extractedStudiesCount: Int, + robQuestionCount: Int, + extractionQuestionsCount: Int + ): ProtocolStage { if (dto.goal.isNullOrBlank() && dto.justification.isNullOrBlank()) { return ProtocolStage.PROTOCOL_PART_I } @@ -78,7 +96,7 @@ class GetProtocolStageServiceImpl( val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } - val extractionAndRobDefined = dto.extractionQuestions.isNotEmpty() && dto.robQuestions.isNotEmpty() + val extractionAndRobDefined = isThereExtractionAndRobQuestions(robQuestionCount, extractionQuestionsCount) val hasDatabases = dto.informationSources.isNotEmpty() val protocolPartIIICompleted = hasInclusionCriteria && hasExclusionCriteria && @@ -108,4 +126,8 @@ class GetProtocolStageServiceImpl( // Finalization would be returned here once its criteria are defined. } + + private fun isThereExtractionAndRobQuestions(robQuestionCount: Int, extractionQuestionsCount: Int): Boolean { + return robQuestionCount > 0 && extractionQuestionsCount > 0 + } } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt b/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt index 70923399e..11efb480f 100644 --- a/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt +++ b/web/src/main/kotlin/br/all/protocol/controller/ProtocolServicesConfiguration.kt @@ -1,8 +1,10 @@ package br.all.protocol.controller import br.all.application.protocol.find.FindProtocolServiceImpl +import br.all.application.protocol.find.GetProtocolStageServiceImpl import br.all.application.protocol.repository.ProtocolRepository import br.all.application.protocol.update.UpdateProtocolServiceImpl +import br.all.application.question.repository.QuestionRepository import br.all.application.review.repository.SystematicStudyRepository import br.all.application.study.repository.StudyReviewRepository import br.all.application.user.CredentialsService @@ -27,4 +29,13 @@ class ProtocolServicesConfiguration { studyReviewRepository: StudyReviewRepository, scoreCalculatorService: ScoreCalculatorService ) = UpdateProtocolServiceImpl(protocolRepository, systematicStudyRepository, credentialsService, studyReviewRepository, scoreCalculatorService) + + @Bean + fun getProtocolStageService( + protocolRepository: ProtocolRepository, + systematicStudyRepository: SystematicStudyRepository, + studyReviewRepository: StudyReviewRepository, + credentialsService: CredentialsService, + questionRepository: QuestionRepository + ) = GetProtocolStageServiceImpl(protocolRepository, systematicStudyRepository, studyReviewRepository, credentialsService, questionRepository) } From 454ed5fdbabe80f2ebf151a1b0b4be6a9c94a6e8 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Fri, 4 Jul 2025 14:45:09 -0300 Subject: [PATCH 72/74] refactor(protocolStage): extract functions from condition for readability reasons --- .../find/GetProtocolStageServiceImpl.kt | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt index 4ee1e3cfc..22403c249 100644 --- a/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImpl.kt @@ -26,7 +26,6 @@ class GetProtocolStageServiceImpl( ) : GetProtocolStageService { override fun getStage(presenter: GetProtocolStagePresenter, request: RequestModel) { val user = credentialsService.loadCredentials(request.userId)?.toUser() - val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } @@ -43,6 +42,7 @@ class GetProtocolStageServiceImpl( 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" } @@ -59,7 +59,6 @@ class GetProtocolStageServiceImpl( presenter.prepareSuccessView(ResponseModel(request.userId, request.systematicStudyId, stage)) } - private fun evaluateStage( dto: ProtocolDto, totalStudiesCount: Int, @@ -68,66 +67,63 @@ class GetProtocolStageServiceImpl( robQuestionCount: Int, extractionQuestionsCount: Int ): ProtocolStage { - if (dto.goal.isNullOrBlank() && dto.justification.isNullOrBlank()) { - return ProtocolStage.PROTOCOL_PART_I + return when { + isProtocolPartI(dto) -> ProtocolStage.PROTOCOL_PART_I + picocStage(dto) -> ProtocolStage.PICOC + isProtocolPartII(dto) -> ProtocolStage.PROTOCOL_PART_II + !isProtocolPartIIICompleted(dto, robQuestionCount, extractionQuestionsCount) -> ProtocolStage.PROTOCOL_PART_III + totalStudiesCount == 0 -> ProtocolStage.IDENTIFICATION + includedStudiesCount == 0 -> ProtocolStage.SELECTION + extractedStudiesCount == 0 -> ProtocolStage.EXTRACTION + else -> ProtocolStage.GRAPHICS } + } - val picoc = dto.picoc - val picocIsStarted = picoc != null && ( - !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() || - !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank() - ) + private fun isProtocolPartI(dto: ProtocolDto): Boolean { + return dto.goal.isNullOrBlank() && dto.justification.isNullOrBlank() + } - if (picocIsStarted) { - val picocIsCompleted = !picoc!!.population.isNullOrBlank() && !picoc.intervention.isNullOrBlank() && - !picoc.control.isNullOrBlank() && !picoc.outcome.isNullOrBlank() && !picoc.context.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() + } - if (!picocIsCompleted) { - return ProtocolStage.PICOC - } - } + 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) } - if (dto.studiesLanguages.isEmpty() && dto.eligibilityCriteria.isEmpty() && - dto.informationSources.isEmpty() && dto.keywords.isEmpty() && - dto.sourcesSelectionCriteria.isNullOrBlank() && dto.searchMethod.isNullOrBlank() && - dto.selectionProcess.isNullOrBlank()) { - return ProtocolStage.PROTOCOL_PART_II - } + val hasExtractionAndRob = robQuestionCount > 0 && extractionQuestionsCount > 0 - val hasInclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("INCLUSION", true) } - val hasExclusionCriteria = dto.eligibilityCriteria.any { it.type.equals("EXCLUSION", true) } - val extractionAndRobDefined = isThereExtractionAndRobQuestions(robQuestionCount, extractionQuestionsCount) val hasDatabases = dto.informationSources.isNotEmpty() + val hasResearchQuestions = dto.researchQuestions.isNotEmpty() + val hasAnalysisProcess = !dto.analysisAndSynthesisProcess.isNullOrBlank() - val protocolPartIIICompleted = hasInclusionCriteria && hasExclusionCriteria && - extractionAndRobDefined && hasDatabases && - dto.researchQuestions.isNotEmpty() && - !dto.analysisAndSynthesisProcess.isNullOrBlank() + return hasInclusionCriteria && hasExclusionCriteria && + hasExtractionAndRob && hasDatabases && + hasResearchQuestions && hasAnalysisProcess + } - if (!protocolPartIIICompleted) { - return ProtocolStage.PROTOCOL_PART_III - } + private fun picocStage(dto: ProtocolDto): Boolean { + val picoc = dto.picoc + if (picoc == null) return false - if (totalStudiesCount == 0) { - return ProtocolStage.IDENTIFICATION - } + val picocIsStarted = !picoc.population.isNullOrBlank() || !picoc.intervention.isNullOrBlank() || + !picoc.control.isNullOrBlank() || !picoc.outcome.isNullOrBlank() || !picoc.context.isNullOrBlank() - if (includedStudiesCount == 0) { - return ProtocolStage.SELECTION - } + if (picocIsStarted) { + val picocIsCompleted = !picoc.population.isNullOrBlank() && !picoc.intervention.isNullOrBlank() && + !picoc.control.isNullOrBlank() && !picoc.outcome.isNullOrBlank() && !picoc.context.isNullOrBlank() - if (extractedStudiesCount == 0) { - return ProtocolStage.EXTRACTION + if (!picocIsCompleted) { + return true + } } - // This stage is reached when extraction is complete, but finalization has not yet begun. - // As Finalization criteria are not yet defined, this is the default final step. - return ProtocolStage.GRAPHICS - - // Finalization would be returned here once its criteria are defined. - } - - private fun isThereExtractionAndRobQuestions(robQuestionCount: Int, extractionQuestionsCount: Int): Boolean { - return robQuestionCount > 0 && extractionQuestionsCount > 0 + return false } -} \ No newline at end of file +} From a627b954716f46d46b8313cf16b200a824f5ecef Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Fri, 4 Jul 2025 14:45:19 -0300 Subject: [PATCH 73/74] test(protocolStage): fix broken tests after refactoring --- .../find/GetProtocolStageServiceImplTest.kt | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt index bee856a40..37c359294 100644 --- a/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt +++ b/review/src/test/kotlin/br/all/application/protocol/find/GetProtocolStageServiceImplTest.kt @@ -3,6 +3,7 @@ package br.all.application.protocol.find import br.all.application.protocol.repository.ProtocolRepository import br.all.application.protocol.util.TestDataFactory as ProtocolFactory import br.all.application.study.util.TestDataFactory as StudyReviewFactory +import br.all.application.question.util.TestDataFactory as QuestionFactory import br.all.application.review.repository.SystematicStudyRepository import br.all.application.study.repository.StudyReviewRepository import br.all.application.user.CredentialsService @@ -23,6 +24,8 @@ import br.all.application.protocol.find.GetProtocolStageService.ResponseModel import br.all.application.protocol.find.GetProtocolStageService.ProtocolStage import br.all.application.protocol.repository.CriterionDto import br.all.application.protocol.repository.PicocDto +import br.all.application.question.repository.QuestionRepository +import br.all.domain.model.question.QuestionContextEnum import io.mockk.verify @Tag("UnitTest") @@ -39,6 +42,9 @@ class GetProtocolStageServiceImplTest { @MockK(relaxUnitFun = true) private lateinit var studyReviewRepository: StudyReviewRepository + @MockK(relaxUnitFun = true) + private lateinit var questionRepository: QuestionRepository + @MockK private lateinit var credentialsService: CredentialsService @@ -51,6 +57,7 @@ class GetProtocolStageServiceImplTest { private lateinit var precondition: PreconditionCheckerMockingNew private lateinit var protocolFactory: ProtocolFactory private lateinit var studyReviewFactory: StudyReviewFactory + private lateinit var questionFactory: QuestionFactory private lateinit var researcherId: UUID private lateinit var systematicStudyId: UUID @@ -59,6 +66,7 @@ class GetProtocolStageServiceImplTest { fun setup() { protocolFactory = ProtocolFactory() studyReviewFactory = StudyReviewFactory() + questionFactory = QuestionFactory() researcherId = protocolFactory.researcher systematicStudyId = protocolFactory.systematicStudy @@ -84,8 +92,11 @@ class GetProtocolStageServiceImplTest { goal = null, justification = null ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList() val request = RequestModel(researcherId, systematicStudyId) @@ -109,8 +120,11 @@ class GetProtocolStageServiceImplTest { 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) @@ -132,8 +146,11 @@ class GetProtocolStageServiceImplTest { informationSources = emptySet(), keywords = emptySet() ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList() val request = RequestModel(researcherId, systematicStudyId) @@ -153,8 +170,11 @@ class GetProtocolStageServiceImplTest { keywords = setOf("test"), extractionQuestions = emptySet() ) + every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns emptyList() val request = RequestModel(researcherId, systematicStudyId) @@ -167,9 +187,14 @@ class GetProtocolStageServiceImplTest { @Test fun `should return IDENTIFICATION stage when no studies have been submitted`() { val protocolDto = createFullProtocolDto() + val questions = listOf( + questionFactory.generateTextualDto() + ) every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns emptyList() + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions val request = RequestModel(researcherId, systematicStudyId) @@ -182,13 +207,17 @@ class GetProtocolStageServiceImplTest { @Test fun `should return SELECTION stage when studies exist but none are included`() { val protocolDto = createFullProtocolDto() - val studies = listOf( - createFullStudyReviewDto(selectionStatus = "REJECTED", extractionStatus = "PENDING") + createFullStudyReviewDto(selectionStatus = "", extractionStatus = "") + ) + val questions = listOf( + questionFactory.generateTextualDto() ) every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions val request = RequestModel(researcherId, systematicStudyId) @@ -201,13 +230,17 @@ class GetProtocolStageServiceImplTest { @Test fun `should return EXTRACTION stage when studies are included but none are extracted`() { val protocolDto = createFullProtocolDto() - val studies = listOf( - createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "PENDING") + createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "") + ) + val questions = listOf( + questionFactory.generateTextualDto() ) every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions val request = RequestModel(researcherId, systematicStudyId) @@ -220,13 +253,17 @@ class GetProtocolStageServiceImplTest { @Test fun `should return GRAPHICS stage when studies have been extracted`() { val protocolDto = createFullProtocolDto() - val studies = listOf( createFullStudyReviewDto(selectionStatus = "INCLUDED", extractionStatus = "INCLUDED") ) + val questions = listOf( + questionFactory.generateTextualDto() + ) every { protocolRepository.findById(systematicStudyId) } returns protocolDto every { studyReviewRepository.findAllFromReview(systematicStudyId) } returns studies + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.ROB) } returns questions + every { questionRepository.findAllBySystematicStudyId(systematicStudyId, QuestionContextEnum.EXTRACTION) } returns questions val request = RequestModel(researcherId, systematicStudyId) @@ -237,6 +274,7 @@ class GetProtocolStageServiceImplTest { } } + private fun createFullProtocolDto() = protocolFactory.protocolDto( systematicStudy = systematicStudyId, goal = "A goal", From eb3cdcbc8ae1467e604694aebf09e1c42e36b430 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Fri, 4 Jul 2025 14:47:25 -0300 Subject: [PATCH 74/74] feat(batchAnswer): add controller configuration for batch endpoint --- .../controller/StudyReviewServicesConfiguration.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt index 95ff740fd..ea5d2acd6 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt @@ -139,6 +139,19 @@ class StudyReviewServicesConfiguration { credentialsService ) + @Bean + fun batchAnswerQuestionService( + studyReviewRepository: StudyReviewRepository, + questionRepository: QuestionRepository, + systematicStudyRepository: SystematicStudyRepository, + credentialsService: CredentialsService + ) = BatchAnswerQuestionServiceImpl( + studyReviewRepository, + questionRepository, + systematicStudyRepository, + credentialsService + ) + @Bean fun removeCriteriaService( studyReviewRepository: StudyReviewRepository,