From e300e22618a9ab0fd2ee8c05aa98294eb51ccf6b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 8 Jul 2024 12:00:00 +0200 Subject: [PATCH] Search with AQL --- .../numportal/domain/dto/QueryDto.java | 11 +++ .../properties/FeatureProperties.java | 2 + .../web/controller/QueryController.java | 37 +++++++++ src/main/resources/application-local.yml | 1 + .../QueryControllerFeatureDisabledIT.java | 52 +++++++++++++ .../tests/QueryControllerIT.java | 75 +++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 src/main/java/org/highmed/numportal/domain/dto/QueryDto.java create mode 100644 src/main/java/org/highmed/numportal/web/controller/QueryController.java create mode 100644 src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerFeatureDisabledIT.java create mode 100644 src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerIT.java 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..91191a5f --- /dev/null +++ b/src/main/java/org/highmed/numportal/domain/dto/QueryDto.java @@ -0,0 +1,11 @@ +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; +} diff --git a/src/main/java/org/highmed/numportal/properties/FeatureProperties.java b/src/main/java/org/highmed/numportal/properties/FeatureProperties.java index 1cb9144b..0b78693f 100644 --- a/src/main/java/org/highmed/numportal/properties/FeatureProperties.java +++ b/src/main/java/org/highmed/numportal/properties/FeatureProperties.java @@ -6,4 +6,6 @@ @Data @ConfigurationProperties(prefix = "feature") public class FeatureProperties { + + private boolean searchWithAql = false; } diff --git a/src/main/java/org/highmed/numportal/web/controller/QueryController.java b/src/main/java/org/highmed/numportal/web/controller/QueryController.java new file mode 100644 index 00000000..ccc42178 --- /dev/null +++ b/src/main/java/org/highmed/numportal/web/controller/QueryController.java @@ -0,0 +1,37 @@ +package org.highmed.numportal.web.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.ehrbase.openehr.sdk.response.dto.QueryResponseData; +import org.highmed.numportal.domain.dto.QueryDto; +import org.highmed.numportal.service.ehrbase.EhrBaseService; +import org.highmed.numportal.web.config.Role; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +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.RestController; + +@RestController +@AllArgsConstructor +@RequestMapping(value = "/query", produces = "application/json") +@SecurityRequirement(name = "security_auth") +@ConditionalOnProperty(value = "feature.search-with-aql", havingValue = "true") +public class QueryController { + + private final EhrBaseService ehrBaseService; + + @PostMapping("execute") + @Operation(description = "Executes an AQL query") + @PreAuthorize(Role.MANAGER) + public ResponseEntity execute( + @RequestBody @Valid QueryDto queryDto) { + return ResponseEntity.ok( + ehrBaseService.executePlainQuery(queryDto.getAql()) + ); + } +} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index a3075bdf..371af535 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -107,3 +107,4 @@ user-service: delete-users-cron: 0 0 5 * * * feature: + search-with-aql: false diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerFeatureDisabledIT.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerFeatureDisabledIT.java new file mode 100644 index 00000000..a839942b --- /dev/null +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerFeatureDisabledIT.java @@ -0,0 +1,52 @@ +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-with-aql = false + """) +public class QueryControllerFeatureDisabledIT extends IntegrationTest { + + @Autowired + public MockMvc mockMvc; + + private static final String PATH = "/query/execute"; + @Autowired + private ObjectMapper mapper; + + @Test + @SneakyThrows + @WithMockNumUser(roles = {"MANAGER"}) + public void execute() { + QueryDto queryDto = new QueryDto(); + + mockMvc.perform(post(PATH).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isNotFound()); + } + + @Test + @SneakyThrows + @WithMockNumUser() + public void executeAsNonAuthorizedUser() { + QueryDto queryDto = new QueryDto(); + + mockMvc.perform(post(PATH).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isNotFound()); + } +} diff --git a/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerIT.java b/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerIT.java new file mode 100644 index 00000000..d22e1eb7 --- /dev/null +++ b/src/test/java/org/highmed/numportal/integrationtesting/tests/QueryControllerIT.java @@ -0,0 +1,75 @@ +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.domain.dto.QueryDto; +import org.highmed.numportal.integrationtesting.security.WithMockNumUser; +import org.junit.Test; +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.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +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; + +@TestPropertySource(properties = """ + feature.search-with-aql = true + """) +public class QueryControllerIT extends IntegrationTest { + + @Autowired + public MockMvc mockMvc; + + private static final String PATH = "/query/execute"; + + @Autowired + private ObjectMapper mapper; + + @Test + @SneakyThrows + @WithMockNumUser(roles = {"MANAGER"}) + public void execute() { + var 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)); + + var result = mockMvc.perform(post(PATH).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 executeAsNonAuthorizedUser() { + var query = "SELECT *"; + QueryDto queryDto = new QueryDto(); + queryDto.setAql(query); + + mockMvc.perform(post(PATH).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(queryDto)) + ).andExpect(status().isForbidden()); + } +}