diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4755292..02db2b1 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -37,12 +37,23 @@ 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'
+ DS_ENFORCE_NEW_ANALYZER: 'true'
+
+include:
+ - template: Security/SAST.gitlab-ci.yml
+ - template: Jobs/Secret-Detection.gitlab-ci.yml
+ - template: Jobs/Dependency-Scanning.gitlab-ci.yml
+
+sast:
+ stage: test
stages:
# - env
- build
+ - test
- dev-deploy
- prod-deploy
@@ -184,4 +195,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..9192e51 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 2.5.2
+ 4.0.6
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);
+ }
+}