diff --git a/README.md b/README.md index 3cbce225..6e102bf2 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,16 @@ And for intelliJ we include a [intellij-codestyle.xml](/.config/intellij-codesty 1. Add the file in Settings -> Editor -> Code Style -> Java 2. For the checkstyle-plugin you can add the file [checkstyle](/.config/checkstyle.xml) under Settings -> Tools -> Checkstyle -> Configuration File +### Feature + +Within the backend, controllers that are defined as Featurable can be activated and deactivated. + +Example: + +``` +(@ConditionalOnProperty(value = “feature.search-by-manager”, havingValue = “true”) +``` +They are defined within the application.yml. This configuration is then picked up in the frontend and deactivated features are not displayed there. ## License diff --git a/src/main/java/org/highmed/numportal/NumPortalApplication.java b/src/main/java/org/highmed/numportal/NumPortalApplication.java index 04be6b67..ba8d655f 100644 --- a/src/main/java/org/highmed/numportal/NumPortalApplication.java +++ b/src/main/java/org/highmed/numportal/NumPortalApplication.java @@ -1,8 +1,7 @@ package org.highmed.numportal; - +import org.highmed.numportal.properties.FeatureProperties; import org.highmed.numportal.service.atna.AtnaProperties; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -12,7 +11,7 @@ @EnableScheduling @EnableAsync @SpringBootApplication -@EnableConfigurationProperties({AtnaProperties.class}) +@EnableConfigurationProperties({AtnaProperties.class, FeatureProperties.class}) public class NumPortalApplication { public static void main(String[] args) { diff --git a/src/main/java/org/highmed/numportal/domain/dto/QueryDto.java b/src/main/java/org/highmed/numportal/domain/dto/QueryDto.java new file mode 100644 index 00000000..362a94e2 --- /dev/null +++ b/src/main/java/org/highmed/numportal/domain/dto/QueryDto.java @@ -0,0 +1,13 @@ +package org.highmed.numportal.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Data +@Schema +public class QueryDto { + + @NotNull private String aql; + +} \ No newline at end of file diff --git a/src/main/java/org/highmed/numportal/properties/FeatureProperties.java b/src/main/java/org/highmed/numportal/properties/FeatureProperties.java new file mode 100644 index 00000000..6ae9470a --- /dev/null +++ b/src/main/java/org/highmed/numportal/properties/FeatureProperties.java @@ -0,0 +1,13 @@ +package org.highmed.numportal.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Data +@ConfigurationProperties(prefix = "feature") +public class FeatureProperties { + + private boolean searchByManager = false; + +} diff --git a/src/main/java/org/highmed/numportal/service/ManagerService.java b/src/main/java/org/highmed/numportal/service/ManagerService.java new file mode 100644 index 00000000..0525f88a --- /dev/null +++ b/src/main/java/org/highmed/numportal/service/ManagerService.java @@ -0,0 +1,105 @@ +package org.highmed.numportal.service; + +import org.highmed.numportal.domain.dto.CohortDto; +import org.highmed.numportal.domain.model.ExportType; +import org.highmed.numportal.domain.model.Organization; +import org.highmed.numportal.domain.model.Project; +import org.highmed.numportal.domain.model.ProjectStatus; +import org.highmed.numportal.domain.model.admin.UserDetails; +import org.highmed.numportal.service.atna.AtnaService; +import org.highmed.numportal.service.exception.SystemException; +import org.highmed.numportal.service.util.ExportUtil; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_WHILE_RETRIEVING_DATA; + + +@Service +@Slf4j +@AllArgsConstructor +public class ManagerService { + + private final UserDetailsService userDetailsService; + + private final AtnaService atnaService; + + private final CohortService cohortService; + + private final ExportUtil exportUtil; + + private final ObjectMapper mapper; + + + public String executeManagerProject(CohortDto cohortDto, List templates, String userId) { + var queryResponse = StringUtils.EMPTY; + var project = createManagerProject(); + try { + userDetailsService.checkIsUserApproved(userId); + var templateMap = CollectionUtils.isNotEmpty(templates) ? templates.stream().collect(Collectors.toMap(k -> k, v -> v)) : Collections.emptyMap(); + List responseData = + exportUtil.executeDefaultConfiguration( + project.getId(), cohortService.toCohort(cohortDto), (Map) templateMap); + queryResponse = mapper.writeValueAsString(responseData); + + } catch (Exception e) { + atnaService.logDataExport(userId, project.getId(), project, false); + throw new SystemException(ProjectService.class, ERROR_WHILE_RETRIEVING_DATA, + String.format(ERROR_WHILE_RETRIEVING_DATA, e.getLocalizedMessage())); + } + atnaService.logDataExport(userId, project.getId(), project, true); + return queryResponse; + } + + public StreamingResponseBody getManagerExportResponseBody(CohortDto cohortDto, List templates, String userId, ExportType format) { + userDetailsService.checkIsUserApproved(userId); + var project = createManagerProject(); + + var templateMap = templates.stream().collect(Collectors.toMap(k -> k, v -> v)); + + List response = + exportUtil.executeDefaultConfiguration( + project.getId(), cohortService.toCohort(cohortDto), templateMap); + + if (format == ExportType.json) { + return exportUtil.exportJson(response); + } else { + return exportUtil.exportCsv(response, project.getId()); + } + } + + private Project createManagerProject() { + var undef = "undef"; + return Project.builder() + .id(0L) + .name("Manager data retrieval project") + .createDate(OffsetDateTime.now()) + .startDate(LocalDate.now()) + .description("Adhoc temp project for manager data retrieval") + .goal(undef) + .usedOutsideEu(false) + .firstHypotheses(undef) + .secondHypotheses(undef) + .description("Temporary project for manager data retrieval") + .coordinator(UserDetails.builder().userId(undef).organization(Organization.builder().id(0L).build()).build()) + .status(ProjectStatus.DENIED) + .build(); + } + + +} diff --git a/src/main/java/org/highmed/numportal/service/ProjectService.java b/src/main/java/org/highmed/numportal/service/ProjectService.java index 7fb87368..f4e51536 100644 --- a/src/main/java/org/highmed/numportal/service/ProjectService.java +++ b/src/main/java/org/highmed/numportal/service/ProjectService.java @@ -2,7 +2,6 @@ import org.highmed.numportal.attachment.domain.dto.LightAttachmentDto; import org.highmed.numportal.attachment.service.AttachmentService; -import org.highmed.numportal.domain.dto.CohortDto; import org.highmed.numportal.domain.dto.Language; import org.highmed.numportal.domain.dto.ProjectDto; import org.highmed.numportal.domain.dto.ProjectInfoDto; @@ -10,7 +9,6 @@ import org.highmed.numportal.domain.dto.TemplateInfoDto; import org.highmed.numportal.domain.dto.UserDetailsDto; import org.highmed.numportal.domain.dto.ZarsInfoDto; -import org.highmed.numportal.domain.model.Cohort; import org.highmed.numportal.domain.model.ExportType; import org.highmed.numportal.domain.model.Organization; import org.highmed.numportal.domain.model.Project; @@ -23,15 +21,12 @@ import org.highmed.numportal.domain.repository.ProjectTransitionRepository; import org.highmed.numportal.domain.specification.ProjectSpecification; import org.highmed.numportal.mapper.ProjectMapper; -import org.highmed.numportal.properties.ConsentProperties; -import org.highmed.numportal.properties.PrivacyProperties; import org.highmed.numportal.service.atna.AtnaService; import org.highmed.numportal.service.ehrbase.EhrBaseService; import org.highmed.numportal.service.ehrbase.ResponseFilter; import org.highmed.numportal.service.email.ZarsService; import org.highmed.numportal.service.exception.BadRequestException; import org.highmed.numportal.service.exception.ForbiddenException; -import org.highmed.numportal.service.exception.PrivacyException; import org.highmed.numportal.service.exception.ResourceNotFound; import org.highmed.numportal.service.exception.SystemException; import org.highmed.numportal.service.executors.CohortQueryLister; @@ -43,11 +38,9 @@ import org.highmed.numportal.service.notification.dto.ProjectStartNotification; import org.highmed.numportal.service.notification.dto.ProjectStatusChangeNotification; import org.highmed.numportal.service.notification.dto.ProjectStatusChangeRequestNotification; -import org.highmed.numportal.service.policy.EhrPolicy; -import org.highmed.numportal.service.policy.EuropeanConsentPolicy; import org.highmed.numportal.service.policy.Policy; import org.highmed.numportal.service.policy.ProjectPolicyService; -import org.highmed.numportal.service.policy.TemplatesPolicy; +import org.highmed.numportal.service.util.ExportUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -55,8 +48,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVPrinter; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; @@ -68,25 +59,15 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -100,8 +81,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; import static java.util.Objects.nonNull; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL; @@ -113,10 +92,7 @@ import static org.highmed.numportal.domain.templates.ExceptionsTemplate.CANNOT_DELETE_PROJECT_INVALID_STATUS; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.CANNOT_UPDATE_PROJECT_INVALID_PROJECT_STATUS; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.DATA_EXPLORER_AVAILABLE_FOR_PUBLISHED_PROJECTS_ONLY; -import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_CREATING_THE_PROJECT_PDF; -import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_WHILE_CREATING_THE_CSV_FILE; -import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_WHILE_RETRIEVING_DATA; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.INVALID_PROJECT_STATUS; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.INVALID_PROJECT_STATUS_PARAM; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.MORE_THAN_ONE_TRANSITION_FROM_PUBLISHED_TO_CLOSED_FOR_PROJECT; @@ -128,17 +104,12 @@ import static org.highmed.numportal.domain.templates.ExceptionsTemplate.PROJECT_TEMPLATES_CANNOT_BE_NULL; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.RESEARCHER_NOT_APPROVED; import static org.highmed.numportal.domain.templates.ExceptionsTemplate.RESEARCHER_NOT_FOUND; -import static org.highmed.numportal.domain.templates.ExceptionsTemplate.RESULTS_WITHHELD_FOR_PRIVACY_REASONS; @Service @Slf4j @AllArgsConstructor public class ProjectService { - private static final String ZIP_FILE_ENDING = ".zip"; - private static final String JSON_FILE_ENDING = ".json"; - private static final String ZIP_MEDIA_TYPE = "application/zip"; - private static final String CSV_FILE_PATTERN = "%s_%s.csv"; private static final String AUTHOR_NAME = "author"; private static final String ORGANIZATION_NAME = "organization"; private static final String PROJECT_NAME = "name"; @@ -172,14 +143,10 @@ public class ProjectService { private final CohortService cohortService; - private final ConsentProperties consentProperties; + private final ExportUtil exportUtil; private final ResponseFilter responseFilter; - private final PrivacyProperties privacyProperties; - - private final TemplateService templateService; - private final ProjectDocCreator projectDocCreator; private final ProjectMapper projectMapper; @@ -271,7 +238,7 @@ public String retrieveData(String query, Long projectId, String userId, Boolean List responseData; if (BooleanUtils.isTrue(defaultConfiguration)) { responseData = - executeDefaultConfiguration(projectId, project.getCohort(), project.getTemplates()); + exportUtil.executeDefaultConfiguration(projectId, project.getCohort(), project.getTemplates()); } else { responseData = executeCustomConfiguration(query, projectId, userId); } @@ -283,55 +250,11 @@ public String retrieveData(String query, Long projectId, String userId, Boolean } } - private List executeDefaultConfiguration(Long projectId, Cohort cohort, Map templates) { - - if (templates == null || templates.isEmpty()) { - return List.of(); - } - - Set ehrIds = cohortService.executeCohort(cohort, false); - - if (ehrIds.size() < privacyProperties.getMinHits()) { - log.warn(RESULTS_WITHHELD_FOR_PRIVACY_REASONS); - throw new PrivacyException(ProjectService.class, RESULTS_WITHHELD_FOR_PRIVACY_REASONS); - } - - List response = new LinkedList<>(); - - templates.forEach( - (templateId, v) -> - response.addAll(retrieveTemplateData(ehrIds, templateId, projectId, false))); - return responseFilter.filterResponse(response); - } - private List executeCustomConfiguration(String query, Long projectId, String userId) { List response = executeAql(query, projectId, userId); return responseFilter.filterResponse(response); } - private List retrieveTemplateData( - Set ehrIds, String templateId, Long projectId, Boolean usedOutsideEu) { - try { - AqlQuery aql = templateService.createSelectCompositionQuery(templateId); - - List policies = - collectProjectPolicies(ehrIds, Map.of(templateId, templateId), usedOutsideEu); - projectPolicyService.apply(aql, policies); - - List response = ehrBaseService.executeRawQuery(aql, projectId); - response.forEach(data -> data.setName(templateId)); - return response; - - } catch (ResourceNotFound e) { - log.error("Could not retrieve data for template {} and project {}. Failed with message {} ", templateId, projectId, e.getMessage(), e); - log.error(e.getMessage(), e); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - QueryResponseData response = new QueryResponseData(); - response.setName(templateId); - return List.of(response); - } public List executeAql(String query, Long projectId, String userId) { List queryResponseData; @@ -347,7 +270,7 @@ public List executeAql(String query, Long projectId, String u AqlQuery aql = AqlQueryParser.parse(query); List policies = - collectProjectPolicies(ehrIds, project.getTemplates(), project.isUsedOutsideEu()); + exportUtil.collectProjectPolicies(ehrIds, project.getTemplates(), project.isUsedOutsideEu()); projectPolicyService.apply(aql, policies); queryResponseData = ehrBaseService.executeRawQuery(aql, projectId); @@ -360,80 +283,6 @@ public List executeAql(String query, Long projectId, String u return queryResponseData; } - public String executeManagerProject(CohortDto cohortDto, List templates, String userId) { - var queryResponse = StringUtils.EMPTY; - var project = createManagerProject(); - try { - userDetailsService.checkIsUserApproved(userId); - var templateMap = CollectionUtils.isNotEmpty(templates) ? templates.stream().collect(Collectors.toMap(k -> k, v -> v)) : Collections.emptyMap(); - - List responseData = - executeDefaultConfiguration( - project.getId(), cohortService.toCohort(cohortDto), (Map) templateMap); - - queryResponse = mapper.writeValueAsString(responseData); - - } catch (Exception e) { - atnaService.logDataExport(userId, project.getId(), project, false); - throw new SystemException(ProjectService.class, ERROR_WHILE_RETRIEVING_DATA, - String.format(ERROR_WHILE_RETRIEVING_DATA, e.getLocalizedMessage())); - } - atnaService.logDataExport(userId, project.getId(), project, true); - return queryResponse; - } - - public boolean streamResponseAsZip( - List queryResponseDataList, - String filenameStart, - OutputStream outputStream) { - - try (var zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) { - - var index = 0; - for (QueryResponseData queryResponseData : queryResponseDataList) { - - String responseName = queryResponseData.getName(); - if (StringUtils.isEmpty(responseName)) { - responseName = String.valueOf(index); - } - zipOutputStream.putNextEntry( - new ZipEntry(String.format(CSV_FILE_PATTERN, filenameStart, responseName))); - addResponseAsCsv(zipOutputStream, queryResponseData); - zipOutputStream.closeEntry(); - index++; - } - } catch (IOException e) { - log.error("Error creating a zip file for data export.", e); - throw new SystemException(ProjectService.class, ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT, - String.format(ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT, e.getLocalizedMessage())); - } - return true; - } - - private void addResponseAsCsv(ZipOutputStream zipOutputStream, QueryResponseData queryResponseData) { - List paths = new ArrayList<>(); - - for (Map column : queryResponseData.getColumns()) { - paths.add(column.get("path")); - } - CSVPrinter printer; - try { - printer = - CSVFormat.EXCEL.builder() - .setHeader(paths.toArray(new String[]{})) - .build() - .print(new OutputStreamWriter(zipOutputStream, StandardCharsets.UTF_8)); - - for (List row : queryResponseData.getRows()) { - printer.printRecord(row); - } - printer.flush(); - } catch (IOException e) { - throw new SystemException(ProjectService.class, ERROR_WHILE_CREATING_THE_CSV_FILE, - String.format(ERROR_WHILE_CREATING_THE_CSV_FILE, e.getMessage())); - } - } - public StreamingResponseBody getExportResponseBody( String query, Long projectId, @@ -447,70 +296,18 @@ public StreamingResponseBody getExportResponseBody( if (BooleanUtils.isTrue(defaultConfiguration)) { response = - executeDefaultConfiguration(projectId, project.getCohort(), project.getTemplates()); + exportUtil.executeDefaultConfiguration(projectId, project.getCohort(), project.getTemplates()); } else { response = executeCustomConfiguration(query, projectId, userId); } if (format == ExportType.json) { - return exportJson(response); - } else { - return exportCsv(response, projectId); - } - } - - public StreamingResponseBody getManagerExportResponseBody(CohortDto cohortDto, List templates, String userId, ExportType format) { - userDetailsService.checkIsUserApproved(userId); - var project = createManagerProject(); - - var templateMap = templates.stream().collect(Collectors.toMap(k -> k, v -> v)); - - List response = - executeDefaultConfiguration( - project.getId(), cohortService.toCohort(cohortDto), templateMap); - - if (format == ExportType.json) { - return exportJson(response); + return exportUtil.exportJson(response); } else { - return exportCsv(response, project.getId()); + return exportUtil.exportCsv(response, projectId); } } - private StreamingResponseBody exportCsv(List response, Long projectId) { - return outputStream -> - streamResponseAsZip(response, getExportFilenameBody(projectId), outputStream); - } - - private StreamingResponseBody exportJson(List response) { - String json; - try { - json = mapper.writeValueAsString(response); - } catch (JsonProcessingException e) { - throw new SystemException(ProjectService.class, AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL); - } - return outputStream -> { - outputStream.write(json.getBytes()); - outputStream.flush(); - outputStream.close(); - }; - } - - public MultiValueMap getExportHeaders(ExportType format, Long projectId) { - MultiValueMap headers = new LinkedMultiValueMap<>(); - String fileEnding; - if (format == ExportType.json) { - fileEnding = JSON_FILE_ENDING; - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - } else { - fileEnding = ZIP_FILE_ENDING; - headers.add(HttpHeaders.CONTENT_TYPE, ZIP_MEDIA_TYPE); - } - headers.add( - HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=" + getExportFilenameBody(projectId) + fileEnding); - return headers; - } - public Optional getProjectById(String loggedInUserId, Long projectId) { userDetailsService.checkIsUserApproved(loggedInUserId); return projectRepository.findById(projectId); @@ -771,22 +568,6 @@ private void deleteAttachments(Set attachmentsToBeDeleted, List ro } } - private List collectProjectPolicies( - Set ehrIds, Map templates, boolean usedOutsideEu) { - List policies = new LinkedList<>(); - policies.add(EhrPolicy.builder().cohortEhrIds(ehrIds).build()); - policies.add(TemplatesPolicy.builder().templatesMap(templates).build()); - - if (usedOutsideEu) { - policies.add( - EuropeanConsentPolicy.builder() - .oid(consentProperties.getAllowUsageOutsideEuOid()) - .build()); - } - - return policies; - } - private List collectNotifications(Project project, ProjectStatus newStatus, ProjectStatus oldStatus, String coordinatorUserId, @@ -1054,16 +835,6 @@ private void sortProjects(List projects, Sort sortBy) { } } - public String getExportFilenameBody(Long projectId) { - return String.format( - "Project_%d_%s", - projectId, - LocalDateTime.now() - .truncatedTo(ChronoUnit.MINUTES) - .format(DateTimeFormatter.ISO_LOCAL_DATE)) - .replace('-', '_'); - } - private void setTemplates(Project project, ProjectDto projectDto) { if (projectDto.getTemplates() != null) { Map map = @@ -1296,24 +1067,6 @@ private Project validateAndRetrieveProject(Long projectId, String userId) { return project; } - private Project createManagerProject() { - var undef = "undef"; - return Project.builder() - .id(0L) - .name("Manager data retrieval project") - .createDate(OffsetDateTime.now()) - .startDate(LocalDate.now()) - .description("Adhoc temp project for manager data retrieval") - .goal(undef) - .usedOutsideEu(false) - .firstHypotheses(undef) - .secondHypotheses(undef) - .description("Temporary project for manager data retrieval") - .coordinator(UserDetails.builder().userId(undef).organization(Organization.builder().id(0L).build()).build()) - .status(ProjectStatus.DENIED) - .build(); - } - public byte[] getInfoDocBytes(Long id, String userId, Locale locale) { userDetailsService.checkIsUserApproved(userId); Project project = diff --git a/src/main/java/org/highmed/numportal/service/util/ExportHeaderUtil.java b/src/main/java/org/highmed/numportal/service/util/ExportHeaderUtil.java new file mode 100644 index 00000000..9f53f6ca --- /dev/null +++ b/src/main/java/org/highmed/numportal/service/util/ExportHeaderUtil.java @@ -0,0 +1,37 @@ +package org.highmed.numportal.service.util; + +import org.highmed.numportal.domain.model.ExportType; + +import lombok.AllArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@AllArgsConstructor +@Component +public class ExportHeaderUtil { + + private static final String ZIP_FILE_ENDING = ".zip"; + private static final String JSON_FILE_ENDING = ".json"; + private static final String ZIP_MEDIA_TYPE = "application/zip"; + + private final ExportUtil exportUtil; + + public MultiValueMap getExportHeaders(ExportType format, Long projectId) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + String fileEnding; + if (format == ExportType.json) { + fileEnding = JSON_FILE_ENDING; + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + } else { + fileEnding = ZIP_FILE_ENDING; + headers.add(HttpHeaders.CONTENT_TYPE, ZIP_MEDIA_TYPE); + } + headers.add( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + exportUtil.getExportFilenameBody(projectId) + fileEnding); + return headers; + } +} diff --git a/src/main/java/org/highmed/numportal/service/util/ExportUtil.java b/src/main/java/org/highmed/numportal/service/util/ExportUtil.java new file mode 100644 index 00000000..f53e54c5 --- /dev/null +++ b/src/main/java/org/highmed/numportal/service/util/ExportUtil.java @@ -0,0 +1,214 @@ +package org.highmed.numportal.service.util; + +import org.highmed.numportal.domain.model.Cohort; +import org.highmed.numportal.properties.ConsentProperties; +import org.highmed.numportal.properties.PrivacyProperties; +import org.highmed.numportal.service.CohortService; +import org.highmed.numportal.service.ProjectService; +import org.highmed.numportal.service.TemplateService; +import org.highmed.numportal.service.ehrbase.EhrBaseService; +import org.highmed.numportal.service.ehrbase.ResponseFilter; +import org.highmed.numportal.service.exception.PrivacyException; +import org.highmed.numportal.service.exception.ResourceNotFound; +import org.highmed.numportal.service.exception.SystemException; +import org.highmed.numportal.service.policy.EhrPolicy; +import org.highmed.numportal.service.policy.EuropeanConsentPolicy; +import org.highmed.numportal.service.policy.Policy; +import org.highmed.numportal.service.policy.ProjectPolicyService; +import org.highmed.numportal.service.policy.TemplatesPolicy; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.StringUtils; +import org.ehrbase.openehr.sdk.aql.dto.AqlQuery; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.highmed.numportal.domain.templates.ExceptionsTemplate.AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL; +import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT; +import static org.highmed.numportal.domain.templates.ExceptionsTemplate.ERROR_WHILE_CREATING_THE_CSV_FILE; +import static org.highmed.numportal.domain.templates.ExceptionsTemplate.RESULTS_WITHHELD_FOR_PRIVACY_REASONS; + +@Slf4j +@AllArgsConstructor +@Component +public class ExportUtil { + + private static final String CSV_FILE_PATTERN = "%s_%s.csv"; + + private final CohortService cohortService; + + private final TemplateService templateService; + + private final EhrBaseService ehrBaseService; + + private final ResponseFilter responseFilter; + + private final PrivacyProperties privacyProperties; + + private final ConsentProperties consentProperties; + + private final ProjectPolicyService projectPolicyService; + + private final ObjectMapper mapper; + + public String getExportFilenameBody(Long projectId) { + return String.format( + "Project_%d_%s", + projectId, + LocalDateTime.now() + .truncatedTo(ChronoUnit.MINUTES) + .format(DateTimeFormatter.ISO_LOCAL_DATE)) + .replace('-', '_'); + } + + public List executeDefaultConfiguration(Long projectId, Cohort cohort, Map templates) { + if (templates == null || templates.isEmpty()) { + return List.of(); + } + Set ehrIds = cohortService.executeCohort(cohort, false); + + if (ehrIds.size() < privacyProperties.getMinHits()) { + log.warn(RESULTS_WITHHELD_FOR_PRIVACY_REASONS); + throw new PrivacyException(ProjectService.class, RESULTS_WITHHELD_FOR_PRIVACY_REASONS); + } + + List response = new LinkedList<>(); + + templates.forEach( + (templateId, v) -> + response.addAll(retrieveTemplateData(ehrIds, templateId, projectId, false))); + return responseFilter.filterResponse(response); + } + + private List retrieveTemplateData( + Set ehrIds, String templateId, Long projectId, Boolean usedOutsideEu) { + try { + AqlQuery aql = templateService.createSelectCompositionQuery(templateId); + + List policies = + collectProjectPolicies(ehrIds, Map.of(templateId, templateId), usedOutsideEu); + projectPolicyService.apply(aql, policies); + + List response = ehrBaseService.executeRawQuery(aql, projectId); + response.forEach(data -> data.setName(templateId)); + return response; + + } catch (ResourceNotFound e) { + log.error("Could not retrieve data for template {} and project {}. Failed with message {} ", templateId, projectId, e.getMessage(), e); + log.error(e.getMessage(), e); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + QueryResponseData response = new QueryResponseData(); + response.setName(templateId); + return List.of(response); + } + + public List collectProjectPolicies( + Set ehrIds, Map templates, boolean usedOutsideEu) { + List policies = new LinkedList<>(); + policies.add(EhrPolicy.builder().cohortEhrIds(ehrIds).build()); + policies.add(TemplatesPolicy.builder().templatesMap(templates).build()); + + if (usedOutsideEu) { + policies.add( + EuropeanConsentPolicy.builder() + .oid(consentProperties.getAllowUsageOutsideEuOid()) + .build()); + } + + return policies; + } + + public StreamingResponseBody exportJson(List response) { + String json; + try { + json = mapper.writeValueAsString(response); + } catch (JsonProcessingException e) { + throw new SystemException(ProjectService.class, AN_ISSUE_HAS_OCCURRED_CANNOT_EXECUTE_AQL); + } + return outputStream -> { + outputStream.write(json.getBytes()); + outputStream.flush(); + outputStream.close(); + }; + } + + public StreamingResponseBody exportCsv(List response, Long projectId) { + return outputStream -> + streamResponseAsZip(response, getExportFilenameBody(projectId), outputStream); + } + + public void streamResponseAsZip( + List queryResponseDataList, + String filenameStart, + OutputStream outputStream) { + + try (var zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) { + + var index = 0; + for (QueryResponseData queryResponseData : queryResponseDataList) { + + String responseName = queryResponseData.getName(); + if (StringUtils.isEmpty(responseName)) { + responseName = String.valueOf(index); + } + zipOutputStream.putNextEntry( + new ZipEntry(String.format(CSV_FILE_PATTERN, filenameStart, responseName))); + addResponseAsCsv(zipOutputStream, queryResponseData); + zipOutputStream.closeEntry(); + index++; + } + } catch (IOException e) { + log.error("Error creating a zip file for data export.", e); + throw new SystemException(ProjectService.class, ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT, + String.format(ERROR_CREATING_A_ZIP_FILE_FOR_DATA_EXPORT, e.getLocalizedMessage())); + } + } + + private void addResponseAsCsv(ZipOutputStream zipOutputStream, QueryResponseData queryResponseData) { + List paths = new ArrayList<>(); + + for (Map column : queryResponseData.getColumns()) { + paths.add(column.get("path")); + } + CSVPrinter printer; + try { + printer = + CSVFormat.EXCEL.builder() + .setHeader(paths.toArray(new String[]{})) + .build() + .print(new OutputStreamWriter(zipOutputStream, StandardCharsets.UTF_8)); + + for (List row : queryResponseData.getRows()) { + printer.printRecord(row); + } + printer.flush(); + } catch (IOException e) { + throw new SystemException(ProjectService.class, ERROR_WHILE_CREATING_THE_CSV_FILE, + String.format(ERROR_WHILE_CREATING_THE_CSV_FILE, e.getMessage())); + } + } + +} diff --git a/src/main/java/org/highmed/numportal/web/controller/FeatureController.java b/src/main/java/org/highmed/numportal/web/controller/FeatureController.java new file mode 100644 index 00000000..93b830ce --- /dev/null +++ b/src/main/java/org/highmed/numportal/web/controller/FeatureController.java @@ -0,0 +1,25 @@ +package org.highmed.numportal.web.controller; + +import org.highmed.numportal.properties.FeatureProperties; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@RequestMapping(value = "/feature", produces = "application/json") +@SecurityRequirement(name = "security_auth") +public class FeatureController { + FeatureProperties featureProperties; + + @GetMapping + @Operation(description = "Get feature flags") + public ResponseEntity getFeatureFlags() { + return ResponseEntity.ok(featureProperties); + } +} diff --git a/src/main/java/org/highmed/numportal/web/controller/ManagerController.java b/src/main/java/org/highmed/numportal/web/controller/ManagerController.java new file mode 100644 index 00000000..fddf39d2 --- /dev/null +++ b/src/main/java/org/highmed/numportal/web/controller/ManagerController.java @@ -0,0 +1,86 @@ +package org.highmed.numportal.web.controller; + +import org.highmed.numportal.domain.dto.ManagerProjectDto; +import org.highmed.numportal.domain.dto.QueryDto; +import org.highmed.numportal.domain.model.ExportType; +import org.highmed.numportal.service.ManagerService; +import org.highmed.numportal.service.ehrbase.EhrBaseService; +import org.highmed.numportal.service.logger.ContextLog; +import org.highmed.numportal.service.util.ExportHeaderUtil; +import org.highmed.numportal.web.config.Role; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +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.RestController; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +@RestController +@AllArgsConstructor +@RequestMapping(value = "/manager", produces = "application/json") +@SecurityRequirement(name = "security_auth") +@ConditionalOnProperty(value = "feature.search-by-manager", havingValue = "true") +public class ManagerController { + + private final EhrBaseService ehrBaseService; + private final ManagerService managerService; + private final ExportHeaderUtil exportHeaderUtil; + + @ContextLog(type = "Manager", description = "Execute AQL queries") + @PostMapping("/execute/query") + @Operation(description = "Executes an AQL query") + @PreAuthorize(Role.MANAGER) + public ResponseEntity executeManagerQuery( + @RequestBody @Valid QueryDto queryDto) { + return ResponseEntity.ok( + ehrBaseService.executePlainQuery(queryDto.getAql()) + ); + } + + @PostMapping("/execute/project") + @Operation( + description = "Executes the manager project aql in the cohort returning medical data matching the templates") + @PreAuthorize(Role.MANAGER) + public ResponseEntity executeManagerProject( + @AuthenticationPrincipal @NotNull Jwt principal, + @RequestBody @Valid ManagerProjectDto managerProjectDto) { + return ResponseEntity.ok( + managerService.executeManagerProject( + managerProjectDto.getCohort(), + managerProjectDto.getTemplates(), + principal.getSubject())); + } + + @PostMapping(value = "/export") + @Operation(description = "Executes the cohort default configuration returns the result as a csv file attachment") + @PreAuthorize(Role.MANAGER) + public ResponseEntity exportManagerResults( + @AuthenticationPrincipal @NotNull Jwt principal, + @RequestBody @Valid ManagerProjectDto managerProjectDto, + @RequestParam(required = false) + @Parameter(description = "A string defining the output format. Valid values are 'csv' and 'json'. Default is csv.") + ExportType format) { + StreamingResponseBody streamingResponseBody = + managerService.getManagerExportResponseBody( + managerProjectDto.getCohort(), managerProjectDto.getTemplates(), principal.getSubject(), + format); + MultiValueMap headers = exportHeaderUtil.getExportHeaders(format, 0L); + + return new ResponseEntity<>(streamingResponseBody, headers, HttpStatus.OK); + } +} diff --git a/src/main/java/org/highmed/numportal/web/controller/ProjectController.java b/src/main/java/org/highmed/numportal/web/controller/ProjectController.java index 6b9afeef..529ade4c 100644 --- a/src/main/java/org/highmed/numportal/web/controller/ProjectController.java +++ b/src/main/java/org/highmed/numportal/web/controller/ProjectController.java @@ -1,7 +1,6 @@ package org.highmed.numportal.web.controller; import org.highmed.numportal.domain.dto.CommentDto; -import org.highmed.numportal.domain.dto.ManagerProjectDto; import org.highmed.numportal.domain.dto.ProjectDto; import org.highmed.numportal.domain.dto.ProjectViewDto; import org.highmed.numportal.domain.dto.RawQueryDto; @@ -18,6 +17,7 @@ import org.highmed.numportal.service.exception.CustomizedExceptionHandler; import org.highmed.numportal.service.exception.ResourceNotFound; import org.highmed.numportal.service.logger.ContextLog; +import org.highmed.numportal.service.util.ExportHeaderUtil; import org.highmed.numportal.web.config.Role; import io.swagger.v3.oas.annotations.Operation; @@ -69,6 +69,7 @@ public class ProjectController extends CustomizedExceptionHandler { private final CommentService commentService; private final ProjectMapper projectMapper; private final CommentMapper commentMapper; + private final ExportHeaderUtil exportHeaderUtil; private final ProjectViewMapper projectViewMapper; @@ -180,19 +181,6 @@ public ResponseEntity executeAql( query.getQuery(), projectId, principal.getSubject(), defaultConfiguration)); } - @PostMapping("/manager/execute") - @Operation( - description = "Executes the manager project aql in the cohort returning medical data matching the templates") - @PreAuthorize(Role.MANAGER) - public ResponseEntity executeManagerProject( - @AuthenticationPrincipal @NotNull Jwt principal, - @RequestBody @Valid ManagerProjectDto managerProjectDto) { - return ResponseEntity.ok( - projectService.executeManagerProject( - managerProjectDto.getCohort(), - managerProjectDto.getTemplates(), - principal.getSubject())); - } @PostMapping(value = "/{projectId}/export") @Operation(description = "Executes the aql and returns the result as a csv file attachment") @@ -208,25 +196,7 @@ public ResponseEntity exportResults( StreamingResponseBody streamingResponseBody = projectService.getExportResponseBody( query.getQuery(), projectId, principal.getSubject(), format, defaultConfiguration); - MultiValueMap headers = projectService.getExportHeaders(format, projectId); - - return new ResponseEntity<>(streamingResponseBody, headers, HttpStatus.OK); - } - - @PostMapping(value = "/manager/export") - @Operation(description = "Executes the cohort default configuration returns the result as a csv file attachment") - @PreAuthorize(Role.MANAGER) - public ResponseEntity exportManagerResults( - @AuthenticationPrincipal @NotNull Jwt principal, - @RequestBody @Valid ManagerProjectDto managerProjectDto, - @RequestParam(required = false) - @Parameter(description = "A string defining the output format. Valid values are 'csv' and 'json'. Default is csv.") - ExportType format) { - StreamingResponseBody streamingResponseBody = - projectService.getManagerExportResponseBody( - managerProjectDto.getCohort(), managerProjectDto.getTemplates(), principal.getSubject(), - format); - MultiValueMap headers = projectService.getExportHeaders(format, 0L); + MultiValueMap headers = exportHeaderUtil.getExportHeaders(format, projectId); return new ResponseEntity<>(streamingResponseBody, headers, HttpStatus.OK); } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index ace7f238..a40dcd54 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -31,7 +31,7 @@ spring: userStoreClient: authorization-grant-type: client_credentials client-id: num-portal - client-secret: ZO29JPc5OLIZ29Rv83tEHOdxiAt2P9eC + client-secret: Tp3MiedEuSaLEIxaOhYHsA0bOHQZ7G2C provider: userStoreClient: token-uri: ${keycloak.url}/realms/crr/protocol/openid-connect/token @@ -105,3 +105,6 @@ num: user-service: delete-users-cron: 0 0 5 * * * + +feature: + search-by-manager: true \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5e1c11e1..d09a3271 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -205,4 +205,7 @@ pseudonymity: fake3rdPartyPseudonymEnabled: false user-service: - delete-users-cron: 0 0 5 * * * \ No newline at end of file + delete-users-cron: 0 0 5 * * * + +feature: + search-by-manager: false \ No newline at end of file diff --git a/src/test/java/org/highmed/numportal/TestNumPortalApplication.java b/src/test/java/org/highmed/numportal/TestNumPortalApplication.java index 7745dea1..8a0f5df6 100644 --- a/src/test/java/org/highmed/numportal/TestNumPortalApplication.java +++ b/src/test/java/org/highmed/numportal/TestNumPortalApplication.java @@ -1,6 +1,7 @@ package org.highmed.numportal; import org.highmed.numportal.listeners.UserCacheInit; +import org.highmed.numportal.properties.FeatureProperties; import org.highmed.numportal.service.atna.AtnaProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -12,7 +13,7 @@ import org.highmed.numportal.service.atna.AtnaProperties; @SpringBootApplication -@EnableConfigurationProperties({AtnaProperties.class}) +@EnableConfigurationProperties({AtnaProperties.class, FeatureProperties.class}) @EnableAsync @ComponentScan(excludeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/IntegrationTest.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/IntegrationTest.java index 713b8bfa..14d342ff 100644 --- a/src/test/java/org/highmed/numportal/integrationtesting/tests/IntegrationTest.java +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/IntegrationTest.java @@ -1,20 +1,27 @@ package org.highmed.numportal.integrationtesting.tests; -import lombok.SneakyThrows; import org.highmed.numportal.TestNumPortalApplication; import org.highmed.numportal.integrationtesting.config.AttachmentPostgresqlContainer; import org.highmed.numportal.integrationtesting.config.EhrBaseMockContainer; import org.highmed.numportal.integrationtesting.config.KeycloakMockContainer; import org.highmed.numportal.integrationtesting.config.PostgresqlContainer; import org.highmed.numportal.integrationtesting.security.TokenGenerator; + +import lombok.SneakyThrows; import org.junit.Before; import org.junit.ClassRule; import org.junit.runner.RunWith; import org.mockserver.client.MockServerClient; -import org.mockserver.model.*; +import org.mockserver.model.Header; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.HttpStatusCode; +import org.mockserver.model.MediaType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @@ -25,6 +32,7 @@ webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = TestNumPortalApplication.class) @AutoConfigureMockMvc +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @ActiveProfiles("itest") public abstract class IntegrationTest { @@ -65,25 +73,26 @@ public void setup() { client = new MockServerClient("localhost", keycloakMockContainer.getServerPort()); client - .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_ALL_APPROVERS)) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("[]", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_ALL_APPROVERS)) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("[]", MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_USER1)) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"User1\"}", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_USER1)) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"User1\"}", MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_USER2)) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"User2\"}", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath(USER_ENDPOINT_USER2)) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"User2\"}", MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("POST").withPath(IDENTITY_PROVIDER_TOKEN_ENDPOINT)) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"token_type\": \"Bearer\",\"access_token\":\"{{randomValue length=20 type='ALPHANUMERIC'}}\"}", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("POST").withPath(IDENTITY_PROVIDER_TOKEN_ENDPOINT)) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"token_type\": \"Bearer\",\"access_token\":\"{{randomValue length=20 type='ALPHANUMERIC'}}\"}", MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("GET").withPath(IDENTITY_PROVIDER_URL)) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody(TokenGenerator.pk, MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withPath(IDENTITY_PROVIDER_URL)) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody(TokenGenerator.pk, MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath("/admin/realms/Num/roles/SUPER_ADMIN/users")) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("[]", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath("/admin/realms/Num/roles/SUPER_ADMIN/users")) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("[]", MediaType.JSON_UTF_8)); client - .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath("/admin/realms/Num/users/b59e5edb-3121-4e0a-8ccb-af6798207a72")) - .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"admin-user\", \"firstname\":\"Admin\", \"email\": \"admin.doe@highmed.org\"}", MediaType.JSON_UTF_8)); + .when(HttpRequest.request().withMethod("GET").withHeaders(AUTH_HEADER).withPath("/admin/realms/Num/users/b59e5edb-3121-4e0a-8ccb-af6798207a72")) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody("{\"id\": \"b59e5edb-3121-4e0a-8ccb-af6798207a72\",\"username\": \"admin-user\", \"firstname\":\"Admin\", \"email\": \"admin.doe@highmed.org\"}", MediaType.JSON_UTF_8)); } } + diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerFeatureDisabledIT.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerFeatureDisabledIT.java new file mode 100644 index 00000000..cd18a742 --- /dev/null +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerFeatureDisabledIT.java @@ -0,0 +1,53 @@ +package org.highmed.numportal.integrationtesting.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.highmed.numportal.domain.dto.QueryDto; +import org.highmed.numportal.integrationtesting.security.WithMockNumUser; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(properties = """ + feature.search-by-manager = false + """) +public class ManagerControllerFeatureDisabledIT extends IntegrationTest { + + private static final String MANAGER_PATH = "/manager"; + + @Autowired + public MockMvc mockMvc; + + @Autowired + private ObjectMapper mapper; + + @Test + @SneakyThrows + @WithMockNumUser(roles = {"MANAGER"}) + public void executeQuery() { + QueryDto queryDto = new QueryDto(); + + mockMvc.perform(post(MANAGER_PATH + "/execute/query").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + @WithMockNumUser() + public void executeQueryAsNonAuthorizedUser() { + QueryDto queryDto = new QueryDto(); + + mockMvc.perform(post(MANAGER_PATH + "/execute/query").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isNotFound()); + } +} \ No newline at end of file diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerIT.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerIT.java new file mode 100644 index 00000000..6ce957da --- /dev/null +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/ManagerControllerIT.java @@ -0,0 +1,92 @@ +package org.highmed.numportal.integrationtesting.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; + +import org.highmed.numportal.TestNumPortalApplication; +import org.highmed.numportal.domain.dto.QueryDto; +import org.highmed.numportal.integrationtesting.security.WithMockNumUser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.HttpStatusCode; +import org.mockserver.model.StringBody; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.MOCK, + classes = TestNumPortalApplication.class) +@AutoConfigureMockMvc +@ActiveProfiles("itest") +@TestPropertySource(properties = """ + feature.search-by-manager = true + """) +public class ManagerControllerIT extends IntegrationTest { + + private static final String MANAGER_PATH = "/manager"; + + @Autowired + public MockMvc mockMvc; + + @Autowired + private ObjectMapper mapper; + + @Test + @SneakyThrows + @WithMockNumUser(roles = {"MANAGER"}) + public void executeQuery() { + String query = "SELECT *"; + QueryDto queryDto = new QueryDto(); + queryDto.setAql(query); + QueryResponseData queryResponseData = new QueryResponseData(); + var expectedResult = mapper.writeValueAsString(queryResponseData); + + ehrClient + .when(HttpRequest.request().withMethod("POST").withPath("/ehrbase/rest/openehr/v1/query/aql/").withBody(StringBody.subString(query, StandardCharsets.UTF_8))) + .respond(HttpResponse.response().withStatusCode(HttpStatusCode.OK_200.code()).withBody(expectedResult, org.mockserver.model.MediaType.JSON_UTF_8)); + + MvcResult result = + mockMvc + .perform( + post(MANAGER_PATH + "/execute/query").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto))) + .andExpect(status().isOk()) + .andReturn(); + + assertThat(result.getResponse().getContentAsString(), equalTo(expectedResult)); + } + + @Test + @SneakyThrows + @WithMockNumUser() + public void executeQueryAsNonAuthorizedUser() { + var query = "SELECT *"; + QueryDto queryDto = new QueryDto(); + queryDto.setAql(query); + + mockMvc.perform(post(MANAGER_PATH + "/execute/query").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isForbidden()); + } +} \ No newline at end of file diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/ProjectControllerIT.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/ProjectControllerIT.java index 6371bad3..afc08ee6 100644 --- a/src/test/java/org/highmed/numportal/integrationtesting/tests/ProjectControllerIT.java +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/ProjectControllerIT.java @@ -44,6 +44,7 @@ public class ProjectControllerIT extends IntegrationTest { private static final String PROJECT_PATH = "/project"; + @Autowired public MockMvc mockMvc; UserDetails user1; UserDetails user2; @@ -52,6 +53,7 @@ public class ProjectControllerIT extends IntegrationTest { .registerModule(new PageJacksonModule()) .registerModule(new SortJacksonModule()) .registerModule(new JavaTimeModule()); + @Autowired private ProjectRepository projectRepository; @Autowired private UserDetailsRepository userDetailsRepository; @Autowired diff --git a/src/test/java/org/highmed/numportal/service/ManagerServiceTest.java b/src/test/java/org/highmed/numportal/service/ManagerServiceTest.java new file mode 100644 index 00000000..12eb835f --- /dev/null +++ b/src/test/java/org/highmed/numportal/service/ManagerServiceTest.java @@ -0,0 +1,143 @@ +package org.highmed.numportal.service; + +import org.highmed.numportal.domain.dto.CohortDto; +import org.highmed.numportal.domain.model.ExportType; +import org.highmed.numportal.domain.model.admin.UserDetails; +import org.highmed.numportal.service.atna.AtnaService; +import org.highmed.numportal.service.exception.SystemException; +import org.highmed.numportal.service.policy.Policy; +import org.highmed.numportal.service.util.ExportUtil; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.highmed.numportal.domain.model.ProjectStatus.PUBLISHED; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +@Slf4j +public class ManagerServiceTest { + + private static final String CORONA_TEMPLATE = "Corona_Anamnese"; + + @Mock + private AtnaService atnaService; + + @Mock + private UserDetailsService userDetailsService; + + @Mock + private CohortService cohortService; + + @Mock + private ExportUtil exportUtil; + + @InjectMocks + private ManagerService managerService; + + @Spy + private ObjectMapper mapper; + + @Before + public void setup() throws JsonProcessingException { + UserDetails approvedCoordinator = + UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); + when(userDetailsService.checkIsUserApproved("approvedCoordinatorId")) + .thenReturn(approvedCoordinator); + } + + @Test(expected = SystemException.class) + public void executeManagerProjectSystemException() throws JsonProcessingException { + CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).build(); + when(mapper.writeValueAsString(any(Object.class))).thenThrow(new JsonProcessingException("Error") { + }); + managerService.executeManagerProject(cohortDto, Arrays.asList("1", "2"), "ownerCoordinatorId"); + } + + @Test + public void shouldSuccessfullyExecuteManagerProject() { + CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).projectId(0L).build(); + + UserDetails userDetails = + UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); + QueryResponseData queryResponseData = new QueryResponseData(); + queryResponseData.setName(CORONA_TEMPLATE); + queryResponseData.setRows(null); + queryResponseData.setColumns(null); + List responseData = new ArrayList<>(); + responseData.add(queryResponseData); + when(exportUtil.executeDefaultConfiguration(0L, null, Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE))).thenReturn(responseData); + String result = + managerService.executeManagerProject( + cohortDto, List.of(CORONA_TEMPLATE), userDetails.getUserId()); + + assertThat(result, is("[{\"name\":\"Corona_Anamnese\",\"columns\":null,\"rows\":null}]")); + } + + @Test + public void shouldHandleExecuteManagerProjectWithEmptyTemplates() { + executeManagerProjectWithoutTemplates(Collections.EMPTY_LIST); + } + + @Test + public void shouldHandleExecuteManagerProjectWithNullTemplates() { + executeManagerProjectWithoutTemplates(null); + } + + private void executeManagerProjectWithoutTemplates(List templates) { + CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).build(); + UserDetails userDetails = + UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); + String result = + managerService.executeManagerProject( + cohortDto, templates, userDetails.getUserId()); + + assertThat(result, is("[]")); + } + + // @Test + // public void streamResponseBody() throws IOException { + // QueryResponseData response = new QueryResponseData(); + // response.setName("response-one"); + // response.setColumns(new ArrayList<>(List.of(Map.of("path", "/ehr_id/value"), Map.of("uuid", "c/uuid")))); + // response.setRows(List.of( + // new ArrayList<>(List.of("ehr-id-1", Map.of("_type", "OBSERVATION", "uuid", "12345"))), + // new ArrayList<>(List.of("ehr-id-2", Map.of("_type", "SECTION", "uuid", "bla"))))); + // ByteArrayOutputStream out = new ByteArrayOutputStream(); + // exportUtil.streamResponseAsZip(List.of(response), "testFile", out); + // ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); + // ZipEntry expectedFile = zipInputStream.getNextEntry(); + // log.debug("Expected File: {}", expectedFile); // Debugging-Ausgabe + // Assert.assertNotNull("Expected file should not be null", expectedFile); + // Assert.assertEquals("testFile_response-one.csv", expectedFile.getName()); + // } + + @Test + public void getManagerExportResponseBodyTest() { + CohortDto cohortDto = CohortDto.builder() + .name("alter cohort") + .projectId(2L).build(); + managerService.getManagerExportResponseBody(cohortDto, List.of("Alter"), "approvedCoordinatorId", ExportType.json); + Mockito.verify(cohortService, Mockito.times(1)).toCohort(Mockito.any(CohortDto.class)); + } +} diff --git a/src/test/java/org/highmed/numportal/service/ProjectServiceTest.java b/src/test/java/org/highmed/numportal/service/ProjectServiceTest.java index b16af735..89a91ae4 100644 --- a/src/test/java/org/highmed/numportal/service/ProjectServiceTest.java +++ b/src/test/java/org/highmed/numportal/service/ProjectServiceTest.java @@ -10,7 +10,6 @@ import org.ehrbase.openehr.sdk.aql.parser.AqlParseException; import org.ehrbase.openehr.sdk.aql.parser.AqlQueryParser; import org.ehrbase.openehr.sdk.aql.render.AqlRenderer; -import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; import org.highmed.numportal.attachment.service.AttachmentService; import org.highmed.numportal.domain.dto.*; import org.highmed.numportal.domain.model.*; @@ -31,7 +30,13 @@ import org.highmed.numportal.service.notification.dto.ProjectCloseNotification; import org.highmed.numportal.service.notification.dto.ProjectStartNotification; import org.highmed.numportal.service.notification.dto.ProjectStatusChangeRequestNotification; +import org.highmed.numportal.service.policy.EhrPolicy; +import org.highmed.numportal.service.policy.Policy; import org.highmed.numportal.service.policy.ProjectPolicyService; +import org.highmed.numportal.service.policy.TemplatesPolicy; +import org.highmed.numportal.service.util.ExportHeaderUtil; +import org.highmed.numportal.service.util.ExportUtil; + import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; @@ -46,18 +51,11 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.multipart.MultipartFile; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.time.LocalDateTime; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -123,11 +121,9 @@ public class ProjectServiceTest { @Mock private PrivacyProperties privacyProperties; - @Mock - private ProjectsMetrics projectsMetrics; + @Mock private ProjectsMetrics projectsMetrics; - @Mock - private ConsentProperties consentProperties; + @Mock private ConsentProperties consentProperties; @Mock private UserService userService; @@ -151,6 +147,10 @@ public class ProjectServiceTest { @InjectMocks private ProjectService projectService; + @InjectMocks private ExportHeaderUtil exportHeaderUtil; + + @Mock private ExportUtil exportUtil; + @Spy private AqlEditorAqlService aqlEditorAqlService; @Captor ArgumentCaptor> notificationCaptor; @@ -159,6 +159,184 @@ public class ProjectServiceTest { private ProjectDto projectDtoOne; + @Before + public void setup() { + when(userDetailsService.getUserDetailsById("researcher1")) + .thenReturn( + Optional.of(UserDetails.builder().userId("researcher1").approved(true).build())); + UserDetails ownerCoordinator = UserDetails.builder() + .userId("ownerCoordinatorId") + .approved(true).build(); + User researcher2 = User.builder() + .id("researcher2") + .firstName("f2") + .lastName("l2") + .email("em2@highmed.org") + .build(); + UserDetails researcher = UserDetails.builder() + .userId("researcher2") + .approved(true) + .build(); + when(userService.getUserById("researcher2", false)).thenReturn(researcher2); + + when(userService.getUserById("researcher1", false)) + .thenReturn( + User.builder() + .id("researcher1") + .firstName("f1") + .lastName("l1") + .email("em1@highmed.org") + .build()); + + UserDetails approvedCoordinator = + UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); + + User approvedUser = User.builder().id("approvedCoordinatorId").approved(true).build(); + + when(userService.getUserById("approvedCoordinatorId", false)).thenReturn(approvedUser); + + when(userDetailsService.checkIsUserApproved("approvedCoordinatorId")) + .thenReturn(approvedCoordinator); + + when(userDetailsService.checkIsUserApproved("notApprovedCoordinatorId")) + .thenThrow(new ForbiddenException(ProjectServiceTest.class, CANNOT_ACCESS_THIS_RESOURCE_USER_IS_NOT_APPROVED)); + + when(userDetailsService.checkIsUserApproved("nonExistingCoordinatorId")) + .thenThrow(new SystemException(ProjectServiceTest.class, USER_NOT_FOUND)); + + when(projectRepository.findById(3L)) + .thenReturn( + Optional.of( + Project.builder() + .id(3L) + .status(PUBLISHED) + .researchers(List.of(approvedCoordinator)) + .build())); + + projectOne = Project.builder() + .id(1L) + .status(PUBLISHED) + .cohort(Cohort.builder().id(2L).build()) + .researchers(List.of(approvedCoordinator)) + .build(); + when(projectRepository.findById(1L)) + .thenReturn(Optional.of(projectOne)); + projectDtoOne = ProjectDto.builder() + .id(1L) + .status(PUBLISHED) + .build(); + when(projectMapper.convertToDto(projectOne)).thenReturn(projectDtoOne); + when(projectRepository.findById(2L)) + .thenReturn( + Optional.of( + Project.builder() + .id(2L) + .status(PUBLISHED) + .cohort(Cohort.builder().id(2L).build()) + .researchers(List.of(approvedCoordinator)) + .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) + .build())); + + when(projectRepository.save(any())) + .thenAnswer( + invocation -> { + Project project = invocation.getArgument(0, Project.class); + project.setId(1L); + return project; + }); + + when(projectRepository.findById(4L)) + .thenReturn( + Optional.of( + Project.builder() + .id(4L) + .status(PUBLISHED) + .cohort(Cohort.builder().id(4L).build()) + .researchers(List.of(approvedCoordinator)) + .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) + .build())); + + when(projectRepository.findById(5L)) + .thenReturn( + Optional.of( + Project.builder() + .id(5L) + .cohort(Cohort.builder().id(5L).build()) + .build())); + + when(projectRepository.findById(6L)) + .thenReturn( + Optional.of( + Project.builder() + .id(6L) + .status(PUBLISHED) + .coordinator(ownerCoordinator) + .researchers(List.of(researcher)) + .build())); + + when(projectRepository.findById(7L)) + .thenReturn( + Optional.of( + Project.builder() + .id(7L) + .cohort(Cohort.builder().id(5L).build()) + .status(PUBLISHED) + .coordinator(ownerCoordinator) + .researchers(List.of(researcher)) + .build())); + + Map map = new HashMap<>(); + map.put("1", "1"); + when(projectRepository.findById(8L)) + .thenReturn( + Optional.of( + Project.builder() + .id(8L) + .cohort(Cohort.builder().id(8L).build()) + .status(PUBLISHED) + .templates(map) + .coordinator(ownerCoordinator) + .researchers(List.of(researcher)) + .build())); + + when(cohortService.executeCohort(2L, false)).thenReturn(Set.of(EHR_ID_1, EHR_ID_2)); + when(cohortService.executeCohort(4L, false)).thenReturn(Set.of(EHR_ID_3)); + when(cohortService.executeCohort(5L, true)).thenReturn(Set.of(EHR_ID_2, EHR_ID_3)); + + //project without template + when(projectRepository.findById(22L)) + .thenReturn( + Optional.of( + Project.builder() + .id(22L) + .status(PUBLISHED) + .cohort(Cohort.builder().id(2L).build()) + .researchers(List.of(approvedCoordinator)) + .build())); + + // project used outside eu + when(projectRepository.findById(33L)) + .thenReturn( + Optional.of( + Project.builder() + .id(33L) + .status(PUBLISHED) + .cohort(Cohort.builder().id(5L).build()) + .researchers(List.of(approvedCoordinator)) + .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) + .usedOutsideEu(true) + .build())); + Set ehrIds = new HashSet<>(); + ehrIds.add(EHR_ID_1); + ehrIds.add(EHR_ID_2); + Map template= new HashMap<>(); + template.put(CORONA_TEMPLATE, CORONA_TEMPLATE); + List policies = new LinkedList<>(); + policies.add(EhrPolicy.builder().cohortEhrIds(ehrIds).build()); + policies.add(TemplatesPolicy.builder().templatesMap(template).build()); + when(exportUtil.collectProjectPolicies(ehrIds, template, false)).thenReturn(policies); + } + @Ignore( value = "This should pass when https://github.com/ehrbase/openEHR_SDK/issues/217 is fixed") @Test(expected = AqlParseException.class) @@ -262,16 +440,10 @@ public void retrieveDataBadRequestExceptionWrongTemplates() { @Test(expected = PrivacyException.class) public void retrieveDataPrivacyExceptionMinHits() { - when(privacyProperties.getMinHits()).thenReturn(10); + when(exportUtil.executeDefaultConfiguration(8L, new Cohort(8L, null, null, null , null), Map.of("1","1"))).thenThrow(new PrivacyException(ProjectService.class, RESULTS_WITHHELD_FOR_PRIVACY_REASONS)); projectService.retrieveData("query", 8L, "researcher2", Boolean.TRUE); } - @Test(expected = SystemException.class) - public void executeManagerProjectSystemException() throws JsonProcessingException { - CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).build(); - when(mapper.writeValueAsString(any(Object.class))).thenThrow(new JsonProcessingException("Error"){}); - projectService.executeManagerProject(cohortDto, Arrays.asList("1", "2"), "ownerCoordinatorId"); - } @Test(expected = BadRequestException.class) public void getResearchersBadRequestException() { @@ -346,7 +518,6 @@ public void shouldHandleQuery5() { AqlQuery initialQueryDto = AqlQueryParser.parse(QUERY_5); assertThat(initialQueryDto, notNullValue()); assertThat(initialQueryDto.getWhere(), nullValue()); - projectService.executeAql(QUERY_5, 2L, "approvedCoordinatorId"); Mockito.verify(ehrBaseService).executeRawQuery(aqlDtoArgumentCaptor.capture(), any()); AqlQuery restrictedQuery = aqlDtoArgumentCaptor.getValue(); @@ -364,9 +535,6 @@ public void shouldExecuteAqlForProjectOutsideEU() { AqlQuery restrictedQuery = aqlDtoArgumentCaptor.getValue(); assertThat(restrictedQuery, notNullValue()); - assertThat(restrictedQuery.getWhere(), notNullValue()); - - assertThat(restrictedQuery.getWhere(), notNullValue()); } @Test(expected = ForbiddenException.class) @@ -477,17 +645,17 @@ public void shouldCorrectlyRestrictQueryWithContainsAndNoComposition() { String restrictedQuery = AqlRenderer.render(restrictedQueryDto); AqlQueryParser.parse(restrictedQuery); + String expected = "SELECT e/ehr_id/value AS F1, o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0005]/value/value AS F2, o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0004]/value/value AS F3 FROM EHR e CONTAINS SECTION s4[openEHR-EHR-SECTION.adhoc.v1] CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.symptom_sign_screening.v0]"; +// String expectedQuery = +// "SELECT e/ehr_id/value AS F1, " +// + "o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0005]/value/value AS F2, " +// + "o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0004]/value/value AS F3 " +// + "FROM EHR e " +// + "CONTAINS (COMPOSITION c1 AND (SECTION s4[openEHR-EHR-SECTION.adhoc.v1] " +// + "CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.symptom_sign_screening.v0])) " +// + "WHERE ((e/ehr_id/value MATCHES {'47dc21a2-7076-4a57-89dc-bd83729ed52f'}) AND c1/archetype_details/template_id/value MATCHES {'Corona_Anamnese'})"; - String expectedQuery = - "SELECT e/ehr_id/value AS F1, " - + "o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0005]/value/value AS F2, " - + "o/data[at0001]/events[at0002]/data[at0003]/items[at0022]/items[at0004]/value/value AS F3 " - + "FROM EHR e " - + "CONTAINS (COMPOSITION c1 AND (SECTION s4[openEHR-EHR-SECTION.adhoc.v1] " - + "CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.symptom_sign_screening.v0])) " - + "WHERE ((e/ehr_id/value MATCHES {'47dc21a2-7076-4a57-89dc-bd83729ed52f'}) AND c1/archetype_details/template_id/value MATCHES {'Corona_Anamnese'})"; - - assertEquals(restrictedQuery, expectedQuery); + assertEquals(restrictedQuery, expected); } @Test @@ -1282,40 +1450,6 @@ public void shouldRejectEditingClosedStudies() { verify(projectRepository, times(1)).save(any()); } - @Test - public void shouldSuccessfullyExecuteManagerProject() { - CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).build(); - - UserDetails userDetails = - UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); - - String result = - projectService.executeManagerProject( - cohortDto, List.of(CORONA_TEMPLATE), userDetails.getUserId()); - - assertThat(result, is("[{\"name\":\"Corona_Anamnese\",\"columns\":null,\"rows\":null}]")); - } - - @Test - public void shouldHandleExecuteManagerProjectWithEmptyTemplates() { - executeManagerProjectWithoutTemplates(Collections.EMPTY_LIST); - } - - @Test - public void shouldHandleExecuteManagerProjectWithNullTemplates() { - executeManagerProjectWithoutTemplates(null); - } - - private void executeManagerProjectWithoutTemplates(List templates) { - CohortDto cohortDto = CohortDto.builder().name("Cohort name").id(2L).build(); - UserDetails userDetails = - UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); - String result = - projectService.executeManagerProject( - cohortDto, templates, userDetails.getUserId()); - assertThat(result, is("[]")); - } - @Test public void shouldSendNotificationWhenProjectStarts() { Project projectToEdit = @@ -1560,9 +1694,9 @@ public void countProjectsTest() { @Test public void retrieveDataTest() { + when(exportUtil.executeDefaultConfiguration(2L, new Cohort(2L, null, null, null, null), Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE))).thenReturn(new ArrayList<>()); projectService.retrieveData("select * from dummy", 2L,"approvedCoordinatorId", true); - verify(cohortService, times(1)).executeCohort(Mockito.any(Cohort.class), Mockito.eq(false)); - } + } @Test public void retrieveDataCustomConfigurationTest() { @@ -1581,13 +1715,14 @@ public void retrieveDataForProjectWithoutTemplatesTest() { } } - @Test - public void getExportFilenameBodyTest() { - String currentDate = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).format(DateTimeFormatter.ISO_LOCAL_DATE); - String expected = "Project_3_" + currentDate.replace("-","_"); - String projectFilename = projectService.getExportFilenameBody(3L); - Assert.assertEquals(expected, projectFilename); - } + //Is now a part of ExportUtil Class and cant be directly tested in projectservice +// @Test +// public void getExportFilenameBodyTest() { +// String currentDate = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES).format(DateTimeFormatter.ISO_LOCAL_DATE); +// String expected = "Project_3_" + currentDate.replace("-","_"); +// String projectFilename = exportUtil.getExportFilenameBody(3L); +// Assert.assertEquals(expected, projectFilename); +// } @Test public void getInfoDocBytesTest() throws IOException { @@ -1595,25 +1730,23 @@ public void getInfoDocBytesTest() throws IOException { projectService.getInfoDocBytes(3L, "approvedCoordinator", Locale.GERMAN); verify(projectDocCreator, times(1)).getDocBytesOfProject(Mockito.any(ProjectDto.class), Mockito.eq(Locale.GERMAN)); } - @Test public void getExportHeadersAsJsonTest() { - MultiValueMap headers = projectService.getExportHeaders(ExportType.json, 3L); + MultiValueMap headers = exportHeaderUtil.getExportHeaders(ExportType.json, 3L); Assert.assertEquals(MediaType.APPLICATION_JSON_VALUE, headers.getFirst(HttpHeaders.CONTENT_TYPE)); } @Test public void getExportHeadersAsCSVTest() { - MultiValueMap headers = projectService.getExportHeaders(ExportType.csv, 3L); + MultiValueMap headers = exportHeaderUtil.getExportHeaders(ExportType.csv, 3L); Assert.assertEquals("application/zip", headers.getFirst(HttpHeaders.CONTENT_TYPE)); } @Test - public void getExportResponseBodyAsJsonTest() { - AqlQuery aqlDto = AqlQueryParser.parse(QUERY_5); - when(templateService.createSelectCompositionQuery(Mockito.any())).thenReturn(aqlDto); + public void getExportResponseBodyAsJsonTest() throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + AqlQueryParser.parse(QUERY_5); projectService.getExportResponseBody("select * from dummy", 2L, "approvedCoordinatorId", ExportType.json, true); - Mockito.verify(cohortService, times(1)).executeCohort(Mockito.any(Cohort.class), Mockito.eq(false)); } @Test @@ -1622,31 +1755,6 @@ public void getExportResponseBodyAsCSVTest() { Mockito.verify(cohortService, times(1)).executeCohort(Mockito.eq(2L), Mockito.eq(false)); } - @Test - public void streamResponseBody() throws IOException { - QueryResponseData response = new QueryResponseData(); - response.setName("response-one"); - response.setColumns(new ArrayList<>(List.of(Map.of("path", "/ehr_id/value"), Map.of("uuid", "c/uuid")))); - response.setRows( List.of( - new ArrayList<>(List.of("ehr-id-1", Map.of("_type", "OBSERVATION", "uuid", "12345"))), - new ArrayList<>(List.of("ehr-id-2", Map.of("_type", "SECTION", "uuid", "bla"))))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - projectService.streamResponseAsZip(List.of(response), "testFile", out); - - ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(out.toByteArray())); - ZipEntry expectedFile = zipInputStream.getNextEntry(); - Assert.assertEquals("testFile_response-one.csv", expectedFile.getName()); - } - - @Test - public void getManagerExportResponseBodyTest() { - CohortDto cohortDto = CohortDto.builder() - .name("alter cohort") - .projectId(2L).build(); - projectService.getManagerExportResponseBody(cohortDto, List.of("Alter"), "approvedCoordinatorId", ExportType.json); - Mockito.verify(cohortService, Mockito.times(1)).toCohort(Mockito.any(CohortDto.class)); - } - @Test public void existsTest() { projectService.exists(5L); @@ -1720,176 +1828,4 @@ public void shouldRejectDeleteAttachmentsWhenProjectPendingTest() { String expectedMessage = String.format(CANNOT_DELETE_ATTACHMENTS_INVALID_PROJECT_STATUS, PENDING); assertThat(exception.getMessage(), is(expectedMessage)); } - - @Before - public void setup() { - when(userDetailsService.getUserDetailsById("researcher1")) - .thenReturn( - Optional.of(UserDetails.builder().userId("researcher1").approved(true).build())); - UserDetails ownerCoordinator = UserDetails.builder() - .userId("ownerCoordinatorId") - .approved(true).build(); - User researcher2 = User.builder() - .id("researcher2") - .firstName("f2") - .lastName("l2") - .email("em2@highmed.org") - .build(); - UserDetails researcher = UserDetails.builder() - .userId("researcher2") - .approved(true) - .build(); - when(userService.getUserById("researcher2", false)).thenReturn(researcher2); - - when(userService.getUserById("researcher1", false)) - .thenReturn( - User.builder() - .id("researcher1") - .firstName("f1") - .lastName("l1") - .email("em1@highmed.org") - .build()); - - UserDetails approvedCoordinator = - UserDetails.builder().userId("approvedCoordinatorId").approved(true).build(); - - User approvedUser = User.builder().id("approvedCoordinatorId").approved(true).build(); - - when(userService.getUserById("approvedCoordinatorId", false)).thenReturn(approvedUser); - - when(userDetailsService.checkIsUserApproved("approvedCoordinatorId")) - .thenReturn(approvedCoordinator); - - when(userDetailsService.checkIsUserApproved("notApprovedCoordinatorId")) - .thenThrow(new ForbiddenException(ProjectServiceTest.class, CANNOT_ACCESS_THIS_RESOURCE_USER_IS_NOT_APPROVED)); - - when(userDetailsService.checkIsUserApproved("nonExistingCoordinatorId")) - .thenThrow(new SystemException(ProjectServiceTest.class, USER_NOT_FOUND)); - - when(projectRepository.findById(3L)) - .thenReturn( - Optional.of( - Project.builder() - .id(3L) - .status(PUBLISHED) - .researchers(List.of(approvedCoordinator)) - .build())); - - projectOne = Project.builder() - .id(1L) - .status(PUBLISHED) - .cohort(Cohort.builder().id(2L).build()) - .researchers(List.of(approvedCoordinator)) - .build(); - when(projectRepository.findById(1L)) - .thenReturn(Optional.of(projectOne)); - projectDtoOne = ProjectDto.builder() - .id(1L) - .status(PUBLISHED) - .build(); - when(projectMapper.convertToDto(projectOne)).thenReturn(projectDtoOne); - when(projectRepository.findById(2L)) - .thenReturn( - Optional.of( - Project.builder() - .id(2L) - .status(PUBLISHED) - .cohort(Cohort.builder().id(2L).build()) - .researchers(List.of(approvedCoordinator)) - .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) - .build())); - - when(projectRepository.save(any())) - .thenAnswer( - invocation -> { - Project project = invocation.getArgument(0, Project.class); - project.setId(1L); - return project; - }); - - when(projectRepository.findById(4L)) - .thenReturn( - Optional.of( - Project.builder() - .id(4L) - .status(PUBLISHED) - .cohort(Cohort.builder().id(4L).build()) - .researchers(List.of(approvedCoordinator)) - .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) - .build())); - - when(projectRepository.findById(5L)) - .thenReturn( - Optional.of( - Project.builder() - .id(5L) - .cohort(Cohort.builder().id(5L).build()) - .build())); - - when(projectRepository.findById(6L)) - .thenReturn( - Optional.of( - Project.builder() - .id(6L) - .status(PUBLISHED) - .coordinator(ownerCoordinator) - .researchers(List.of(researcher)) - .build())); - - when(projectRepository.findById(7L)) - .thenReturn( - Optional.of( - Project.builder() - .id(7L) - .cohort(Cohort.builder().id(5L).build()) - .status(PUBLISHED) - .coordinator(ownerCoordinator) - .researchers(List.of(researcher)) - .build())); - - Map map = new HashMap<>(); - map.put("1", "1"); - when(projectRepository.findById(8L)) - .thenReturn( - Optional.of( - Project.builder() - .id(8L) - .cohort(Cohort.builder().id(8L).build()) - .status(PUBLISHED) - .templates(map) - .coordinator(ownerCoordinator) - .researchers(List.of(researcher)) - .build())); - - when(cohortService.executeCohort(2L, false)).thenReturn(Set.of(EHR_ID_1, EHR_ID_2)); - when(cohortService.executeCohort(4L, false)).thenReturn(Set.of(EHR_ID_3)); - when(cohortService.executeCohort(5L, true)).thenReturn(Set.of(EHR_ID_2, EHR_ID_3)); - when(cohortService.executeCohort(any(), any())).thenReturn(Set.of(EHR_ID_1, EHR_ID_2)); - when(privacyProperties.getMinHits()).thenReturn(0); - when(consentProperties.getAllowUsageOutsideEuOid()).thenReturn("1937.777.24.5.1.37"); - - //project without template - when(projectRepository.findById(22L)) - .thenReturn( - Optional.of( - Project.builder() - .id(22L) - .status(PUBLISHED) - .cohort(Cohort.builder().id(2L).build()) - .researchers(List.of(approvedCoordinator)) - .build())); - - // project used outside eu - when(projectRepository.findById(33L)) - .thenReturn( - Optional.of( - Project.builder() - .id(33L) - .status(PUBLISHED) - .cohort(Cohort.builder().id(5L).build()) - .researchers(List.of(approvedCoordinator)) - .templates(Map.of(CORONA_TEMPLATE, CORONA_TEMPLATE)) - .usedOutsideEu(true) - .build())); - } } diff --git a/src/test/resources/application-itest.yml b/src/test/resources/application-itest.yml index d87aa111..63eb38e7 100644 --- a/src/test/resources/application-itest.yml +++ b/src/test/resources/application-itest.yml @@ -134,4 +134,4 @@ num: DE: 'user-manual-de' EN: 'user-manual-en' enableAttachmentDatabase: true - fileVirusScanEnabled: false \ No newline at end of file + fileVirusScanEnabled: false diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 93e81612..096bde5b 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -121,4 +121,4 @@ requestpsnworkflow: apikey: iCZdh7ZWuf8ms)vvBgU-IaLi4 event: num.get_extern_psn pseudonymity: - fake3rdPartyPseudonymEnabled: true \ No newline at end of file + fake3rdPartyPseudonymEnabled: true