From 34df9eeae03f98842e7e976f57110c3afb2181f6 Mon Sep 17 00:00:00 2001 From: ogunes-ebi Date: Wed, 29 Apr 2026 13:25:59 +0100 Subject: [PATCH 1/3] Add comprehensive tests, upgrade dependencies, and enhance CI/CD pipeline - Introduced unit tests for repositories and core models. - Upgraded Spring Boot to 4.0.5 and updated Java to version 25. - Refined application properties for MongoDB configuration. - Enhanced `.gitlab-ci.yml` to include SAST and Trivy scanning steps. - Improved test suite with MockMvc and repository context validation. --- .gitlab-ci.yml | 50 +++++- pom.xml | 51 +++++- .../api/ImpcMousephenotypeApiApplication.java | 2 +- .../mousephenotype/api/models/GeneBundle.java | 2 - .../mousephenotype/api/models/GeneImage.java | 1 - .../api/repositories/GeneRepository.java | 5 - .../resources/application-docker.properties | 4 +- .../resources/application-local.properties | 4 +- src/main/resources/application.properties | 4 +- ...ImpcMousephenotypeApiApplicationTests.java | 72 +++++++- .../configuration/GeneConfigurationTest.java | 73 ++++++++ .../api/models/ModelContractTest.java | 159 ++++++++++++++++++ .../CustomObservationRepositoryImplTest.java | 15 ++ .../RepositoryDefinitionsTest.java | 83 +++++++++ 14 files changed, 506 insertions(+), 19 deletions(-) create mode 100644 src/test/java/org/mousephenotype/api/configuration/GeneConfigurationTest.java create mode 100644 src/test/java/org/mousephenotype/api/models/ModelContractTest.java create mode 100644 src/test/java/org/mousephenotype/api/repositories/CustomObservationRepositoryImplTest.java create mode 100644 src/test/java/org/mousephenotype/api/repositories/RepositoryDefinitionsTest.java diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4755292..bdb1d23 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,12 +37,21 @@ variables: # Logged as: GitLab.com CI jobs failing if using docker:stable-dind image # see: https://gitlab.com/gitlab-com/gl-infra/production/issues/982 DOCKER_TLS_CERTDIR: "" + GITLAB_ADVANCED_SAST_ENABLED: 'true' + +include: + - template: Security/SAST.gitlab-ci.yml + - template: Jobs/Secret-Detection.gitlab-ci.yml + +sast: + stage: test stages: # - env - build + - test - dev-deploy - prod-deploy @@ -184,4 +193,43 @@ hx-prod: fi kubectl get pod,deployment,rs,svc,ing - fi \ No newline at end of file + fi + + + + +trivy_container_scanning: + stage: test + + services: + - name: $CI_REGISTRY/mouse-informatics/dind:latest + alias: docker + + rules: + - if: '$CI_COMMIT_REF_NAME == "main"' + when: on_success + allow_failure: true + + before_script: + - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + - echo $TRIVY_VERSION + - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - + - echo "${CI_REGISTRY_PASSWORD}" | docker login -u "${CI_REGISTRY_USER}" --password-stdin ${CI_REGISTRY} + + script: + # Build report + - ./trivy --cache-dir .trivycache/ image --exit-code 0 --no-progress --format template --template "@contrib/gitlab.tpl" -o gl-container-scanning-report.json "${CI_REGISTRY_IMAGE}":"${CI_COMMIT_SHA:0:12}" + # Print report + - ./trivy --cache-dir .trivycache/ image --exit-code 0 --no-progress --severity HIGH "${CI_REGISTRY_IMAGE}":"${CI_COMMIT_SHA:0:12}" + # Fail on critical vulnerability + - ./trivy --cache-dir .trivycache/ image --exit-code 1 --severity CRITICAL --no-progress "${CI_REGISTRY_IMAGE}":"${CI_COMMIT_SHA:0:12}" + + - docker logout ${CI_REGISTRY} + + cache: + paths: + - .trivycache/ + + artifacts: + reports: + container_scanning: gl-container-scanning-report.json \ No newline at end of file diff --git a/pom.xml b/pom.xml index 26426eb..8dbc691 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.5.2 + 4.0.5 org.mousephenotype @@ -14,7 +14,8 @@ impc-mousephenotype-api Demo project for Spring Boot - 11 + 25 + 1.18.44 @@ -64,6 +65,26 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${project.parent.version} + + + + org.asciidoctor asciidoctor-maven-plugin @@ -89,6 +110,32 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + + prepare-agent + + + + report + test + + report + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + org.springframework.boot spring-boot-maven-plugin diff --git a/src/main/java/org/mousephenotype/api/ImpcMousephenotypeApiApplication.java b/src/main/java/org/mousephenotype/api/ImpcMousephenotypeApiApplication.java index bc09b5c..7014730 100644 --- a/src/main/java/org/mousephenotype/api/ImpcMousephenotypeApiApplication.java +++ b/src/main/java/org/mousephenotype/api/ImpcMousephenotypeApiApplication.java @@ -6,7 +6,7 @@ @SpringBootApplication public class ImpcMousephenotypeApiApplication { - public static void main(String[] args) { + static void main(String[] args) { SpringApplication.run(ImpcMousephenotypeApiApplication.class, args); } diff --git a/src/main/java/org/mousephenotype/api/models/GeneBundle.java b/src/main/java/org/mousephenotype/api/models/GeneBundle.java index 456b740..afc9427 100644 --- a/src/main/java/org/mousephenotype/api/models/GeneBundle.java +++ b/src/main/java/org/mousephenotype/api/models/GeneBundle.java @@ -4,8 +4,6 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import net.minidev.json.annotate.JsonIgnore; -import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; diff --git a/src/main/java/org/mousephenotype/api/models/GeneImage.java b/src/main/java/org/mousephenotype/api/models/GeneImage.java index 2ac7804..c6cf41b 100644 --- a/src/main/java/org/mousephenotype/api/models/GeneImage.java +++ b/src/main/java/org/mousephenotype/api/models/GeneImage.java @@ -5,7 +5,6 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.Date; import java.util.List; @Data diff --git a/src/main/java/org/mousephenotype/api/repositories/GeneRepository.java b/src/main/java/org/mousephenotype/api/repositories/GeneRepository.java index d56b6e1..103fde6 100644 --- a/src/main/java/org/mousephenotype/api/repositories/GeneRepository.java +++ b/src/main/java/org/mousephenotype/api/repositories/GeneRepository.java @@ -1,18 +1,13 @@ package org.mousephenotype.api.repositories; -import org.bson.types.ObjectId; import org.mousephenotype.api.models.Gene; -import org.mousephenotype.api.models.GeneBundle; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.web.PageableDefault; -import org.springframework.data.web.SortDefault; -import java.util.ArrayList; import java.util.List; public interface GeneRepository extends PagingAndSortingRepository { diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties index 815624a..e4d0c37 100644 --- a/src/main/resources/application-docker.properties +++ b/src/main/resources/application-docker.properties @@ -1,5 +1,5 @@ -spring.data.mongodb.uri=mongodb://${IMPC_API_DB_USER}:${IMPC_API_DB_PASSWORD}@${IMPC_API_DB_HOST}/${IMPC_API_DB}?authSource=${IMPC_API_AUTH_DB}&replicaSet=${IMPC_API_REPLICA_SET} -spring.data.mongodb.database=${IMPC_API_DB} +spring.mongodb.uri=mongodb://${IMPC_API_DB_USER}:${IMPC_API_DB_PASSWORD}@${IMPC_API_DB_HOST}/${IMPC_API_DB}?authSource=${IMPC_API_AUTH_DB}&replicaSet=${IMPC_API_REPLICA_SET} +spring.mongodb.database=${IMPC_API_DB} spring.data.mongodb.field-naming-strategy=org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy server.compression.enabled=true server.compression.min-response-size=2048 diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 9a49420..ce83a7b 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -1,5 +1,5 @@ -spring.data.mongodb.uri=${IMPC_MONGO_URI} -spring.data.mongodb.database=${IMPC_MONGO_DB} +spring.mongodb.uri=${IMPC_MONGO_URI} +spring.mongodb.database=${IMPC_MONGO_DB} spring.data.mongodb.field-naming-strategy=org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG server.compression.enabled=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6431260..5ac979b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ -spring.data.mongodb.uri=mongodb://user:password@localhost:27017 -spring.data.mongodb.database=impc +spring.mongodb.uri=mongodb://user:password@localhost:27017 +spring.mongodb.database=impc spring.data.mongodb.field-naming-strategy=org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG server.compression.enabled=true diff --git a/src/test/java/org/mousephenotype/api/ImpcMousephenotypeApiApplicationTests.java b/src/test/java/org/mousephenotype/api/ImpcMousephenotypeApiApplicationTests.java index 541e2fb..7b8d45f 100644 --- a/src/test/java/org/mousephenotype/api/ImpcMousephenotypeApiApplicationTests.java +++ b/src/test/java/org/mousephenotype/api/ImpcMousephenotypeApiApplicationTests.java @@ -1,13 +1,83 @@ package org.mousephenotype.api; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mousephenotype.api.repositories.GeneBundleRepository; +import org.mousephenotype.api.repositories.GeneRepository; +import org.mousephenotype.api.repositories.ObservationRepository; +import org.mousephenotype.api.repositories.StatisticalResultRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.hateoas.MediaTypes; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; -@SpringBootTest +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(properties = { + "logging.level.org.mongodb.driver=OFF", + "spring.mongodb.uri=mongodb://localhost:27017/impc-test?serverSelectionTimeoutMS=10" +}) class ImpcMousephenotypeApiApplicationTests { + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private WebApplicationContext webApplicationContext; + + private MockMvc mockMvc; + + @BeforeEach + void setUpMockMvc() { + mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + } + @Test void contextLoads() { + assertThat(applicationContext).isNotNull(); + assertThat(applicationContext.getBean(GeneRepository.class)).isNotNull(); + assertThat(applicationContext.getBean(GeneBundleRepository.class)).isNotNull(); + assertThat(applicationContext.getBean(ObservationRepository.class)).isNotNull(); + assertThat(applicationContext.getBean(StatisticalResultRepository.class)).isNotNull(); + } + + @Test + void repositoryRestRootExposesMongoRepositories() throws Exception { + mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.genes.href").exists()) + .andExpect(jsonPath("$._links.geneBundles.href").exists()) + .andExpect(jsonPath("$._links.observations.href").exists()) + .andExpect(jsonPath("$._links.statisticalResults.href").exists()) + .andExpect(jsonPath("$._links.profile.href").exists()); + } + + @Test + void geneSearchResourceExposesCustomQueryMethods() throws Exception { + mockMvc.perform(get("/genes/search").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.significantPhenotypesByGene.href").exists()) + .andExpect(jsonPath("$._links.getAll.href").exists()) + .andExpect(jsonPath("$._links.findAllBySignificantMpTermIdsContains.href").exists()) + .andExpect(jsonPath("$._links.findAllBySignificantTopLevelMpTermIdsContains.href").exists()) + .andExpect(jsonPath("$._links.findAllByTestedParameter.href").exists()) + .andExpect(jsonPath("$._links.findAllByTestedProcedure.href").exists()) + .andExpect(jsonPath("$._links.findAllByTestedParameterId.href").exists()) + .andExpect(jsonPath("$._links.findAllByTestedProcedureId.href").exists()) + .andExpect(jsonPath("$._links.findAllByTestedPipelineId.href").exists()); + } + + @Test + void profileEndpointExposesGeneMetadata() throws Exception { + mockMvc.perform(get("/profile/genes").accept(MediaTypes.ALPS_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.alps.descriptor").isArray()); } } diff --git a/src/test/java/org/mousephenotype/api/configuration/GeneConfigurationTest.java b/src/test/java/org/mousephenotype/api/configuration/GeneConfigurationTest.java new file mode 100644 index 0000000..c8663e6 --- /dev/null +++ b/src/test/java/org/mousephenotype/api/configuration/GeneConfigurationTest.java @@ -0,0 +1,73 @@ +package org.mousephenotype.api.configuration; + +import org.junit.jupiter.api.Test; +import org.mousephenotype.api.models.Gene; +import org.mousephenotype.api.models.GeneBundle; +import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; +import org.springframework.data.rest.webmvc.support.PagingAndSortingTemplateVariables; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.objenesis.SpringObjenesis; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +class GeneConfigurationTest { + + private final RecordingRepositoryEntityLinks entityLinks = new SpringObjenesis() + .newInstance(RecordingRepositoryEntityLinks.class); + private final GeneConfiguration configuration = new GeneConfiguration(); + + @Test + void geneProcessorAddsGeneBundleLinkUsingGeneId() { + ReflectionTestUtils.setField(configuration, "entityLinks", entityLinks); + Gene gene = Gene.builder() + .id("MGI:12345") + .mgiAccessionId("MGI:12345") + .build(); + Link bundleLink = Link.of("https://example.org/geneBundles/MGI:12345", "geneBundle"); + entityLinks.linkToReturn = bundleLink; + + EntityModel model = EntityModel.of(gene); + EntityModel processed = configuration.geneProcessor().process(model); + + assertThat(processed).isSameAs(model); + assertThat(processed.getLinks()).contains(bundleLink); + assertThat(entityLinks.domainType).isEqualTo(GeneBundle.class); + assertThat(entityLinks.id).isEqualTo("MGI:12345"); + assertThat(entityLinks.calls).isOne(); + } + + @Test + void geneProcessorRejectsEntityModelWithoutContent() { + ReflectionTestUtils.setField(configuration, "entityLinks", entityLinks); + EntityModel model = new EmptyGeneEntityModel(); + + assertThatNullPointerException() + .isThrownBy(() -> configuration.geneProcessor().process(model)); + assertThat(entityLinks.calls).isZero(); + } + + private static class EmptyGeneEntityModel extends EntityModel { + } + + private static class RecordingRepositoryEntityLinks extends RepositoryEntityLinks { + private Class domainType; + private Object id; + private Link linkToReturn; + private int calls; + + private RecordingRepositoryEntityLinks() { + super(null, null, null, (PagingAndSortingTemplateVariables) null, null); + } + + @Override + public Link linkToItemResource(Class type, Object id) { + this.domainType = type; + this.id = id; + this.calls++; + return linkToReturn; + } + } +} diff --git a/src/test/java/org/mousephenotype/api/models/ModelContractTest.java b/src/test/java/org/mousephenotype/api/models/ModelContractTest.java new file mode 100644 index 0000000..e7e9780 --- /dev/null +++ b/src/test/java/org/mousephenotype/api/models/ModelContractTest.java @@ -0,0 +1,159 @@ +package org.mousephenotype.api.models; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class ModelContractTest { + + private static final List> MODEL_CLASSES = Arrays.asList( + Gene.class, + GeneBundle.class, + GeneImage.class, + GeneParameter.class, + GenePhenotypeAssociation.class, + GeneProduct.class, + MpTerm.class, + Observation.class, + StatisticalResult.class + ); + + static Stream> modelClasses() { + return MODEL_CLASSES.stream(); + } + + @ParameterizedTest(name = "{0} setters and getters round-trip") + @MethodSource("modelClasses") + void settersAndGettersRoundTripEveryField(Class modelClass) throws Exception { + Object model = modelClass.getDeclaredConstructor().newInstance(); + + for (Field field : instanceFields(modelClass)) { + Object expected = sampleValue(field, 1); + + setter(modelClass, field).invoke(model, expected); + + assertThat(readProperty(model, field)) + .as("%s.%s", modelClass.getSimpleName(), field.getName()) + .isEqualTo(expected); + } + } + + @ParameterizedTest(name = "{0} all-args constructor assigns fields") + @MethodSource("modelClasses") + void allArgsConstructorAssignsEveryField(Class modelClass) throws Exception { + List fields = instanceFields(modelClass); + Class[] parameterTypes = fields.stream() + .map(Field::getType) + .toArray(Class[]::new); + Object[] args = fields.stream() + .map(field -> sampleValue(field, 2)) + .toArray(Object[]::new); + + Constructor constructor = modelClass.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + Object model = constructor.newInstance(args); + + for (int i = 0; i < fields.size(); i++) { + assertThat(readProperty(model, fields.get(i))) + .as("%s.%s", modelClass.getSimpleName(), fields.get(i).getName()) + .isEqualTo(args[i]); + } + } + + @ParameterizedTest(name = "{0} builder assigns fields") + @MethodSource("modelClasses") + void builderAssignsEveryField(Class modelClass) throws Exception { + Object model = buildModel(modelClass, 3); + + for (Field field : instanceFields(modelClass)) { + assertThat(readProperty(model, field)) + .as("%s.%s", modelClass.getSimpleName(), field.getName()) + .isEqualTo(sampleValue(field, 3)); + } + } + + @ParameterizedTest(name = "{0} equality hashCode and toString") + @MethodSource("modelClasses") + void equalityHashCodeAndToStringUseFieldValues(Class modelClass) throws Exception { + Object first = buildModel(modelClass, 4); + Object second = buildModel(modelClass, 4); + Object different = buildModel(modelClass, 5); + + assertThat(first).isEqualTo(second); + assertThat(first).hasSameHashCodeAs(second); + assertThat(first).isNotEqualTo(different); + assertThat(first).isNotEqualTo(null); + assertThat(first).isNotEqualTo("not a " + modelClass.getSimpleName()); + assertThat(first.toString()).contains(modelClass.getSimpleName()); + } + + private static Object buildModel(Class modelClass, int variant) throws Exception { + Object builder = modelClass.getMethod("builder").invoke(null); + + for (Field field : instanceFields(modelClass)) { + builderSetter(builder, field).invoke(builder, sampleValue(field, variant)); + } + + return builder.getClass().getMethod("build").invoke(builder); + } + + private static List instanceFields(Class modelClass) { + Field[] fields = modelClass.getDeclaredFields(); + return Arrays.stream(fields) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .collect(java.util.stream.Collectors.toList()); + } + + private static Method setter(Class modelClass, Field field) throws NoSuchMethodException { + return modelClass.getMethod("set" + accessorSuffix(field.getName()), field.getType()); + } + + private static Method builderSetter(Object builder, Field field) throws NoSuchMethodException { + return builder.getClass().getMethod(field.getName(), field.getType()); + } + + private static Object readProperty(Object model, Field field) throws Exception { + Method getter = model.getClass().getMethod("get" + accessorSuffix(field.getName())); + return getter.invoke(model); + } + + private static String accessorSuffix(String fieldName) { + return Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); + } + + private static Object sampleValue(Field field, int variant) { + Class type = field.getType(); + String value = field.getName() + "-" + variant; + + if (String.class.equals(type)) { + return value; + } + if (Boolean.class.equals(type) || boolean.class.equals(type)) { + return variant % 2 == 0; + } + if (Integer.class.equals(type) || int.class.equals(type)) { + return variant; + } + if (Long.class.equals(type) || long.class.equals(type)) { + return (long) variant; + } + if (Double.class.equals(type) || double.class.equals(type)) { + return variant + 0.25d; + } + if (List.class.isAssignableFrom(type)) { + return Collections.singletonList(value); + } + + throw new IllegalArgumentException("No sample value for " + field); + } +} diff --git a/src/test/java/org/mousephenotype/api/repositories/CustomObservationRepositoryImplTest.java b/src/test/java/org/mousephenotype/api/repositories/CustomObservationRepositoryImplTest.java new file mode 100644 index 0000000..9849563 --- /dev/null +++ b/src/test/java/org/mousephenotype/api/repositories/CustomObservationRepositoryImplTest.java @@ -0,0 +1,15 @@ +package org.mousephenotype.api.repositories; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CustomObservationRepositoryImplTest { + + @Test + void getAllByParameterStableIdReturnsNullUntilCustomQueryIsImplemented() { + CustomObservationRepositoryImpl repository = new CustomObservationRepositoryImpl(); + + assertThat(repository.getAllByParameterStableId("IMPC_BODY_001_001")).isNull(); + } +} diff --git a/src/test/java/org/mousephenotype/api/repositories/RepositoryDefinitionsTest.java b/src/test/java/org/mousephenotype/api/repositories/RepositoryDefinitionsTest.java new file mode 100644 index 0000000..b5002ea --- /dev/null +++ b/src/test/java/org/mousephenotype/api/repositories/RepositoryDefinitionsTest.java @@ -0,0 +1,83 @@ +package org.mousephenotype.api.repositories; + +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.Query; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryDefinitionsTest { + + @Test + void geneRepositoryDeclaresExpectedCustomMongoQueries() throws Exception { + assertQuery( + GeneRepository.class.getMethod("significantPhenotypesByGene", Pageable.class), + "{}", + "{'mgiAccessionId': 1, 'significant_mp_terms.mp_term_id': 1, 'significant_mp_terms.mp_term_name': 1}" + ); + assertQuery( + GeneRepository.class.getMethod("getAll", Pageable.class), + "{}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllBySignificantMpTermIdsContains", List.class, Pageable.class), + "{'significant_mp_terms.mp_term_id': {$in: ?0}}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllBySignificantTopLevelMpTermIdsContains", List.class, Pageable.class), + "{'significant_mp_terms.top_level_ancestors.mp_term_id': {$in: ?0}}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllByTestedParameter", String.class, Pageable.class), + "{'tested_parameters.parameter_name': {'$regex' : '.*?0.*', '$options' : 'i'}}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllByTestedProcedure", String.class, Pageable.class), + "{'tested_parameters.procedure_name': {'$regex' : '.*?0.*', '$options' : 'i'}}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllByTestedParameterId", String.class, Pageable.class), + "{'tested_parameters.parameter_stable_id': '?0'}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllByTestedProcedureId", String.class, Pageable.class), + "{'tested_parameters.procedure_stable_id': '?0'}", + "" + ); + assertQuery( + GeneRepository.class.getMethod("findAllByTestedPipelineId", String.class, Pageable.class), + "{'tested_parameters.pipeline_stable_id': '?0'}", + "" + ); + } + + @Test + void repositoryInterfacesExposeExpectedFinderMethods() throws Exception { + assertThat(GeneRepository.class.getMethod("getGeneByMgiAccessionId", String.class)).isNotNull(); + assertThat(GeneRepository.class.getMethod("findAllByMgiAccessionIdIn", List.class, Pageable.class)).isNotNull(); + assertThat(GeneBundleRepository.class.getMethod("getGeneBundleByMgiAccessionId", String.class)).isNotNull(); + assertThat(GeneBundleRepository.class.getMethod("getGeneBundlesByMgiAccessionIdIn", java.util.ArrayList.class)).isNotNull(); + assertThat(ObservationRepository.class.getMethod("findAllByParameterStableId", String.class, Pageable.class)).isNotNull(); + assertThat(StatisticalResultRepository.class.getMethod("findAllByMarkerAccessionIdIn", List.class, Pageable.class)).isNotNull(); + assertThat(StatisticalResultRepository.class.getMethod("findAllByMarkerAccessionIdIsAndSignificantTrue", String.class, Pageable.class)).isNotNull(); + } + + private static void assertQuery(Method method, String expectedValue, String expectedFields) { + Query query = method.getAnnotation(Query.class); + + assertThat(query) + .as("%s should declare @Query", method.getName()) + .isNotNull(); + assertThat(query.value()).isEqualTo(expectedValue); + assertThat(query.fields()).isEqualTo(expectedFields); + } +} From b31e1d5533cc841c2679e56253af3beeb76f8054 Mon Sep 17 00:00:00 2001 From: ogunes-ebi Date: Wed, 13 May 2026 13:10:03 +0100 Subject: [PATCH 2/3] Upgrade Spring Boot to 4.0.6 and add dependency scanning in CI/CD pipeline --- .gitlab-ci.yml | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bdb1d23..b523834 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,6 +42,7 @@ variables: include: - template: Security/SAST.gitlab-ci.yml - template: Jobs/Secret-Detection.gitlab-ci.yml + - template: Jobs/Dependency-Scanning.gitlab-ci.yml sast: stage: test diff --git a/pom.xml b/pom.xml index 8dbc691..9192e51 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.5 + 4.0.6 org.mousephenotype From 646f1fa9bb287469ea1148358ff6afef194d2031 Mon Sep 17 00:00:00 2001 From: ogunes-ebi Date: Wed, 13 May 2026 14:08:54 +0100 Subject: [PATCH 3/3] Enable new analyzer enforcement in CI/CD pipeline --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b523834..02db2b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,6 +38,7 @@ variables: # see: https://gitlab.com/gitlab-com/gl-infra/production/issues/982 DOCKER_TLS_CERTDIR: "" GITLAB_ADVANCED_SAST_ENABLED: 'true' + DS_ENFORCE_NEW_ANALYZER: 'true' include: - template: Security/SAST.gitlab-ci.yml