From 48234180618058a6cea0c8694552216dabc14657 Mon Sep 17 00:00:00 2001 From: SteveGT96 Date: Mon, 14 Oct 2024 15:04:22 +0100 Subject: [PATCH 1/5] update(OH2-388): Update admission create and update endpoints to support exam rows --- openapi/oh.yaml | 201 +++++++++-------- .../java/org/isf/config/SecurityConfig.java | 6 +- .../org/isf/exam/dto/ExamWithRowsDTO.java | 14 ++ .../org/isf/exam/rest/ExamController.java | 208 ++++++++++-------- 4 files changed, 240 insertions(+), 189 deletions(-) create mode 100644 src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java diff --git a/openapi/oh.yaml b/openapi/oh.yaml index cbdb6afde..5613903af 100644 --- a/openapi/oh.yaml +++ b/openapi/oh.yaml @@ -1470,7 +1470,7 @@ paths: put: tags: - Exams - operationId: updateExams + operationId: updateExam parameters: - name: code in: path @@ -1481,7 +1481,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExamDTO' + $ref: '#/components/schemas/ExamWithRowsDTO' required: true responses: "200": @@ -3045,11 +3045,11 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExamDTO' + $ref: '#/components/schemas/ExamWithRowsDTO' required: true responses: - "200": - description: OK + "201": + description: Created content: application/json: schema: @@ -6219,14 +6219,14 @@ components: description: lock format: int32 example: 0 + opd: + type: boolean male: type: boolean female: type: boolean pharmacy: type: boolean - opd: - type: boolean PatientDTO: required: - age @@ -6912,11 +6912,11 @@ components: type: string description: "Flag record deleted, values are 'Y' OR 'N' " example: "N" - fhu: - type: string yprog: type: integer format: int32 + fhu: + type: string description: The admission AdmissionTypeDTO: required: @@ -7544,6 +7544,23 @@ components: description: Lock format: int32 example: 0 + ExamWithRowsDTO: + required: + - exam + type: object + properties: + exam: + $ref: '#/components/schemas/ExamDTO' + rows: + type: array + description: Possible result for the exam(only for exams with procedure + 1 and 2) + example: "['POSITIVE', 'NEGATIVE']" + items: + type: string + description: Possible result for the exam(only for exams with procedure + 1 and 2) + example: "['POSITIVE', 'NEGATIVE']" PatientExaminationDTO: required: - patientCode @@ -8176,6 +8193,9 @@ components: type: object plain: type: boolean + height: + type: integer + format: int32 ascent: type: integer format: int32 @@ -8254,9 +8274,6 @@ components: maxAdvance: type: integer format: int32 - height: - type: integer - format: int32 clipBounds: type: object properties: @@ -8297,6 +8314,15 @@ components: properties: empty: type: boolean + height: + type: number + format: double + minX: + type: number + format: double + minY: + type: number + format: double maxX: type: number format: double @@ -8312,13 +8338,18 @@ components: width: type: number format: double - height: + "y": type: number format: double x: type: number format: double - "y": + rect: + type: object + properties: + empty: + type: boolean + height: type: number format: double minX: @@ -8327,11 +8358,6 @@ components: minY: type: number format: double - rect: - type: object - properties: - empty: - type: boolean maxX: type: number format: double @@ -8347,38 +8373,29 @@ components: width: type: number format: double - height: - type: number - format: double - x: - type: number - format: double "y": type: number format: double - minX: - type: number - format: double - minY: + x: type: number format: double writeOnly: true - maxX: + minX: type: number format: double - maxY: + minY: type: number format: double - centerX: + maxX: type: number format: double - centerY: + maxY: type: number format: double - minX: + centerX: type: number format: double - minY: + centerY: type: number format: double xormode: @@ -8457,6 +8474,15 @@ components: properties: empty: type: boolean + height: + type: number + format: double + minX: + type: number + format: double + minY: + type: number + format: double maxX: type: number format: double @@ -8472,13 +8498,18 @@ components: width: type: number format: double - height: + "y": type: number format: double x: type: number format: double - "y": + rect: + type: object + properties: + empty: + type: boolean + height: type: number format: double minX: @@ -8487,11 +8518,6 @@ components: minY: type: number format: double - rect: - type: object - properties: - empty: - type: boolean maxX: type: number format: double @@ -8507,22 +8533,19 @@ components: width: type: number format: double - height: - type: number - format: double - x: - type: number - format: double "y": type: number format: double - minX: - type: number - format: double - minY: + x: type: number format: double writeOnly: true + minX: + type: number + format: double + minY: + type: number + format: double maxX: type: number format: double @@ -8535,17 +8558,20 @@ components: centerY: type: number format: double + bounds2D: + type: object + properties: + empty: + type: boolean + height: + type: number + format: double minX: type: number format: double minY: type: number format: double - bounds2D: - type: object - properties: - empty: - type: boolean maxX: type: number format: double @@ -8561,19 +8587,10 @@ components: width: type: number format: double - height: - type: number - format: double - x: - type: number - format: double "y": type: number format: double - minX: - type: number - format: double - minY: + x: type: number format: double clipRect: @@ -8616,6 +8633,15 @@ components: properties: empty: type: boolean + height: + type: number + format: double + minX: + type: number + format: double + minY: + type: number + format: double maxX: type: number format: double @@ -8631,13 +8657,18 @@ components: width: type: number format: double - height: + "y": type: number format: double x: type: number format: double - "y": + rect: + type: object + properties: + empty: + type: boolean + height: type: number format: double minX: @@ -8646,11 +8677,6 @@ components: minY: type: number format: double - rect: - type: object - properties: - empty: - type: boolean maxX: type: number format: double @@ -8666,38 +8692,29 @@ components: width: type: number format: double - height: - type: number - format: double - x: - type: number - format: double "y": type: number format: double - minX: - type: number - format: double - minY: + x: type: number format: double writeOnly: true - maxX: + minX: type: number format: double - maxY: + minY: type: number format: double - centerX: + maxX: type: number format: double - centerY: + maxY: type: number format: double - minX: + centerX: type: number format: double - minY: + centerY: type: number format: double deprecated: true @@ -8762,10 +8779,10 @@ components: smsInt: type: integer format: int32 - sms: - type: boolean notify: type: boolean + sms: + type: boolean medical: type: integer format: int32 diff --git a/src/main/java/org/isf/config/SecurityConfig.java b/src/main/java/org/isf/config/SecurityConfig.java index 9259fd923..edd196fcc 100644 --- a/src/main/java/org/isf/config/SecurityConfig.java +++ b/src/main/java/org/isf/config/SecurityConfig.java @@ -155,9 +155,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.PUT, "/deliverytypes/**").hasAuthority("deliverytypes.update") .requestMatchers(HttpMethod.DELETE, "/deliverytypes/**").hasAuthority("deliverytypes.delete") // exams - .requestMatchers(HttpMethod.POST, "/exams/**").hasAuthority("exams.create") + .requestMatchers(HttpMethod.POST, "/exams/**") + .access("hasAuthority('exams.create') and hasAuthority('examrows.create')") .requestMatchers(HttpMethod.GET, "/exams/**").hasAnyAuthority("exams.read") - .requestMatchers(HttpMethod.PUT, "/exams/**").hasAuthority("exams.update") + .requestMatchers(HttpMethod.PUT, "/exams/**") + .access("hasAuthority('exams.update') and hasAuthority('examrows.create') and hasAuthority('examrows.delete')") .requestMatchers(HttpMethod.DELETE, "/exams/**").hasAuthority("exams.delete") // examrows .requestMatchers(HttpMethod.POST, "/examrows/**").hasAuthority("examrows.create") diff --git a/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java b/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java new file mode 100644 index 000000000..3ba7ed286 --- /dev/null +++ b/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java @@ -0,0 +1,14 @@ +package org.isf.exam.dto; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ExamWithRowsDTO( + @NotNull @Schema(name = "exam", description = "The exam to mutate") ExamDTO exam, + @Schema(name = "rows", example = "['POSITIVE', 'NEGATIVE']", description = "Possible result for the exam(only for exams with procedure 1 and 2)") List rows +) { + +} diff --git a/src/main/java/org/isf/exam/rest/ExamController.java b/src/main/java/org/isf/exam/rest/ExamController.java index 40432646b..db26e3bb6 100644 --- a/src/main/java/org/isf/exam/rest/ExamController.java +++ b/src/main/java/org/isf/exam/rest/ExamController.java @@ -21,12 +21,16 @@ */ package org.isf.exam.rest; +import java.util.Collections; import java.util.List; import java.util.Optional; +import jakarta.validation.Valid; + import org.isf.exa.manager.ExamBrowsingManager; import org.isf.exa.model.Exam; import org.isf.exam.dto.ExamDTO; +import org.isf.exam.dto.ExamWithRowsDTO; import org.isf.exam.mapper.ExamMapper; import org.isf.exatype.manager.ExamTypeBrowserManager; import org.isf.exatype.model.ExamType; @@ -43,108 +47,122 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; -@RestController(value = "/exams") @Tag(name = "Exams") +@RestController(value = "/exams") @SecurityRequirement(name = "bearerAuth") public class ExamController { - @Autowired - protected ExamBrowsingManager examManager; - - @Autowired - protected ExamTypeBrowserManager examTypeBrowserManager; - - @Autowired - private ExamMapper examMapper; - - public ExamController(ExamBrowsingManager examManager, ExamMapper examMapper) { - this.examManager = examManager; - this.examMapper = examMapper; - } - - @PostMapping(value = "/exams", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity newExam(@RequestBody ExamDTO newExam) throws OHServiceException { - ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> newExam.getExamtype().getCode().equals(et.getCode())).findFirst().orElse(null); - - if (examType == null) { - throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); - } - - Exam exam = examMapper.map2Model(newExam); - exam.setExamtype(examType); - try { - examManager.newExam(exam); - } catch (OHServiceException serviceException) { - throw new OHAPIException(new OHExceptionMessage("Exam not created.")); - } - return ResponseEntity.ok(examMapper.map2DTO(exam)); - } - - @PutMapping(value = "/exams/{code:.+}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateExams(@PathVariable String code, @RequestBody ExamDTO updateExam) throws OHServiceException { - - if (!updateExam.getCode().equals(code)) { - throw new OHAPIException(new OHExceptionMessage("Exam code mismatch.")); - } - if (examManager.getExams().stream().noneMatch(e -> e.getCode().equals(code))) { - throw new OHAPIException(new OHExceptionMessage("Exam not found.")); - } - - ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> updateExam.getExamtype().getCode().equals(et.getCode())).findFirst().orElse(null); - if (examType == null) { - throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); - } - - Exam exam = examMapper.map2Model(updateExam); - exam.setExamtype(examType); - exam.setLock(updateExam.getLock()); - Exam examUpdated = examManager.updateExam(exam); - if (examUpdated == null) { - throw new OHAPIException(new OHExceptionMessage("Exam not updated.")); - } - - return ResponseEntity.ok(examMapper.map2DTO(examUpdated)); - } - - - @GetMapping(value = "/exams/description/{description:.+}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getExams(@PathVariable String description) throws OHServiceException { - List exams = examMapper.map2DTOList(examManager.getExams(description)); - - if (exams == null || exams.isEmpty()) { - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); - } else { - return ResponseEntity.ok(exams); - } - } - - @GetMapping(value = "/exams", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> getExams() throws OHServiceException { - List exams = examMapper.map2DTOList(examManager.getExams()); - - if (exams == null || exams.isEmpty()) { - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); - } else { - return ResponseEntity.ok(exams); - } - } - - @DeleteMapping(value = "/exams/{code:.+}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteExam(@PathVariable String code) throws OHServiceException { - Optional exam = examManager.getExams().stream().filter(e -> e.getCode().equals(code)).findFirst(); - if (!exam.isPresent()) { - throw new OHAPIException(new OHExceptionMessage("Exam not found.")); - } - try { - examManager.deleteExam(exam.get()); - } catch (OHServiceException serviceException) { - throw new OHAPIException(new OHExceptionMessage("Exam not deleted.")); - } - return ResponseEntity.ok(true); - } + @Autowired + protected ExamBrowsingManager examManager; + + @Autowired + protected ExamTypeBrowserManager examTypeBrowserManager; + + @Autowired + private ExamMapper examMapper; + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping(value = "/exams", produces = MediaType.APPLICATION_JSON_VALUE) + public ExamDTO newExam(@Valid @RequestBody ExamWithRowsDTO examWithRowsDTO) throws OHServiceException { + ExamDTO examDTO = examWithRowsDTO.exam(); + List examRows = examWithRowsDTO.rows(); + + ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> examDTO.getExamtype().getCode().equals(et.getCode())).findFirst() + .orElse(null); + + if (examType == null) { + throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); + } + + validateExamDefaultResult(examDTO, examRows); + + Exam exam = examMapper.map2Model(examDTO); + exam.setExamtype(examType); + try { + exam = examManager.create(exam, examRows); + } catch (OHServiceException serviceException) { + throw new OHAPIException(new OHExceptionMessage("Exam not created.")); + } + return examMapper.map2DTO(exam); + } + + @PutMapping(value = "/exams/{code:.+}", produces = MediaType.APPLICATION_JSON_VALUE) + public ExamDTO updateExam(@PathVariable String code, @Valid @RequestBody ExamWithRowsDTO examWithRowsDTO) throws OHServiceException { + ExamDTO examDTO = examWithRowsDTO.exam(); + List examRows = examWithRowsDTO.rows(); + + if (!examDTO.getCode().equals(code)) { + throw new OHAPIException(new OHExceptionMessage("Exam code mismatch.")); + } + if (examManager.getExams().stream().noneMatch(e -> e.getCode().equals(code))) { + throw new OHAPIException(new OHExceptionMessage("Exam not found."), HttpStatus.NOT_FOUND); + } + + ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> examDTO.getExamtype().getCode().equals(et.getCode())).findFirst() + .orElse(null); + if (examType == null) { + throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); + } + + validateExamDefaultResult(examDTO, examRows); + + Exam exam = examMapper.map2Model(examDTO); + exam.setExamtype(examType); + Exam examUpdated = examManager.update(exam, examRows); + if (examUpdated == null) { + throw new OHAPIException(new OHExceptionMessage("Exam not updated.")); + } + + return examMapper.map2DTO(examUpdated); + } + + private void validateExamDefaultResult(ExamDTO examDTO, List examRows) throws OHAPIException { + if (examDTO.getProcedure() == 1 && examDTO.getDefaultResult() != null) { + if ((examRows == null ? Collections.emptyList() : examRows).stream().noneMatch(row -> examDTO.getDefaultResult() == row)) { + throw new OHAPIException(new OHExceptionMessage("Exam default result doesn't match any exam rows.")); + } + } + } + + @GetMapping(value = "/exams/description/{description:.+}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getExams(@PathVariable String description) throws OHServiceException { + List exams = examMapper.map2DTOList(examManager.getExams(description)); + + if (exams == null || exams.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } else { + return ResponseEntity.ok(exams); + } + } + + @GetMapping(value = "/exams", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getExams() throws OHServiceException { + List exams = examMapper.map2DTOList(examManager.getExams()); + + if (exams == null || exams.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null); + } else { + return ResponseEntity.ok(exams); + } + } + + @DeleteMapping(value = "/exams/{code:.+}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity deleteExam(@PathVariable String code) throws OHServiceException { + Optional exam = examManager.getExams().stream().filter(e -> e.getCode().equals(code)).findFirst(); + if (!exam.isPresent()) { + throw new OHAPIException(new OHExceptionMessage("Exam not found.")); + } + try { + examManager.deleteExam(exam.get()); + } catch (OHServiceException serviceException) { + throw new OHAPIException(new OHExceptionMessage("Exam not deleted.")); + } + return ResponseEntity.ok(true); + } } From 2f0886739ed7af946e2aefc5ca9f84086a5e76c1 Mon Sep 17 00:00:00 2001 From: SteveGT96 Date: Mon, 14 Oct 2024 17:58:30 +0100 Subject: [PATCH 2/5] chore: Add tests --- .../org/isf/exam/rest/ExamController.java | 30 ++- .../java/org/isf/exam/data/ExamHelper.java | 94 ++++++++ .../org/isf/exam/rest/ExamControllerTest.java | 217 ++++++++++++++++++ 3 files changed, 325 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/isf/exam/data/ExamHelper.java create mode 100644 src/test/java/org/isf/exam/rest/ExamControllerTest.java diff --git a/src/main/java/org/isf/exam/rest/ExamController.java b/src/main/java/org/isf/exam/rest/ExamController.java index db26e3bb6..fa3ca3cc9 100644 --- a/src/main/java/org/isf/exam/rest/ExamController.java +++ b/src/main/java/org/isf/exam/rest/ExamController.java @@ -72,15 +72,18 @@ public class ExamController { public ExamDTO newExam(@Valid @RequestBody ExamWithRowsDTO examWithRowsDTO) throws OHServiceException { ExamDTO examDTO = examWithRowsDTO.exam(); List examRows = examWithRowsDTO.rows(); - - ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> examDTO.getExamtype().getCode().equals(et.getCode())).findFirst() - .orElse(null); + + ExamType examType = examTypeBrowserManager.findByCode(examDTO.getExamtype().getCode()); if (examType == null) { throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); } - validateExamDefaultResult(examDTO, examRows); + if (examDTO.getProcedure() == 1 && examDTO.getDefaultResult() != null) { + if ((examRows == null ? Collections.emptyList() : examRows).stream().noneMatch(row -> examDTO.getDefaultResult().equals(row))) { + throw new OHAPIException(new OHExceptionMessage("Exam default result doesn't match any exam rows.")); + } + } Exam exam = examMapper.map2Model(examDTO); exam.setExamtype(examType); @@ -100,17 +103,20 @@ public ExamDTO updateExam(@PathVariable String code, @Valid @RequestBody ExamWit if (!examDTO.getCode().equals(code)) { throw new OHAPIException(new OHExceptionMessage("Exam code mismatch.")); } - if (examManager.getExams().stream().noneMatch(e -> e.getCode().equals(code))) { + if (examManager.findByCode(code) == null) { throw new OHAPIException(new OHExceptionMessage("Exam not found."), HttpStatus.NOT_FOUND); } - ExamType examType = examTypeBrowserManager.getExamType().stream().filter(et -> examDTO.getExamtype().getCode().equals(et.getCode())).findFirst() - .orElse(null); + ExamType examType = examTypeBrowserManager.findByCode(examDTO.getExamtype().getCode()); if (examType == null) { throw new OHAPIException(new OHExceptionMessage("Exam type not found.")); } - validateExamDefaultResult(examDTO, examRows); + if (examDTO.getProcedure() == 1 && examDTO.getDefaultResult() != null) { + if ((examRows == null ? Collections.emptyList() : examRows).stream().noneMatch(row -> examDTO.getDefaultResult().equals(row))) { + throw new OHAPIException(new OHExceptionMessage("Exam default result doesn't match any exam rows.")); + } + } Exam exam = examMapper.map2Model(examDTO); exam.setExamtype(examType); @@ -122,14 +128,6 @@ public ExamDTO updateExam(@PathVariable String code, @Valid @RequestBody ExamWit return examMapper.map2DTO(examUpdated); } - private void validateExamDefaultResult(ExamDTO examDTO, List examRows) throws OHAPIException { - if (examDTO.getProcedure() == 1 && examDTO.getDefaultResult() != null) { - if ((examRows == null ? Collections.emptyList() : examRows).stream().noneMatch(row -> examDTO.getDefaultResult() == row)) { - throw new OHAPIException(new OHExceptionMessage("Exam default result doesn't match any exam rows.")); - } - } - } - @GetMapping(value = "/exams/description/{description:.+}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getExams(@PathVariable String description) throws OHServiceException { List exams = examMapper.map2DTOList(examManager.getExams(description)); diff --git a/src/test/java/org/isf/exam/data/ExamHelper.java b/src/test/java/org/isf/exam/data/ExamHelper.java new file mode 100644 index 000000000..153fa4806 --- /dev/null +++ b/src/test/java/org/isf/exam/data/ExamHelper.java @@ -0,0 +1,94 @@ +/* + * Open Hospital (www.open-hospital.org) + * Copyright © 2006-2023 Informatici Senza Frontiere (info@informaticisenzafrontiere.org) + * + * Open Hospital is a free and open source software for healthcare data management. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * https://www.gnu.org/licenses/gpl-3.0-standalone.html + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.isf.exam.data; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.isf.exam.dto.ExamDTO; +import org.isf.exam.dto.ExamWithRowsDTO; +import org.isf.exatype.dto.ExamTypeDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +/** + * Helper class to generate DTOs and Entities for users endpoints test + * @author Silevester D. + * @since 1.15 + */ +public class ExamHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExamHelper.class); + + private static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()); + + private static ExamTypeDTO generateExamType() { + ExamTypeDTO examType = new ExamTypeDTO(); + examType.setCode("HB"); + examType.setDescription("1.Haematology"); + return examType; + } + public static ExamDTO generateExam() { + return new ExamDTO("44.02", "1.2 MDR Lite", 1, "NORMAL", generateExamType()); + } + + public static List generateExamList(int count) { + return IntStream.range(1, count + 1).mapToObj(index -> { + ExamDTO exam = generateExam(); + exam.setCode(String.format("44.0%d", index)); + exam.setDescription(String.format("1.0%d MDR Lite 0%d", index, index)); + return exam; + }).collect(Collectors.toList()); + } + + public static List generateExamRows() { + return List.of("NORMAL", "DANGER", "URI"); + } + + public static ExamWithRowsDTO generateExamWithRowsDTO() { + return new ExamWithRowsDTO(generateExam(), ExamHelper.generateExamRows()); + } + + public static ExamWithRowsDTO generateExamWithRowsDTO(ExamDTO examDTO) { + return new ExamWithRowsDTO(examDTO, ExamHelper.generateExamRows()); + } + + // TODO: to be moved in a general package? + public static String asJsonString(T object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + LOGGER.error("Error converting object to JSON", e); + return null; + } + } +} diff --git a/src/test/java/org/isf/exam/rest/ExamControllerTest.java b/src/test/java/org/isf/exam/rest/ExamControllerTest.java new file mode 100644 index 000000000..c0559f2e9 --- /dev/null +++ b/src/test/java/org/isf/exam/rest/ExamControllerTest.java @@ -0,0 +1,217 @@ +/* + * Open Hospital (www.open-hospital.org) + * Copyright © 2006-2024 Informatici Senza Frontiere (info@informaticisenzafrontiere.org) + * + * Open Hospital is a free and open source software for healthcare data management. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * https://www.gnu.org/licenses/gpl-3.0-standalone.html + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.isf.exam.rest; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.isf.OpenHospitalApiApplication; +import org.isf.exa.manager.ExamBrowsingManager; +import org.isf.exa.model.Exam; +import org.isf.exa.service.ExamRowIoOperationRepository; +import org.isf.exam.data.ExamHelper; +import org.isf.exam.dto.ExamDTO; +import org.isf.exam.dto.ExamWithRowsDTO; +import org.isf.exam.mapper.ExamMapper; +import org.isf.exatype.manager.ExamTypeBrowserManager; +import org.isf.exatype.mapper.ExamTypeMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@SpringBootTest(classes = OpenHospitalApiApplication.class) +@AutoConfigureMockMvc +public class ExamControllerTest { + + private final Logger LOGGER = LoggerFactory.getLogger(ExamControllerTest.class); + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ExamMapper examMapper; + + @Autowired + private ExamTypeMapper examTypeMapper; + + @MockBean + private ExamBrowsingManager examManager; + + @MockBean + private ExamTypeBrowserManager examTypeBrowserManager; + + @MockBean + private ExamRowIoOperationRepository examRowIoOperationRepository; + + @Nested + @DisplayName("Create exam") + class CreateExamTests { + + @Test + @DisplayName("Should create a new exam with associated examrows") + @WithMockUser(username = "admin", authorities = { "exams.create", "examrows.create" }) + void shouldCreateExamWithRows() throws Exception { + ExamWithRowsDTO payload = ExamHelper.generateExamWithRowsDTO(); + + when(examManager.create(any(), any())).thenReturn(examMapper.map2Model(payload.exam())); + when(examTypeBrowserManager.findByCode(any())).thenReturn(examTypeMapper.map2Model(payload.exam().getExamtype())); + + var result = mvc.perform( + post("/exams").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(payload))) + .andDo(log()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("description", equalTo(payload.exam().getDescription()))) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + + @Test + @DisplayName("Should fail to create exam procedure 1 with rows when default result doesn't match") + @WithMockUser(username = "admin", authorities = { "exams.create", "examrows.create" }) + void shouldFailToCreateExamWithInvalidDefaultResult() throws Exception { + ExamDTO examDTO = ExamHelper.generateExam(); + examDTO.setDefaultResult("IRES"); + + when(examTypeBrowserManager.findByCode(any())).thenReturn(examTypeMapper.map2Model(examDTO.getExamtype())); + + var result = mvc.perform( + post("/exams").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(ExamHelper.generateExamWithRowsDTO(examDTO)))) + .andDo(log()) + .andExpect(status().isBadRequest()) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + + @Test + @DisplayName("Should fail to create exam when user doesn't have required permissions") + @WithMockUser(username = "admin", authorities = { "examrows.create" }) + void shouldFailToCreateExamWhenInsufficientPermissions() throws Exception { + var result = mvc.perform( + post("/exams").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(ExamHelper.generateExamWithRowsDTO()))) + .andDo(log()) + .andExpect(status().isForbidden()) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + } + + @Nested + @DisplayName("Update exam") + class UpdateExamTests { + + @Test + @DisplayName("Should update an exam with associated examrows") + @WithMockUser(username = "admin", authorities = { "exams.update", "examrows.create", "examrows.delete" }) + void shouldUpdateExamWithRows() throws Exception { + ExamWithRowsDTO payload = ExamHelper.generateExamWithRowsDTO(); + Exam exam = examMapper.map2Model(payload.exam()); + + when(examManager.create(any(), any())).thenReturn(exam); + when(examManager.findByCode(any())).thenReturn(exam); + when(examManager.update(any(), any())).thenReturn(exam); + when(examTypeBrowserManager.findByCode(any())).thenReturn(examTypeMapper.map2Model(payload.exam().getExamtype())); + + var result = mvc.perform( + put("/exams/{code}", payload.exam().getCode()).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(payload))) + .andDo(log()) + .andExpect(status().isOk()) + .andExpect(jsonPath("description", equalTo(payload.exam().getDescription()))) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + + @Test + @DisplayName("Should fail to update exam procedure 1 with rows when default result doesn't match") + @WithMockUser(username = "admin", authorities = { "exams.update", "examrows.create", "examrows.delete" }) + void shouldFailToUpdateExamWithInvalidDefaultResult() throws Exception { + ExamDTO examDTO = ExamHelper.generateExam(); + examDTO.setDefaultResult("IRES"); + Exam exam = examMapper.map2Model(examDTO); + + when(examManager.create(any(), any())).thenReturn(exam); + when(examManager.findByCode(any())).thenReturn(exam); + when(examTypeBrowserManager.findByCode(any())).thenReturn(examTypeMapper.map2Model(examDTO.getExamtype())); + + var result = mvc.perform( + put("/exams/{code}", examDTO.getCode()).contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(ExamHelper.generateExamWithRowsDTO(examDTO)))) + .andDo(log()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("message", containsString("Exam default result doesn't match"))) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + + @Test + @DisplayName("Should fail to update exam code in body doesn't match") + @WithMockUser(username = "admin", authorities = { "exams.update", "examrows.create", "examrows.delete" }) + void shouldFailToUpdateExamWhenCodeInBodyDoesntMatch() throws Exception { + var result = mvc.perform( + put("/exams/{code}", "DD").contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(ExamHelper.generateExamWithRowsDTO()))) + .andDo(log()) + .andExpect(status().isBadRequest()) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + + @Test + @DisplayName("Should fail to update exam when user doesn't have required permissions") + @WithMockUser(username = "admin", authorities = { "examrows.create", "examrows.delete" }) + void shouldFailToUpdateExamWhenInsufficientPermissions() throws Exception { + var result = mvc.perform( + put("/exams/hd").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(ExamHelper.generateExamWithRowsDTO()))) + .andDo(log()) + .andExpect(status().isForbidden()) + .andReturn(); + + LOGGER.debug("result: {}", result); + } + } +} From e876f5e0daac3e50496e6ed2eb31511addbe7d8a Mon Sep 17 00:00:00 2001 From: SteveGT96 Date: Tue, 15 Oct 2024 16:01:48 +0100 Subject: [PATCH 3/5] chore: Remove value argument in RestController annotation --- src/main/java/org/isf/exam/rest/ExamController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/isf/exam/rest/ExamController.java b/src/main/java/org/isf/exam/rest/ExamController.java index fa3ca3cc9..8db5568bf 100644 --- a/src/main/java/org/isf/exam/rest/ExamController.java +++ b/src/main/java/org/isf/exam/rest/ExamController.java @@ -54,7 +54,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "Exams") -@RestController(value = "/exams") +@RestController @SecurityRequirement(name = "bearerAuth") public class ExamController { From 912de97bdbdd003129b75e10b216c4643a5f1cdc Mon Sep 17 00:00:00 2001 From: SteveGT96 Date: Tue, 15 Oct 2024 16:17:56 +0100 Subject: [PATCH 4/5] chore: Fix docs --- src/test/java/org/isf/exam/data/ExamHelper.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/java/org/isf/exam/data/ExamHelper.java b/src/test/java/org/isf/exam/data/ExamHelper.java index df701e573..69322d4ef 100644 --- a/src/test/java/org/isf/exam/data/ExamHelper.java +++ b/src/test/java/org/isf/exam/data/ExamHelper.java @@ -38,9 +38,7 @@ import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; /** - * Helper class to generate DTOs and Entities for users endpoints test - * @author Silevester D. - * @since 1.15 + * Helper class to generate DTOs and Entities for exams endpoints test */ public class ExamHelper { From 25e6cd686029422840e364140c0fcb5846e8d7bd Mon Sep 17 00:00:00 2001 From: SteveGT96 Date: Wed, 23 Oct 2024 08:25:46 +0100 Subject: [PATCH 5/5] doc: Update swagger documentation --- openapi/oh.yaml | 8 ++++---- src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openapi/oh.yaml b/openapi/oh.yaml index 59631d500..7ba6a7067 100644 --- a/openapi/oh.yaml +++ b/openapi/oh.yaml @@ -6237,10 +6237,10 @@ components: example: 0 opd: type: boolean - female: - type: boolean male: type: boolean + female: + type: boolean pharmacy: type: boolean PatientDTO: @@ -7571,12 +7571,12 @@ components: type: array description: Possible result for the exam(only for exams with procedure 1 and 2) - example: "['POSITIVE', 'NEGATIVE']" + example: "['>=12 (NORMAL)', 'IRREGULAR']" items: type: string description: Possible result for the exam(only for exams with procedure 1 and 2) - example: "['POSITIVE', 'NEGATIVE']" + example: "['>=12 (NORMAL)', 'IRREGULAR']" PatientExaminationDTO: required: - patientCode diff --git a/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java b/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java index bf5b4f1a5..637193079 100644 --- a/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java +++ b/src/main/java/org/isf/exam/dto/ExamWithRowsDTO.java @@ -28,8 +28,8 @@ import io.swagger.v3.oas.annotations.media.Schema; public record ExamWithRowsDTO( - @NotNull @Schema(name = "exam", description = "The exam to mutate") ExamDTO exam, - @Schema(name = "rows", example = "['POSITIVE', 'NEGATIVE']", description = "Possible result for the exam(only for exams with procedure 1 and 2)") List rows + @NotNull @Schema(name = "exam", description = "The exam to be changed") ExamDTO exam, + @Schema(name = "rows", example = "['>=12 (NORMAL)', 'IRREGULAR']", description = "Possible result for the exam(only for exams with procedure 1 and 2)") List rows ) { }