diff --git a/src/main/java/cz/cvut/kbss/study/model/Institution.java b/src/main/java/cz/cvut/kbss/study/model/Institution.java index 8a1ada98..580a46bd 100644 --- a/src/main/java/cz/cvut/kbss/study/model/Institution.java +++ b/src/main/java/cz/cvut/kbss/study/model/Institution.java @@ -88,8 +88,8 @@ public void setReferenceId(Integer referenceId) { @Override public String toString() { - return "Institution{" + - "name='" + name + '\'' + - "} " + super.toString(); + return "Institution{<" + uri + + ">, name='" + name + '\'' + + "}"; } } diff --git a/src/main/java/cz/cvut/kbss/study/model/PatientRecord.java b/src/main/java/cz/cvut/kbss/study/model/PatientRecord.java index f64db877..2adf4ec6 100644 --- a/src/main/java/cz/cvut/kbss/study/model/PatientRecord.java +++ b/src/main/java/cz/cvut/kbss/study/model/PatientRecord.java @@ -10,13 +10,14 @@ import cz.cvut.kbss.jopa.model.annotations.ParticipationConstraints; import cz.cvut.kbss.study.model.qam.Question; import cz.cvut.kbss.study.model.util.HasOwlKey; +import cz.cvut.kbss.study.model.util.HasUri; import java.io.Serializable; import java.net.URI; import java.util.Date; @OWLClass(iri = Vocabulary.s_c_patient_record) -public class PatientRecord implements Serializable, HasOwlKey { +public class PatientRecord implements Serializable, HasOwlKey, HasUri { @Id private URI uri; @@ -49,9 +50,10 @@ public class PatientRecord implements Serializable, HasOwlKey { private String formTemplate; @OWLObjectProperty(iri = Vocabulary.s_p_has_question, cascade = {CascadeType.MERGE, - CascadeType.REMOVE}, fetch = FetchType.EAGER) + CascadeType.REMOVE}, fetch = FetchType.EAGER) private Question question; + @Override public URI getUri() { return uri; } @@ -136,10 +138,10 @@ public void setFormTemplate(String formTemplate) { @Override public String toString() { - return "PatientRecord{" + - "localName=" + localName + + return "PatientRecord{<" + uri + + ">, localName=" + localName + ", dateCreated=" + dateCreated + ", institution=" + institution + - "} " + super.toString(); + "}"; } } diff --git a/src/main/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDao.java b/src/main/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDao.java index 186b39cf..27e925e0 100644 --- a/src/main/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDao.java +++ b/src/main/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDao.java @@ -19,6 +19,9 @@ import java.math.BigInteger; import java.net.URI; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.List; import java.util.Objects; @@ -164,4 +167,71 @@ public void requireUniqueNonEmptyLocalName(PatientRecord entity) { } em.clear(); } + + /** + * Retrieves records modified (created or modified) in the specified time interval. + *

+ * Since the record modification is tracked by a timestamp and the arguments here are dates, this method uses + * beginning of the min date and end of the max date. + * + * @param minDate Minimum date of modification of matching records, inclusive + * @param maxDate Maximum date of modification of matching records, inclusive + * @return List of matching records + */ + public List findAllFull(LocalDate minDate, LocalDate maxDate) { + Objects.requireNonNull(minDate); + Objects.requireNonNull(maxDate); + + final Instant min = minDate.atStartOfDay(ZoneOffset.UTC).toInstant(); + final Instant max = maxDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant(); + + return em.createNativeQuery("SELECT ?r WHERE {" + + "?r a ?type ;" + + "?hasCreatedDate ?created ." + + "OPTIONAL { ?r ?hasLastModified ?lastModified . }" + + "BIND (IF (BOUND(?lastModified), ?lastModified, ?created) AS ?edited)" + + "FILTER (?edited >= ?minDate && ?edited < ?maxDate)" + + "} ORDER BY DESC(?edited)", PatientRecord.class) + .setParameter("type", typeUri) + .setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created)) + .setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified)) + .setParameter("minDate", min) + .setParameter("maxDate", max).getResultList(); + } + + /** + * Retrieves records modified (created or modified) in the specified time interval. + *

+ * Since the record modification is tracked by a timestamp and the arguments here are dates, this method uses + * beginning of the min date and end of the max date. + * + * @param institution Institution with which matching records have to be associated + * @param minDate Minimum date of modification of matching records, inclusive + * @param maxDate Maximum date of modification of matching records, inclusive + * @return List of matching records + */ + public List findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate) { + Objects.requireNonNull(institution); + Objects.requireNonNull(minDate); + Objects.requireNonNull(maxDate); + + final Instant min = minDate.atStartOfDay(ZoneOffset.UTC).toInstant(); + final Instant max = maxDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant(); + + return em.createNativeQuery("SELECT ?r WHERE {" + + "?r a ?type ; " + + "?hasCreatedDate ?created ; " + + "?hasInstitution ?institution . " + + "OPTIONAL { ?r ?hasLastModified ?lastModified . } " + + "BIND (IF (BOUND(?lastModified), ?lastModified, ?created) AS ?edited) " + + "FILTER (?edited >= ?minDate && ?edited < ?maxDate)" + + "} ORDER BY DESC(?edited)", PatientRecord.class) + .setParameter("type", typeUri) + .setParameter("hasInstitution", URI.create(Vocabulary.s_p_was_treated_at)) + .setParameter("institution", institution) + .setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created)) + .setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified)) + .setParameter("minDate", min) + .setParameter("maxDate", max).getResultList(); + } } diff --git a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java index 77fd3646..1dc87cfe 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java @@ -9,15 +9,25 @@ import cz.cvut.kbss.study.security.SecurityConstants; import cz.cvut.kbss.study.service.InstitutionService; import cz.cvut.kbss.study.service.PatientRecordService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDate; import java.util.List; +import java.util.Optional; @RestController @PreAuthorize("hasRole('" + SecurityConstants.ROLE_USER + "')") @@ -35,16 +45,33 @@ public PatientRecordController(PatientRecordService recordService, InstitutionSe @PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isMemberOfInstitution(#institutionKey)") @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public List getRecords(@RequestParam(value = "institution", required = false) String institutionKey) { - return institutionKey != null ? findByInstitution(institutionKey) : recordService.findAllRecords(); + public List getRecords( + @RequestParam(value = "institution", required = false) String institutionKey) { + return institutionKey != null ? recordService.findByInstitution(getInstitution(institutionKey)) : + recordService.findAllRecords(); } - private List findByInstitution(String institutionKey) { + private Institution getInstitution(String institutionKey) { final Institution institution = institutionService.findByKey(institutionKey); if (institution == null) { throw NotFoundException.create("Institution", institutionKey); } - return recordService.findByInstitution(institution); + return institution; + } + + @PreAuthorize( + "hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isMemberOfInstitution(#institutionKey)") + @GetMapping(value = "/export", produces = MediaType.APPLICATION_JSON_VALUE) + public List exportRecords( + @RequestParam(value = "institution", required = false) String institutionKey, + @RequestParam(name = "minDate", required = false) Optional minDateParam, + @RequestParam(name = "maxDate", required = false) Optional maxDateParam) { + final LocalDate minDate = minDateParam.orElse(LocalDate.EPOCH); + final LocalDate maxDate = maxDateParam.orElse(LocalDate.now()); + if (institutionKey != null) { + return recordService.findAllFull(getInstitution(institutionKey), minDate, maxDate); + } + return recordService.findAllFull(minDate, maxDate); } @PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isRecordInUsersInstitution(#key)") diff --git a/src/main/java/cz/cvut/kbss/study/rest/util/RestUtils.java b/src/main/java/cz/cvut/kbss/study/rest/util/RestUtils.java index 078021ea..0ebc2892 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/util/RestUtils.java +++ b/src/main/java/cz/cvut/kbss/study/rest/util/RestUtils.java @@ -1,16 +1,17 @@ package cz.cvut.kbss.study.rest.util; -import cz.cvut.kbss.study.exception.WebServiceIntegrationException; -import cz.cvut.kbss.study.util.Constants; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; public class RestUtils { @@ -79,4 +80,22 @@ public static String getCookie(HttpServletRequest request, String cookieName) { } return null; } + + /** + * Parses the specified date string. + *

+ * The parameter is expected to be in the ISO format. + * + * @param dateStr Date string + * @return {@code LocalDate} object corresponding to the specified date string + * @throws ResponseStatusException Bad request is thrown if the date string is not parseable + */ + public static LocalDate parseDate(String dateStr) { + try { + return LocalDate.parse(dateStr); + } catch (DateTimeParseException | NullPointerException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Value '" + dateStr + "' is not a valid date in ISO format."); + } + } + } diff --git a/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java b/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java index 5f6920ca..5bf775a5 100644 --- a/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java +++ b/src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java @@ -5,6 +5,7 @@ import cz.cvut.kbss.study.model.PatientRecord; import cz.cvut.kbss.study.model.User; +import java.time.LocalDate; import java.util.List; public interface PatientRecordService extends BaseService { @@ -39,4 +40,31 @@ public interface PatientRecordService extends BaseService { * @return Records of matching patients */ List findAllRecords(); + + /** + * Finds all records that were created or modified in the specified date interval. + *

+ * In contrast to {@link #findAll()}, this method returns full records, not DTOs. + * + * @param minDate Minimum date of modification of returned records, inclusive + * @param maxDate Maximum date of modification of returned records, inclusive + * @return List of matching records + * @see #findAllFull(Institution, LocalDate, LocalDate) + * @see #findAllRecords() + */ + List findAllFull(LocalDate minDate, LocalDate maxDate); + + /** + * Finds all records that were created or modified at the specified institution in the specified date interval. + *

+ * In contrast to {@link #findByInstitution(Institution)}, this method returns full records, not DTOs. + * + * @param institution Institution with which the records are associated + * @param minDate Minimum date of modification of returned records, inclusive + * @param maxDate Maximum date of modification of returned records, inclusive + * @return List of matching records + * @see #findAllFull(LocalDate, LocalDate) + * @see #findByInstitution(Institution) + */ + List findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate); } diff --git a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryPatientRecordService.java b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryPatientRecordService.java index dd45e36c..87e1c2a7 100644 --- a/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryPatientRecordService.java +++ b/src/main/java/cz/cvut/kbss/study/service/repository/RepositoryPatientRecordService.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.util.Date; import java.util.List; @@ -52,6 +53,18 @@ public List findAllRecords() { return recordDao.findAllRecords(); } + @Transactional(readOnly = true) + @Override + public List findAllFull(LocalDate minDate, LocalDate maxDate) { + return recordDao.findAllFull(minDate, maxDate); + } + + @Transactional(readOnly = true) + @Override + public List findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate) { + return recordDao.findAllFull(institution, minDate, maxDate); + } + @Override protected void prePersist(PatientRecord instance) { final User author = securityUtils.getCurrentUser(); diff --git a/src/test/java/cz/cvut/kbss/study/environment/util/ContainsSameEntities.java b/src/test/java/cz/cvut/kbss/study/environment/util/ContainsSameEntities.java new file mode 100644 index 00000000..1b25d4a1 --- /dev/null +++ b/src/test/java/cz/cvut/kbss/study/environment/util/ContainsSameEntities.java @@ -0,0 +1,61 @@ +/* + * TermIt + * Copyright (C) 2023 Czech Technical University in Prague + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cz.cvut.kbss.study.environment.util; + +import cz.cvut.kbss.study.model.util.HasUri; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import java.util.Collection; +import java.util.Objects; + +/** + * Checks whether the provided collection contains the same entities as the expected one. + *

+ * The membership check is done based on entity URIs. + */ +public class ContainsSameEntities extends TypeSafeMatcher> { + + private final Collection expected; + + public ContainsSameEntities(Collection expected) { + this.expected = Objects.requireNonNull(expected); + } + + @Override + protected boolean matchesSafely(Collection actual) { + if (actual == null || actual.size() != expected.size()) { + return false; + } + for (HasUri e : expected) { + if (actual.stream().noneMatch(ee -> Objects.equals(e.getUri(), ee.getUri()))) { + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendValueList("[", ", ", "]", expected); + } + + public static ContainsSameEntities containsSameEntities(Collection expected) { + return new ContainsSameEntities(expected); + } +} diff --git a/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java b/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java index e4b16fc3..52f7d604 100644 --- a/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java +++ b/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java @@ -21,6 +21,8 @@ public class Environment { private static ObjectMapper objectMapper; + public static long MILLIS_PER_DAY = 24 * 3600 * 1000L; + private Environment() { throw new AssertionError(); } diff --git a/src/test/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDaoTest.java b/src/test/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDaoTest.java index 9713bc62..817a3070 100644 --- a/src/test/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDaoTest.java +++ b/src/test/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDaoTest.java @@ -6,6 +6,7 @@ import cz.cvut.kbss.jopa.model.metamodel.EntityType; import cz.cvut.kbss.study.dto.PatientRecordDto; import cz.cvut.kbss.study.environment.generator.Generator; +import cz.cvut.kbss.study.environment.util.Environment; import cz.cvut.kbss.study.model.Institution; import cz.cvut.kbss.study.model.PatientRecord; import cz.cvut.kbss.study.model.User; @@ -16,9 +17,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.stream.IntStream; +import static cz.cvut.kbss.study.environment.util.ContainsSameEntities.containsSameEntities; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; public class PatientRecordDaoTest extends BaseDaoTestRunner { @@ -231,4 +240,63 @@ void findByKeyLoadsRecordByKey() { assertEquals(record.getUri(), result.getUri()); assertNotNull(result.getQuestion()); } + + private void persistRecordWithIdentification(PatientRecord record) { + record.setKey(IdentificationUtils.generateKey()); + record.setUri(PatientRecordDao.generateRecordUriFromKey(record.getKey())); + em.persist(record, getDescriptor(record)); + } + + @Test + void findAllFullReturnsRecordsMatchingSpecifiedDatePeriod() { + final User author = generateAuthorWithInstitution(); + final List allRecords = generateRecordsForAuthor(author); + transactional(() -> allRecords.forEach(this::persistRecordWithIdentification)); + final LocalDate minDate = LocalDate.now().minusDays(3); + final LocalDate maxDate = LocalDate.now().minusDays(1); + final List expected = allRecords.stream().filter(r -> { + final Date modified = r.getLastModified() != null ? r.getLastModified() : r.getDateCreated(); + final LocalDate modifiedDate = modified.toInstant().atZone(ZoneOffset.UTC).toLocalDate(); + return !modifiedDate.isBefore(minDate) && !modifiedDate.isAfter(maxDate); + }).toList(); + + final List result = sut.findAllFull(minDate, maxDate); + assertFalse(result.isEmpty()); + assertThat(result, containsSameEntities(expected)); + } + + private List generateRecordsForAuthor(User author) { + return IntStream.range(0, 5).mapToObj(i -> { + final PatientRecord r = Generator.generatePatientRecord(author); + if (Generator.randomBoolean()) { + r.setDateCreated(new Date(System.currentTimeMillis() - i * Environment.MILLIS_PER_DAY)); + } else { + r.setDateCreated(new Date(System.currentTimeMillis() - 365 * Environment.MILLIS_PER_DAY)); + r.setLastModified(new Date(System.currentTimeMillis() - i * Environment.MILLIS_PER_DAY)); + } + return r; + }).toList(); + } + + @Test + void findAllFullReturnsRecordsMatchingSpecifiedDatePeriodAndInstitution() { + final User authorOne = generateAuthorWithInstitution(); + final Institution institution = authorOne.getInstitution(); + final User authorTwo = generateAuthorWithInstitution(); + final List allRecords = new ArrayList<>(generateRecordsForAuthor(authorOne)); + allRecords.addAll(generateRecordsForAuthor(authorTwo)); + transactional(() -> allRecords.forEach(this::persistRecordWithIdentification)); + final LocalDate minDate = LocalDate.now().minusDays(3); + final LocalDate maxDate = LocalDate.now().minusDays(1); + final List expected = allRecords.stream().filter(r -> { + final Date modified = r.getLastModified() != null ? r.getLastModified() : r.getDateCreated(); + final LocalDate modifiedDate = modified.toInstant().atZone(ZoneOffset.UTC).toLocalDate(); + return !modifiedDate.isBefore(minDate) && !modifiedDate.isAfter(maxDate) && r.getInstitution().getUri() + .equals(institution.getUri()); + }).toList(); + + final List result = sut.findAllFull(institution, minDate, maxDate); + assertFalse(result.isEmpty()); + assertThat(result, containsSameEntities(expected)); + } } diff --git a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java index 88806edc..9c2cffee 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/PatientRecordControllerTest.java @@ -9,27 +9,33 @@ import cz.cvut.kbss.study.model.User; import cz.cvut.kbss.study.service.InstitutionService; import cz.cvut.kbss.study.service.PatientRecordService; +import cz.cvut.kbss.study.util.IdentificationUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static cz.cvut.kbss.study.environment.util.ContainsSameEntities.containsSameEntities; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; @ExtendWith(MockitoExtension.class) public class PatientRecordControllerTest extends BaseControllerTestRunner { @@ -43,11 +49,14 @@ public class PatientRecordControllerTest extends BaseControllerTestRunner { @InjectMocks private PatientRecordController controller; + private User user; + @BeforeEach public void setUp() { super.setUp(controller); Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); + institution.setKey(IdentificationUtils.generateKey()); + this.user = Generator.generateUser(institution); Environment.setCurrentUser(user); } @@ -64,13 +73,14 @@ public void getRecordThrowsNotFoundWhenReportIsNotFound() throws Exception { @Test public void getRecordReturnsFoundRecord() throws Exception { final String key = "12345"; - PatientRecord patientRecord = Generator.generatePatientRecord(Environment.getCurrentUser()); + PatientRecord patientRecord = Generator.generatePatientRecord(user); when(patientRecordServiceMock.findByKey(key)).thenReturn(patientRecord); final MvcResult result = mockMvc.perform(get("/records/" + key)).andReturn(); assertEquals(HttpStatus.OK, HttpStatus.valueOf(result.getResponse().getStatus())); - final PatientRecord res = objectMapper.readValue(result.getResponse().getContentAsString(), PatientRecord.class); - assertEquals(res.getUri(),patientRecord.getUri()); + final PatientRecord res = + objectMapper.readValue(result.getResponse().getContentAsString(), PatientRecord.class); + assertEquals(res.getUri(), patientRecord.getUri()); verify(patientRecordServiceMock).findByKey(key); } @@ -82,8 +92,8 @@ public void getRecordsReturnsEmptyListWhenNoReportsAreFound() throws Exception { assertEquals(HttpStatus.OK, HttpStatus.valueOf(result.getResponse().getStatus())); final List body = objectMapper.readValue(result.getResponse().getContentAsString(), - new TypeReference>() { - }); + new TypeReference<>() { + }); assertTrue(body.isEmpty()); } @@ -108,8 +118,8 @@ public void getRecordsReturnsAllRecords() throws Exception { assertEquals(HttpStatus.OK, HttpStatus.valueOf(result.getResponse().getStatus())); final List body = objectMapper.readValue(result.getResponse().getContentAsString(), - new TypeReference>() { - }); + new TypeReference<>() { + }); assertEquals(3, body.size()); verify(patientRecordServiceMock).findAllRecords(); } @@ -133,12 +143,13 @@ public void finByInstitutionReturnsRecords() throws Exception { when(institutionServiceMock.findByKey(institution.getKey())).thenReturn(institution); when(patientRecordServiceMock.findByInstitution(institution)).thenReturn(records); System.out.println(institution.getKey()); - final MvcResult result = mockMvc.perform(get("/records").param("institution", institution.getKey())).andReturn(); + final MvcResult result = + mockMvc.perform(get("/records").param("institution", institution.getKey())).andReturn(); assertEquals(HttpStatus.OK, HttpStatus.valueOf(result.getResponse().getStatus())); final List body = objectMapper.readValue(result.getResponse().getContentAsString(), - new TypeReference>() { - }); + new TypeReference<>() { + }); assertEquals(2, body.size()); verify(institutionServiceMock).findByKey(institution.getKey()); } @@ -155,12 +166,11 @@ public void findByInstitutionReturnsNotFound() throws Exception { @Test public void createRecordReturnsResponseStatusCreated() throws Exception { - Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); PatientRecord record = Generator.generatePatientRecord(user); final MvcResult result = mockMvc.perform(post("/records").content(toJson(record)) - .contentType(MediaType.APPLICATION_JSON_VALUE)).andReturn(); + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn(); assertEquals(HttpStatus.CREATED, HttpStatus.valueOf(result.getResponse().getStatus())); } @@ -169,15 +179,14 @@ public void createRecordReturnsResponseStatusCreated() throws Exception { public void updateRecordReturnsResponseStatusNoContent() throws Exception { final String key = "12345"; - Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); PatientRecord record = Generator.generatePatientRecord(user); record.setKey(key); when(patientRecordServiceMock.findByKey(key)).thenReturn(record); final MvcResult result = mockMvc.perform(put("/records/" + key).content(toJson(record)) - .contentType(MediaType.APPLICATION_JSON_VALUE)).andReturn(); + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn(); assertEquals(HttpStatus.NO_CONTENT, HttpStatus.valueOf(result.getResponse().getStatus())); verify(patientRecordServiceMock).findByKey(key); @@ -187,13 +196,12 @@ public void updateRecordReturnsResponseStatusNoContent() throws Exception { public void updateRecordWithNonMatchingKeyReturnsResponseStatusBadRequest() throws Exception { final String key = "12345"; - Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); PatientRecord record = Generator.generatePatientRecord(user); record.setKey(key); - final MvcResult result = mockMvc.perform(put("/records/123456" ).content(toJson(record)) - .contentType(MediaType.APPLICATION_JSON_VALUE)).andReturn(); + final MvcResult result = mockMvc.perform(put("/records/123456").content(toJson(record)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn(); assertEquals(HttpStatus.BAD_REQUEST, HttpStatus.valueOf(result.getResponse().getStatus())); } @@ -202,15 +210,14 @@ public void updateRecordWithNonMatchingKeyReturnsResponseStatusBadRequest() thro public void updateRecordReturnsResponseStatusNotFound() throws Exception { final String key = "12345"; - Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); PatientRecord record = Generator.generatePatientRecord(user); record.setKey(key); when(patientRecordServiceMock.findByKey(key)).thenReturn(null); final MvcResult result = mockMvc.perform(put("/records/" + key).content(toJson(record)) - .contentType(MediaType.APPLICATION_JSON_VALUE)).andReturn(); + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andReturn(); assertEquals(HttpStatus.NOT_FOUND, HttpStatus.valueOf(result.getResponse().getStatus())); verify(patientRecordServiceMock).findByKey(key); @@ -220,17 +227,81 @@ public void updateRecordReturnsResponseStatusNotFound() throws Exception { public void deleteRecordReturnsResponseStatusNoContent() throws Exception { final String key = "12345"; - Institution institution = Generator.generateInstitution(); - User user = Generator.generateUser(institution); PatientRecord record = Generator.generatePatientRecord(user); record.setKey(key); when(patientRecordServiceMock.findByKey(key)).thenReturn(record); - final MvcResult result = mockMvc.perform(delete("/records/12345" )).andReturn(); + final MvcResult result = mockMvc.perform(delete("/records/12345")).andReturn(); assertEquals(HttpStatus.NO_CONTENT, HttpStatus.valueOf(result.getResponse().getStatus())); verify(patientRecordServiceMock).findByKey(key); } + @Test + void exportRecordsParsesProvidedDateBoundsAndPassesThemToService() throws Exception { + final LocalDate minDate = LocalDate.now().minusDays(35); + final LocalDate maxDate = LocalDate.now().minusDays(5); + final List records = + List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); + when(patientRecordServiceMock.findAllFull(any(), any())).thenReturn(records); + + final MvcResult mvcResult = mockMvc.perform(get("/records/export") + .param("minDate", minDate.toString()) + .param("maxDate", maxDate.toString())) + .andReturn(); + final List result = readValue(mvcResult, new TypeReference<>() { + }); + assertThat(result, containsSameEntities(records)); + verify(patientRecordServiceMock).findAllFull(minDate, maxDate); + } + + @Test + void exportRecordsUsesDefaultValuesForMinAndMaxDateWhenTheyAreNotProvidedByRequest() throws Exception { + final List records = + List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); + when(patientRecordServiceMock.findAllFull(any(), any())).thenReturn(records); + + final MvcResult mvcResult = mockMvc.perform(get("/records/export")).andReturn(); + final List result = readValue(mvcResult, new TypeReference<>() { + }); + assertThat(result, containsSameEntities(records)); + verify(patientRecordServiceMock).findAllFull(LocalDate.EPOCH, LocalDate.now()); + } + + @Test + void exportRecordsExportsRecordsForProvidedInstitutionForSpecifiedPeriod() throws Exception { + final LocalDate minDate = LocalDate.now().minusDays(35); + final LocalDate maxDate = LocalDate.now().minusDays(5); + final List records = + List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); + when(institutionServiceMock.findByKey(user.getInstitution().getKey())).thenReturn(user.getInstitution()); + when(patientRecordServiceMock.findAllFull(any(), any(), any())).thenReturn(records); + + final MvcResult mvcResult = mockMvc.perform(get("/records/export") + .param("minDate", minDate.toString()) + .param("maxDate", maxDate.toString()) + .param("institution", user.getInstitution().getKey())) + .andReturn(); + final List result = readValue(mvcResult, new TypeReference<>() { + }); + assertThat(result, containsSameEntities(records)); + verify(patientRecordServiceMock).findAllFull(user.getInstitution(), minDate, maxDate); + } + + @Test + void exportRecordsExportsRecordsForProvidedInstitutionWithDefaultDatesWhenNoneAreProvided() throws Exception { + final List records = + List.of(Generator.generatePatientRecord(user), Generator.generatePatientRecord(user)); + when(institutionServiceMock.findByKey(user.getInstitution().getKey())).thenReturn(user.getInstitution()); + when(patientRecordServiceMock.findAllFull(any(), any(), any())).thenReturn(records); + + final MvcResult mvcResult = mockMvc.perform(get("/records/export"). + param("institution", user.getInstitution().getKey())) + .andReturn(); + final List result = readValue(mvcResult, new TypeReference<>() { + }); + assertThat(result, containsSameEntities(records)); + verify(patientRecordServiceMock).findAllFull(user.getInstitution(), LocalDate.EPOCH, LocalDate.now()); + } } diff --git a/src/test/java/cz/cvut/kbss/study/rest/util/RestUtilsTest.java b/src/test/java/cz/cvut/kbss/study/rest/util/RestUtilsTest.java new file mode 100644 index 00000000..a2045f83 --- /dev/null +++ b/src/test/java/cz/cvut/kbss/study/rest/util/RestUtilsTest.java @@ -0,0 +1,35 @@ +package cz.cvut.kbss.study.rest.util; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RestUtilsTest { + + @Test + void parseTimestampReturnsLocalDateParsedFromSpecifiedString() { + final LocalDate value = LocalDate.now(); + assertEquals(value, RestUtils.parseDate(value.toString())); + } + + @Test + void parseTimestampThrowsResponseStatusExceptionWithStatus400ForUnparseableString() { + final Date date = new Date(); + final ResponseStatusException ex = assertThrows(ResponseStatusException.class, + () -> RestUtils.parseDate(date.toString())); + assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode()); + } + + @Test + void parseTimestampThrowsResponseStatusExceptionWithStatus400ForNullArgument() { + final ResponseStatusException ex = assertThrows(ResponseStatusException.class, + () -> RestUtils.parseDate(null)); + assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode()); + } +} \ No newline at end of file