package uk.ac.ox.zoo.seeg.abraid.mp.modeloutputhandler.web; import ch.lambdaj.Lambda; import ch.lambdaj.function.convert.Converter; import freemarker.template.TemplateException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.builder.CompareToBuilder; import org.joda.time.DateTime; import org.joda.time.DateTimeUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.kubek2k.springockito.annotations.ReplaceWithMock; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import uk.ac.ox.zoo.seeg.abraid.mp.common.dao.DiseaseGroupDao; import uk.ac.ox.zoo.seeg.abraid.mp.common.dao.ModelRunDao; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.*; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.csv.CsvCovariateInfluence; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.csv.CsvEffectCurveCovariateInfluence; import uk.ac.ox.zoo.seeg.abraid.mp.common.dto.csv.CsvSubmodelStatistic; import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.EmailService; import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.ValidationParameterCacheService; import uk.ac.ox.zoo.seeg.abraid.mp.modeloutputhandler.geoserver.GeoserverRestService; import uk.ac.ox.zoo.seeg.abraid.mp.testutils.AbstractSpringIntegrationTests; import uk.ac.ox.zoo.seeg.abraid.mp.testutils.GeneralTestUtils; import uk.ac.ox.zoo.seeg.abraid.mp.testutils.SpringockitoWebContextLoader; import java.io.File; import java.io.IOException; import java.nio.file.Paths; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.extractProperty; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Contains integration tests for the MainController class. * * Copyright (c) 2014 University of Oxford */ @ContextConfiguration(loader = SpringockitoWebContextLoader.class, locations = { "file:ModelOutputHandler/web/WEB-INF/abraid-servlet-beans.xml", "file:ModelOutputHandler/web/WEB-INF/applicationContext.xml" }) @WebAppConfiguration("file:ModelOutputHandler/web") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) public class MainControllerIntegrationTest extends AbstractSpringIntegrationTests { private static final String TEST_MODEL_RUN_SERVER = "host"; @Rule public TemporaryFolder testFolder = new TemporaryFolder(); ///CHECKSTYLE:SUPPRESS VisibilityModifier private static final String OUTPUT_HANDLER_PATH = "/handleoutputs"; private static final String TEST_DATA_PATH = "ModelOutputHandler/test/uk/ac/ox/zoo/seeg/abraid/mp/modeloutputhandler/web/testdata"; private static final String TEST_MODEL_RUN_NAME = "deng_2014-05-16-13-28-57_482ae3ca-ab30-414d-acce-388baae7d83c"; private static final int TEST_MODEL_RUN_DISEASE_GROUP_ID = 87; private MockMvc mockMvc; @ReplaceWithMock @Autowired private File rasterFileDirectory; @ReplaceWithMock @Autowired private WaterBodiesMaskRasterFileLocator waterBodiesMaskRasterFileLocator; @ReplaceWithMock @Autowired private GeoserverRestService geoserverRestService; @ReplaceWithMock @Autowired private EmailService emailService; @Autowired private WebApplicationContext webApplicationContext; @Autowired private MainController controller; @Autowired private ModelRunDao modelRunDao; @ReplaceWithMock @Autowired private ValidationParameterCacheService cacheService; @Autowired private DiseaseGroupDao diseaseGroupDao; @Before public void setup() { when(rasterFileDirectory.getAbsolutePath()).thenReturn(testFolder.getRoot().getAbsolutePath()); when(waterBodiesMaskRasterFileLocator.getFile()).thenReturn(new File(TEST_DATA_PATH, "waterbodies.tif")); // Set up Spring test in standalone mode this.mockMvc = MockMvcBuilders .webAppContextSetup(webApplicationContext) .build(); } @Test public void handleModelOutputsRejectsNonPOSTRequests() throws Exception { this.mockMvc.perform(get(OUTPUT_HANDLER_PATH)).andExpect(status().isMethodNotAllowed()); this.mockMvc.perform(put(OUTPUT_HANDLER_PATH)).andExpect(status().isMethodNotAllowed()); this.mockMvc.perform(delete(OUTPUT_HANDLER_PATH)).andExpect(status().isMethodNotAllowed()); this.mockMvc.perform(patch(OUTPUT_HANDLER_PATH)).andExpect(status().isMethodNotAllowed()); } @Test public void handleModelOutputsStoresValidCompletedOutputs() throws Exception { // Arrange DateTime expectedResponseDate = DateTime.now(); DateTimeUtils.setCurrentMillisFixed(expectedResponseDate.getMillis()); insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("valid_completed_outputs.zip"); String expectedOutputText = "test output text"; // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isOk()) .andExpect(content().string("")); // Assert ModelRun run = modelRunDao.getByName(TEST_MODEL_RUN_NAME); assertThat(run.getStatus()).isEqualTo(ModelRunStatus.COMPLETED); assertThat(run.getResponseDate()).isEqualTo(expectedResponseDate); assertThat(run.getOutputText()).isEqualTo(expectedOutputText); assertThat(run.getErrorText()).isNullOrEmpty(); assertThatStatisticsInDatabaseMatchesFile(run, "statistics.csv"); assertThatRelativeInfluencesInDatabaseMatchesFile(run, "relative_influence.csv"); assertThatEffectCurvesInDatabaseMatchesFile(run, "effect_curves.csv"); assertThatRasterWrittenToFile(run, "mean_prediction_full.tif", "mean_full"); assertThatRasterWrittenToFile(run, "mean_prediction.tif", "mean"); verifyEnvironmentalSuitabilityCacheReset(run); assertThatRasterPublishedToGeoserver(run, "mean"); assertThatRasterWrittenToFile(run, "prediction_uncertainty_full.tif", "uncertainty_full"); assertThatRasterWrittenToFile(run, "prediction_uncertainty.tif", "uncertainty"); assertThatRasterPublishedToGeoserver(run, "uncertainty"); assertThatRasterWrittenToFile(run, "extent.tif", "extent"); } @Test public void handleModelOutputsStoresValidFailedOutputsWithResults() throws Exception { // Arrange DateTime expectedResponseDate = DateTime.now(); DateTimeUtils.setCurrentMillisFixed(expectedResponseDate.getMillis()); insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("valid_failed_outputs_with_results.zip"); String expectedErrorText = "test error text"; // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isOk()) .andExpect(content().string("")); // Assert ModelRun run = modelRunDao.getByName(TEST_MODEL_RUN_NAME); assertThat(run.getStatus()).isEqualTo(ModelRunStatus.FAILED); assertThat(run.getResponseDate()).isEqualTo(expectedResponseDate); assertThat(run.getOutputText()).isNullOrEmpty(); assertThat(run.getErrorText()).isEqualTo(expectedErrorText); assertThatStatisticsInDatabaseMatchesFile(run, "statistics.csv"); assertThatRelativeInfluencesInDatabaseMatchesFile(run, "relative_influence.csv"); assertThatEffectCurvesInDatabaseMatchesFile(run, "effect_curves.csv"); assertThatRasterFileDoesNotExist(run, "mean_full"); assertThatRasterWrittenToFile(run, "mean_prediction.tif", "mean"); verifyEnvironmentalSuitabilityCacheReset(run); assertThatRasterFileDoesNotExist(run, "uncertainty_full"); assertThatRasterWrittenToFile(run, "prediction_uncertainty.tif", "uncertainty"); assertThatRasterWrittenToFile(run, "extent.tif", "extent"); assertThatNoRastersPublishedToGeoserver(); assertThatFailureEmailSent("", expectedErrorText); } @Test public void handleModelOutputsStoresValidFailedOutputsWithoutResults() throws Exception { // Arrange DateTime expectedResponseDate = DateTime.now(); DateTimeUtils.setCurrentMillisFixed(expectedResponseDate.getMillis()); insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("valid_failed_outputs_without_results.zip"); String expectedErrorText = "test error text"; // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isOk()) .andExpect(content().string("")); // Assert ModelRun run = modelRunDao.getByName(TEST_MODEL_RUN_NAME); assertThat(run.getStatus()).isEqualTo(ModelRunStatus.FAILED); assertThat(run.getResponseDate()).isEqualTo(expectedResponseDate); assertThat(run.getOutputText()).isNullOrEmpty(); assertThat(run.getErrorText()).isEqualTo(expectedErrorText); assertThatNoRastersPublishedToGeoserver(); assertThatFailureEmailSent("", expectedErrorText); } @Test public void handleModelOutputsRejectsMalformedZipFile() throws Exception { // Arrange byte[] malformedZipFile = "This is not a zip file".getBytes(); // Act and assert this.mockMvc .perform(buildPost(malformedZipFile)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"Probably not a zip file or a corrupted zip file\". See ModelOutputHandler server logs for more details.")); } private MockHttpServletRequestBuilder buildPost(byte[] data) { MockMultipartFile file = new MockMultipartFile("file", data); return fileUpload(OUTPUT_HANDLER_PATH).file(file); } @Test public void handleModelOutputsRejectsMissingMetadata() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_metadata.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File metadata.json missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsIncorrectModelRunName() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("incorrect_model_run_name.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"Model run with name deng_2014-05-13-11-26-37_0469aac2-d9b2-4104-907e-2886eff11682 does not exist\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingMeanPredictionRasterIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_mean_prediction.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File mean_prediction.tif missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingPredictionUncertaintyRasterIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_prediction_uncertainty.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File prediction_uncertainty.tif missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingExtentInputRasterIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_extent_input.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File extent.tif missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingStatisticsIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_statistics.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File statistics.csv missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingEffectCurvesIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_effect_curves.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File effect_curves.csv missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } @Test public void handleModelOutputsRejectsMissingRelativeInfluencesIfStatusIsCompleted() throws Exception { // Arrange insertModelRun(TEST_MODEL_RUN_NAME); byte[] body = loadTestFile("missing_influence.zip"); // Act and assert this.mockMvc .perform(buildPost(body)) .andExpect(status().isInternalServerError()) .andExpect(content().string("Model outputs handler failed with error \"File relative_influence.csv missing from model run outputs\". See ModelOutputHandler server logs for more details.")); } private void insertModelRun(String name) { ModelRun modelRun = new ModelRun(name, diseaseGroupDao.getById(TEST_MODEL_RUN_DISEASE_GROUP_ID), TEST_MODEL_RUN_SERVER, DateTime.now(), DateTime.now(), DateTime.now()); modelRunDao.save(modelRun); } private byte[] loadTestFile(String fileName) throws IOException { return FileUtils.readFileToByteArray(new File(TEST_DATA_PATH, fileName)); } private void assertThatRasterWrittenToFile(ModelRun run, String expectedFileName, String type) throws IOException { File expectedFile = Paths.get(TEST_DATA_PATH, expectedFileName).toFile(); File actualFile = Paths.get(testFolder.getRoot().getAbsolutePath(), run.getName() + "_" + type + ".tif").toFile(); assertThat(actualFile).hasContentEqualTo(expectedFile); } private void verifyEnvironmentalSuitabilityCacheReset(ModelRun run) { verify(cacheService).clearEnvironmentalSuitabilityCacheForDisease(run.getDiseaseGroupId()); } private void assertThatRasterFileDoesNotExist(ModelRun run, String type) throws IOException { File file = Paths.get(testFolder.getRoot().getAbsolutePath(), run.getName() + "_" + type + ".tif").toFile(); assertThat(file).doesNotExist(); } private void assertThatRasterPublishedToGeoserver(ModelRun run, String type) throws IOException, TemplateException { verify(geoserverRestService).publishGeoTIFF(Paths.get(testFolder.getRoot().getAbsolutePath(), run.getName() + "_" + type + ".tif").toFile()); } private void assertThatNoRastersPublishedToGeoserver() throws IOException, TemplateException { verify(geoserverRestService, times(0)).publishGeoTIFF(any(File.class)); } private void assertThatStatisticsInDatabaseMatchesFile(final ModelRun run, String path) throws IOException { List<SubmodelStatistic> database = run.getSubmodelStatistics(); List<CsvSubmodelStatistic> file = CsvSubmodelStatistic.readFromCSV(FileUtils.readFileToString(new File(TEST_DATA_PATH, path))); assertThat(extractProperty("kappa").from(database)).isEqualTo(extractProperty("kappa").from(file)); assertThat(extractProperty("areaUnderCurve").from(database)).isEqualTo(extractProperty("areaUnderCurve").from(file)); assertThat(extractProperty("sensitivity").from(database)).isEqualTo(extractProperty("sensitivity").from(file)); assertThat(extractProperty("specificity").from(database)).isEqualTo(extractProperty("specificity").from(file)); assertThat(extractProperty("proportionCorrectlyClassified").from(database)).isEqualTo(extractProperty("proportionCorrectlyClassified").from(file)); assertThat(extractProperty("kappaStandardDeviation").from(database)).isEqualTo(extractProperty("kappaStandardDeviation").from(file)); assertThat(extractProperty("areaUnderCurveStandardDeviation").from(database)).isEqualTo(extractProperty("areaUnderCurveStandardDeviation").from(file)); assertThat(extractProperty("sensitivityStandardDeviation").from(database)).isEqualTo(extractProperty("sensitivityStandardDeviation").from(file)); assertThat(extractProperty("specificityStandardDeviation").from(database)).isEqualTo(extractProperty("specificityStandardDeviation").from(file)); assertThat(extractProperty("proportionCorrectlyClassifiedStandardDeviation").from(database)).isEqualTo(extractProperty("proportionCorrectlyClassifiedStandardDeviation").from(file)); } private void assertThatRelativeInfluencesInDatabaseMatchesFile(final ModelRun run, String path) throws IOException { List<CovariateInfluence> database = run.getCovariateInfluences(); List<CsvCovariateInfluence> file = CsvCovariateInfluence.readFromCSV(FileUtils.readFileToString(new File(TEST_DATA_PATH, path))); Collections.sort(database, new Comparator<CovariateInfluence>() { @Override public int compare(CovariateInfluence o1, CovariateInfluence o2) { return o1.getMeanInfluence().compareTo(o2.getMeanInfluence()); } }); Collections.sort(file, new Comparator<CsvCovariateInfluence>() { @Override public int compare(CsvCovariateInfluence o1, CsvCovariateInfluence o2) { return o1.getMeanInfluence().compareTo(o2.getMeanInfluence()); } }); assertThat(prepend("id", extractProperty("id").from(extractProperty("covariateFile").from(database)))).isEqualTo(extractProperty("name").from(file)); assertThat(extractProperty("meanInfluence").from(database)).isEqualTo(extractProperty("meanInfluence").from(file)); assertThat(extractProperty("upperQuantile").from(database)).isEqualTo(extractProperty("upperQuantile").from(file)); assertThat(extractProperty("lowerQuantile").from(database)).isEqualTo(extractProperty("lowerQuantile").from(file)); } private List<String> prepend(final String prefix, List<Object> from) { return Lambda.convert(from, new Converter<Object, String>() { @Override public String convert(Object s) { return prefix + (s.toString()); } }); } private void assertThatEffectCurvesInDatabaseMatchesFile(final ModelRun run, String path) throws IOException { List<EffectCurveCovariateInfluence> database = run.getEffectCurveCovariateInfluences(); List<CsvEffectCurveCovariateInfluence> file = CsvEffectCurveCovariateInfluence.readFromCSV(FileUtils.readFileToString(new File(TEST_DATA_PATH, path))); // Discrete covariates only store min and max effect curve entries, this means that row 5 of the csv should not be kept file.remove(4); Collections.sort(database, new Comparator<EffectCurveCovariateInfluence>() { @Override public int compare(EffectCurveCovariateInfluence o1, EffectCurveCovariateInfluence o2) { return new CompareToBuilder() .append(o1.getCovariateFile().getId(), o2.getCovariateFile().getId()) .append(o1.getCovariateValue(), o2.getCovariateValue()) .toComparison(); } }); Collections.sort(file, new Comparator<CsvEffectCurveCovariateInfluence>() { @Override public int compare(CsvEffectCurveCovariateInfluence o1, CsvEffectCurveCovariateInfluence o2) { return new CompareToBuilder() .append(o1.getName(), o2.getName()) .append(o1.getCovariateValue(), o2.getCovariateValue()) .toComparison(); } }); assertThat(prepend("id", extractProperty("id").from(extractProperty("covariateFile").from(database)))).isEqualTo(extractProperty("name").from(file)); assertThat(extractProperty("covariateValue").from(database)).isEqualTo(extractProperty("covariateValue").from(file)); assertThat(extractProperty("meanInfluence").from(database)).isEqualTo(extractProperty("meanInfluence").from(file)); assertThat(extractProperty("upperQuantile").from(database)).isEqualTo(extractProperty("upperQuantile").from(file)); assertThat(extractProperty("lowerQuantile").from(database)).isEqualTo(extractProperty("lowerQuantile").from(file)); } private void assertThatFailureEmailSent(String outputText, String errorText) { ArgumentCaptor<Map<String, String>> captor = GeneralTestUtils.captorForMapClass(); verify(emailService).sendEmailInBackground( eq("Failed Model Run"), eq("modelFailureEmail.ftl"), captor.capture()); Map<String, String> actual = captor.getValue(); assertThat(actual.get("name")).isEqualTo(TEST_MODEL_RUN_NAME); assertThat(actual.get("server")).isEqualTo(TEST_MODEL_RUN_SERVER); assertThat(actual.get("output")).isEqualTo(outputText); assertThat(actual.get("error")).isEqualTo(errorText); } }