From 6d4b9b1746ac891adc7a249027c4eb62ba0102f2 Mon Sep 17 00:00:00 2001 From: flawmop Date: Tue, 15 Oct 2024 22:19:29 -0700 Subject: [PATCH] Expand REST operations for Submissions --- build.gradle | 3 +- .../controller/SubmissionController.java | 61 +++++++-- .../controller/advice/ControllerAdvice.java | 12 ++ .../EntityNotAccessibleException.java | 22 ++++ .../exception/FileProcessingException.java | 2 +- .../persistence/entity/Simulation.java | 11 +- .../persistence/entity/Submission.java | 19 ++- .../repository/SimulationRepository.java | 6 + .../service/InputProcessorService.java | 3 - .../service/InputProcessorServiceImpl.java | 5 - .../submission/service/SubmissionService.java | 28 +++- .../service/SubmissionServiceImpl.java | 59 +++++++-- .../controller/SubmissionControllerE2E.java | 120 +++++++++++++----- .../controller/SubmissionControllerIT.java | 47 +++++-- .../controller/SubmissionControllerTest.java | 31 +++-- 15 files changed, 340 insertions(+), 89 deletions(-) create mode 100644 src/main/java/com/insilicosoft/portal/svc/submission/exception/EntityNotAccessibleException.java diff --git a/build.gradle b/build.gradle index 0c0f821..0a0dc30 100644 --- a/build.gradle +++ b/build.gradle @@ -112,6 +112,7 @@ testing { } dependencies { implementation "com.github.dasniko:testcontainers-keycloak:${tcKeycloakVersion}" + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JdbcTemplate, @Transactional implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.security:spring-security-test' implementation 'org.testcontainers:junit-jupiter' @@ -169,4 +170,4 @@ tasks.named('test') { tasks.withType(JavaCompile) { options.encoding = 'UTF-8' -} \ No newline at end of file +} diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/controller/SubmissionController.java b/src/main/java/com/insilicosoft/portal/svc/submission/controller/SubmissionController.java index 2b4375b..dbcd005 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/controller/SubmissionController.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/controller/SubmissionController.java @@ -1,31 +1,39 @@ package com.insilicosoft.portal.svc.submission.controller; import java.io.IOException ; +import java.net.URI; import org.slf4j.Logger ; import org.slf4j.LoggerFactory ; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart ; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.insilicosoft.portal.svc.submission.SubmissionIdentifiers; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; import com.insilicosoft.portal.svc.submission.exception.FileProcessingException; import com.insilicosoft.portal.svc.submission.exception.InputVerificationException; +import com.insilicosoft.portal.svc.submission.persistence.entity.Submission; import com.insilicosoft.portal.svc.submission.service.InputProcessorService; import com.insilicosoft.portal.svc.submission.service.SubmissionService; import io.micrometer.core.annotation.Timed; /** - * Simulation controller + * Submission controller * * @author geoff */ -@Controller +@RestController @RequestMapping(SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION) public class SubmissionController { @@ -46,10 +54,18 @@ public SubmissionController(final InputProcessorService inputProcessorService, this.submissionService = submissionService; } - @GetMapping() + /** + * Retrieve a {@link Submission}. + * + * @param submissionId Submission identifier. + * @return Found submission. + * @throws EntityNotAccessibleException If not found or not retrievable due to security. + */ + @GetMapping(value = "/{id}") @Timed(value = "submission.get", description = "GET request") - public ResponseEntity get() { - return ResponseEntity.ok(inputProcessorService.get()); + public Submission get(final @PathVariable(name = "id") long submissionId) + throws EntityNotAccessibleException { + return submissionService.retrieve(submissionId); } /** @@ -58,14 +74,15 @@ public ResponseEntity get() { * @param file File being uploaded. * @return Response entity. * @throws FileProcessingException If problems processing file. + * @throws InputVerificationException If supplied input is invalid. */ @PostMapping(value = SubmissionIdentifiers.REQUEST_MAPPING_SIMULATION) @Timed(value = "submission.simulation.post", description = "Submit Simulation POST Multipart request") - public ResponseEntity createSimulation(final @RequestPart(required=false, - value=SubmissionIdentifiers.PARAM_NAME_SIMULATION_FILE) - MultipartFile file) - throws FileProcessingException, - InputVerificationException { + // 'required=false' to enable manual checking for null file + public ResponseEntity createSimulation(final @RequestPart(required=false, + value=SubmissionIdentifiers.PARAM_NAME_SIMULATION_FILE) + MultipartFile file) + throws FileProcessingException, InputVerificationException { if (file == null) { final String message = "No Multipart file! Did you supply the parameter '" + SubmissionIdentifiers.PARAM_NAME_SIMULATION_FILE + "' in the POST request?"; @@ -85,11 +102,29 @@ public ResponseEntity createSimulation(final @RequestPart(required=false throw new FileProcessingException(e.getMessage()); } - final long submissionId = submissionService.submit(); + final Submission submission = submissionService.create(); + final Long submissionId = submission.getEntityId(); inputProcessorService.process(submissionId, fileByteArray); - return ResponseEntity.ok(String.valueOf(submissionId)); + final URI location = ServletUriComponentsBuilder.fromCurrentRequestUri() + .path("/{submissionId}") + .buildAndExpand(submissionId) + .toUri(); + + return ResponseEntity.created(location).build(); } + /** + * Delete a {@link Submission}. + * + * @param id Identifier of submission to delete. + * @throws EntityNotAccessibleException If not found or not retrievable due to security. + */ + @DeleteMapping(value = "/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + @Timed(value = "submission.delete", description = "DELETE request") + public void delete(final @PathVariable(name = "id") long id) throws EntityNotAccessibleException { + submissionService.delete(id); + } } \ No newline at end of file diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/controller/advice/ControllerAdvice.java b/src/main/java/com/insilicosoft/portal/svc/submission/controller/advice/ControllerAdvice.java index 5edae43..b4fe6d4 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/controller/advice/ControllerAdvice.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/controller/advice/ControllerAdvice.java @@ -7,6 +7,7 @@ import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.multipart.MultipartException; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; import com.insilicosoft.portal.svc.submission.exception.FileProcessingException; import com.insilicosoft.portal.svc.submission.exception.InputVerificationException; @@ -18,6 +19,17 @@ @RestControllerAdvice public class ControllerAdvice { + /** + * Something we're going to handle. + * + * @param e Entity not accessible exception. + * @return Response entity. + */ + @ExceptionHandler(EntityNotAccessibleException.class) + public ResponseEntity handleEntityNotAccessible(EntityNotAccessibleException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + /** * Something we're going to handle. * diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/exception/EntityNotAccessibleException.java b/src/main/java/com/insilicosoft/portal/svc/submission/exception/EntityNotAccessibleException.java new file mode 100644 index 0000000..e1d6d7b --- /dev/null +++ b/src/main/java/com/insilicosoft/portal/svc/submission/exception/EntityNotAccessibleException.java @@ -0,0 +1,22 @@ +package com.insilicosoft.portal.svc.submission.exception; + +/** + * Exception thrown when entity not found or not visible to user. + */ +public class EntityNotAccessibleException extends Exception { + + private static final long serialVersionUID = 6311364080916164926L; + + private static final String message = "%s with identifier '%s' was not found"; + + /** + * Initialising constructor. + * + * @param entity Entity name. + * @param id Entity identifier. + */ + public EntityNotAccessibleException(final String entity, final String id) { + super(String.format(message, entity, id)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/exception/FileProcessingException.java b/src/main/java/com/insilicosoft/portal/svc/submission/exception/FileProcessingException.java index b4d4297..1025ed0 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/exception/FileProcessingException.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/exception/FileProcessingException.java @@ -10,7 +10,7 @@ */ public class FileProcessingException extends Exception { - private static final long serialVersionUID = - 8135834798094531867L ; + private static final long serialVersionUID = -8135834798094531867L; /** * Initialising constructor. diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Simulation.java b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Simulation.java index 1084e3e..201dd79 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Simulation.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Simulation.java @@ -21,6 +21,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -61,13 +62,17 @@ public class Simulation { @Column(nullable = false) private BigDecimal pacingMaxTime; - @ElementCollection - @CollectionTable(name = "simulation_plasmapoints", joinColumns = @JoinColumn(name = "entityId")) + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "simulation_plasmapoints", + foreignKey = @ForeignKey(name = "simulation_fk"), + joinColumns = @JoinColumn(name = "entityId")) @Column(nullable = false) private List plasmaPoints = new ArrayList<>(); @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "simulation_message", joinColumns = @JoinColumn(name = "entityId")) + @CollectionTable(name = "simulation_message", + foreignKey = @ForeignKey(name = "simulation_fk"), + joinColumns = @JoinColumn(name = "entityId")) @Column(nullable = false) private Set messages = new HashSet<>(); diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Submission.java b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Submission.java index c0ad3ab..1634c9d 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Submission.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/entity/Submission.java @@ -21,6 +21,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -44,7 +45,10 @@ public class Submission { private State state; @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "submission_message", joinColumns = @JoinColumn(name = "entityId")) + @CollectionTable(name = "submission_message", + foreignKey = @ForeignKey(name = "simulation_fk"), + joinColumns = @JoinColumn(name = "entityId")) + @Column(nullable = false) private Set messages = new HashSet<>(); // See JPA auditing @@ -109,12 +113,21 @@ public Long getEntityId() { return entityId; } + /** + * Retrieve the Submission state. + * + * @return the state + */ + public State getState() { + return state; + } + // Boilerplate implementations @Override public String toString() { - return "Submission [entityId=" + entityId + ", state=" + state + ", createdDate=" + createdDate + ", createdBy=" - + createdBy + "]"; + return "Submission [entityId=" + entityId + ", state=" + state + ", messages=" + messages + ", createdDate=" + + createdDate + ", createdBy=" + createdBy + "]"; } } \ No newline at end of file diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/repository/SimulationRepository.java b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/repository/SimulationRepository.java index 476cfc8..2527dc2 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/persistence/repository/SimulationRepository.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/persistence/repository/SimulationRepository.java @@ -1,6 +1,8 @@ package com.insilicosoft.portal.svc.submission.persistence.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import com.insilicosoft.portal.svc.submission.persistence.entity.Simulation; @@ -11,4 +13,8 @@ */ public interface SimulationRepository extends JpaRepository { + @Modifying + @Query("DELETE FROM Simulation WHERE submissionId = ?1") + void deleteAllBySubmissionId(long submissionId); + } \ No newline at end of file diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorService.java b/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorService.java index d1c60c8..7c00940 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorService.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorService.java @@ -11,9 +11,6 @@ */ public interface InputProcessorService { - // TODO Remove - String get(); - /** * Process a file. * diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorServiceImpl.java b/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorServiceImpl.java index 3bf7196..b95a60e 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorServiceImpl.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/service/InputProcessorServiceImpl.java @@ -68,11 +68,6 @@ public InputProcessorServiceImpl(final SimulationRepository simulationRepository this.submissionService = submissionService; } - @Override - public String get() { - return "All good from SubmissionController->InputProcessorService!!"; - } - @Override public void process(final long submissionId, final byte[] file) throws FileProcessingException, InputVerificationException { diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionService.java b/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionService.java index df879b2..e55af83 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionService.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionService.java @@ -3,14 +3,32 @@ import java.util.Map; import java.util.Set; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; import com.insilicosoft.portal.svc.submission.persistence.entity.Message; import com.insilicosoft.portal.svc.submission.persistence.entity.Submission; /** - * User {@link Submission} service. + * User {@link Submission} service. + * + * @author geoff */ public interface SubmissionService { + /** + * Creates a new {@link Submission}. + * + * @return Submission. + */ + Submission create(); + + /** + * Delete the {@link Submission} (and all linked entities!). + * + * @param submissionId Submission identifier. + * @throws EntityNotAccessibleException If identified Submission not accessible. + */ + void delete(long submissionId) throws EntityNotAccessibleException; + /** * Reject the {@link Submission} due to file processing problems. * @@ -28,10 +46,12 @@ public interface SubmissionService { void rejectOnInvalidInput(long submissionId, Map> problems); /** - * {@link Submission} of some kind. + * Retrieve the {@link Submission} identified by the {@literal submissionId}. * - * @return Submission identifier. + * @param submissionId Submission identifier. + * @return Submission + * @throws EntityNotAccessibleException If identified Submission not accessible. */ - long submit(); + Submission retrieve(long submissionId) throws EntityNotAccessibleException; } \ No newline at end of file diff --git a/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionServiceImpl.java b/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionServiceImpl.java index 6694971..77edb1d 100644 --- a/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionServiceImpl.java +++ b/src/main/java/com/insilicosoft/portal/svc/submission/service/SubmissionServiceImpl.java @@ -3,44 +3,87 @@ import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; import com.insilicosoft.portal.svc.submission.persistence.entity.Message; import com.insilicosoft.portal.svc.submission.persistence.entity.Submission; +import com.insilicosoft.portal.svc.submission.persistence.repository.SimulationRepository; import com.insilicosoft.portal.svc.submission.persistence.repository.SubmissionRepository; +import jakarta.transaction.Transactional; + /** * Submission service implementation. + * + * @author geoff */ @Service public class SubmissionServiceImpl implements SubmissionService { + private static final Logger log = LoggerFactory.getLogger(SubmissionServiceImpl.class); + + private final SimulationRepository simulationRepository; private final SubmissionRepository submissionRepository; - public SubmissionServiceImpl(final SubmissionRepository submissionRepository) { + /** + * Initialising constructor. + * + * @param submissionRepository Submission repository.. + * @param simulationRepository Simulation repository. + */ + public SubmissionServiceImpl(final SubmissionRepository submissionRepository, + final SimulationRepository simulationRepository) { this.submissionRepository = submissionRepository; + this.simulationRepository = simulationRepository; + } + + @Override + public Submission create() { + log.debug("~create() : Invoked."); + + return submissionRepository.save(new Submission()); + } + + @Override + @Transactional + public void delete(final long submissionId) throws EntityNotAccessibleException { + log.debug("~delete() : Invoked for id {}", submissionId); + + Submission submission = submissionRepository.findById(submissionId) + .orElseThrow(() -> new EntityNotAccessibleException("Submission", + String.valueOf(submissionId))); + simulationRepository.deleteAllBySubmissionId(submissionId); + submissionRepository.delete(submission); } - @Override - public void rejectOnFileProcessing(long submissionId, Message problem) { + public void rejectOnFileProcessing(final long submissionId, final Message problem) { + log.debug("~rejectOnFileProcessing() : Invoked for id {}", submissionId); + Submission submission = submissionRepository.getReferenceById(submissionId); submission.rejectWithProblem(problem); submissionRepository.save(submission); } - @Override - public void rejectOnInvalidInput(final long submissionId, Map> problems) { + public void rejectOnInvalidInput(final long submissionId, final Map> problems) { + log.debug("~rejectOnInvalidInput() : Invoked for id {}", submissionId); + Submission submission = submissionRepository.getReferenceById(submissionId); submission.rejectWithProblems(problems); submissionRepository.save(submission); } @Override - public long submit() { - final Submission saved = submissionRepository.save(new Submission()); - return saved.getEntityId(); + public Submission retrieve(final long submissionId) throws EntityNotAccessibleException { + log.debug("~retrieve() : Invoked for id {}", submissionId); + + return submissionRepository.findById(submissionId) + .orElseThrow(() -> new EntityNotAccessibleException("Submission", + String.valueOf(submissionId))); } } \ No newline at end of file diff --git a/src/test-e2e/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerE2E.java b/src/test-e2e/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerE2E.java index cb23f10..51a52a6 100644 --- a/src/test-e2e/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerE2E.java +++ b/src/test-e2e/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerE2E.java @@ -1,25 +1,30 @@ package com.insilicosoft.portal.svc.submission.controller; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; +import static org.springframework.web.reactive.function.BodyInserters.fromMultipartData; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; - +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -34,9 +39,11 @@ @Testcontainers public class SubmissionControllerE2E { + private static final HttpHeaders httpHeaders = new HttpHeaders(); + private static final MediaType applicationJson = new MediaType(MediaType.APPLICATION_JSON); + private static final MediaType textWithCharset = new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8); private static final String message = "No Multipart file! Did you supply the parameter '" + SubmissionIdentifiers.PARAM_NAME_SIMULATION_FILE + "' in the POST request?"; private static final String postUrl = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION.concat(SubmissionIdentifiers.REQUEST_MAPPING_SIMULATION); - private static final HttpHeaders httpHeaders = new HttpHeaders(); private static final String goodRequestFileName = "request_good.json"; private static final Path goodPath = Path.of("src", "test", "resources", "requests", goodRequestFileName); @@ -46,6 +53,12 @@ public class SubmissionControllerE2E { httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); } + @LocalServerPort + private int port; + + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired private WebTestClient webTestClient; @@ -54,13 +67,20 @@ public class SubmissionControllerE2E { private static final KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:19.0") .withRealmImportFile("keycloak/test-realm-config.json"); - @DynamicPropertySource static void dynamicProperties(DynamicPropertyRegistry registry) { registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri", () -> keycloak.getAuthServerUrl() + "realms/PolarBookshop"); } + @AfterEach + void afterEach() { + assertThat(JdbcTestUtils.countRowsInTable(jdbcTemplate, "simulation")).isEqualTo(0); + assertThat(JdbcTestUtils.countRowsInTable(jdbcTemplate, "simulation_message")).isEqualTo(0); + assertThat(JdbcTestUtils.countRowsInTable(jdbcTemplate, "simulation_plasmapoints")).isEqualTo(0); + assertThat(JdbcTestUtils.countRowsInTable(jdbcTemplate, "submission")).isEqualTo(0); + } + @BeforeAll static void generateAccessTokens() { WebClient webClient = WebClient.builder().baseUrl(keycloak.getAuthServerUrl() + "realms/PolarBookshop/protocol/openid-connect/token") @@ -72,19 +92,42 @@ static void generateAccessTokens() { @DisplayName("Test GET method(s)") @Nested class GetMethods { - @DisplayName("Success") + @DisplayName("Fail on Submission not found") @Test - void success() { + void failOnSubmissionNotFound() { + var submissionId = 1l; + var url = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION + "/" + String.valueOf(submissionId); + webTestClient.get() - .uri(SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION) + .uri(url) .headers(headers -> { headers.setBearerAuth(bjornTokens.accessToken); }) .exchange() - .expectStatus().isOk() - .expectBody(String.class).value(body -> { - assertThat(body).isEqualTo("All good from SubmissionController->InputProcessorService!!"); - }); + .expectStatus().isNotFound() + .expectHeader().contentType(textWithCharset) + .expectBody(String.class).isEqualTo("Submission with identifier '1' was not found"); + } + } + + @DisplayName("Test DELETE method(s)") + @Nested + class DeleteMethods { + @DisplayName("Fail on Submission not found") + @Test + void failOnSubmissionNotFound() { + var submissionId = 1l; + var url = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION + "/{id}"; + + webTestClient.delete() + .uri(url, submissionId) + .headers(headers -> { + headers.setBearerAuth(bjornTokens.accessToken); + }) + .exchange() + .expectStatus().isNotFound() + .expectHeader().contentType(textWithCharset) + .expectBody(String.class).isEqualTo("Submission with identifier '1' was not found"); } } @@ -114,9 +157,7 @@ void failOnMultipartException() { }) .exchange() .expectStatus().is5xxServerError() - .expectBody(String.class).value(body -> { - assertThat(body).isEqualTo("Error occurred during file upload - MultipartException"); - }); + .expectBody(String.class).isEqualTo("Error occurred during file upload - MultipartException"); } @DisplayName("Fail on expected request param not supplied") @@ -129,17 +170,21 @@ void failOnBadParamName() { .headers(headers -> { headers.setBearerAuth(bjornTokens.accessToken); }) - .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build())) + .body(fromMultipartData(multipartBodyBuilder.build())) .exchange() .expectStatus().isBadRequest() - .expectBody(String.class).value(body -> { - assertThat(body).isEqualTo(message); - }); + .expectBody(String.class).isEqualTo(message); } + } - @DisplayName("Success on a good simulations request file") + @DisplayName("Test Submission lifecycle") + @Nested + class SubmissionLifecycle { + @DisplayName("Success on Submission lifecycle") @Test - void success() { + void successOnSubmissionLifecycle() { + var submissionId = 1l; + var url = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION + "/{id}"; var multipartBodyBuilder = new MultipartBodyBuilder(); multipartBodyBuilder.part(SubmissionIdentifiers.PARAM_NAME_SIMULATION_FILE, new FileSystemResource(goodPath)); @@ -148,22 +193,39 @@ void success() { .headers(headers -> { headers.setBearerAuth(bjornTokens.accessToken); }) - .body(BodyInserters.fromMultipartData(multipartBodyBuilder.build())) + .body(fromMultipartData(multipartBodyBuilder.build())) .exchange() - .expectStatus().isOk() - .expectBody(String.class).value(body -> { - assertThat(body).isEqualTo("1"); - }); + .expectStatus().isCreated() + .expectHeader().location("http://localhost:" + port + postUrl + "/" + submissionId) + .expectBody().isEmpty(); + webTestClient.get() + .uri(url, submissionId) + .accept(MediaType.APPLICATION_JSON) + .headers(headers -> { + headers.setBearerAuth(bjornTokens.accessToken); + }) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(applicationJson) + .expectBody().jsonPath("$.entityId").isEqualTo(submissionId); + + webTestClient.delete() + .uri(url, submissionId) + .headers(headers -> { + headers.setBearerAuth(bjornTokens.accessToken); + }) + .exchange() + .expectStatus().isNoContent() + .expectBody().isEmpty(); } } private static KeycloakToken authenticateWith(String username, String password, WebClient webClient) { return webClient.post() - .body(BodyInserters.fromFormData("grant_type", "password") - .with("client_id", "polar-test") - .with("username", username) - .with("password", password)) + .body(fromFormData("grant_type", "password").with("client_id", "polar-test") + .with("username", username) + .with("password", password)) .retrieve() .bodyToMono(KeycloakToken.class) .block(); diff --git a/src/test-i/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerIT.java b/src/test-i/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerIT.java index e86b485..a32bf8e 100644 --- a/src/test-i/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerIT.java +++ b/src/test-i/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerIT.java @@ -4,8 +4,9 @@ import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.nio.charset.StandardCharsets; @@ -25,6 +26,9 @@ import com.insilicosoft.portal.svc.submission.SubmissionIdentifiers; import com.insilicosoft.portal.svc.submission.config.SecurityConfig; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; +import com.insilicosoft.portal.svc.submission.persistence.State; +import com.insilicosoft.portal.svc.submission.persistence.entity.Submission; import com.insilicosoft.portal.svc.submission.service.InputProcessorService; import com.insilicosoft.portal.svc.submission.service.SubmissionService; @@ -32,6 +36,7 @@ @Import(SecurityConfig.class) public class SubmissionControllerIT { + private static final MediaType applicationJson = new MediaType(MediaType.APPLICATION_JSON); private static final MediaType textWithCharset = new MediaType(MediaType.TEXT_PLAIN, StandardCharsets.UTF_8); private static final GrantedAuthority userRole = new SimpleGrantedAuthority("ROLE_".concat(SubmissionIdentifiers.ROLE_USER)); @@ -48,21 +53,41 @@ public class SubmissionControllerIT { @DisplayName("Test GET method(s)") @Nested class GetMethods { - @DisplayName("Success") + @DisplayName("Fail on Submission not found") @Test - void testGet() throws Exception { - final String getMessage = "Message from get!"; + void failOnSubmissionNotFound() throws Exception { + var submissionId = 1l; + var url = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION + "/" + String.valueOf(submissionId); - when(mockInputProcessorService.get()).thenReturn(getMessage); + when(mockSubmissionService.retrieve(submissionId)) + .thenThrow(new EntityNotAccessibleException("Entity", "1")); - mockMvc.perform(get(SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION).with(jwt().authorities(userRole))) - //.andDo(print()) - .andExpect(status().isOk()) + mockMvc.perform(get(url).with(jwt().authorities(userRole))) + .andDo(print()) + .andExpect(status().isNotFound()) .andExpect(content().contentType(textWithCharset)) - .andExpect(content().string(getMessage)); + .andExpect(content().string("Entity with identifier '1' was not found")); + + verify(mockSubmissionService).retrieve(submissionId); + } + + @DisplayName("Success on Submission found") + @Test + void successOnSubmissionFound() throws Exception { + var submissionId = 1l; + var url = SubmissionIdentifiers.REQUEST_MAPPING_SUBMISSION + "/" + String.valueOf(submissionId); + + when(mockSubmissionService.retrieve(submissionId)).thenReturn(new Submission()); + + mockMvc.perform(get(url).with(jwt().authorities(userRole))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(applicationJson)) + .andExpect(jsonPath("$.entityId").isEmpty()) + .andExpect(jsonPath("$.state").value(State.CREATED.toString())); - verify(mockInputProcessorService).get(); + verify(mockSubmissionService).retrieve(submissionId); } } -} \ No newline at end of file +} diff --git a/src/test/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerTest.java b/src/test/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerTest.java index 9082e01..5da3e79 100644 --- a/src/test/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerTest.java +++ b/src/test/java/com/insilicosoft/portal/svc/submission/controller/SubmissionControllerTest.java @@ -9,6 +9,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.net.URI; +import java.net.URISyntaxException; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -18,12 +21,18 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import com.insilicosoft.portal.svc.submission.SubmissionIdentifiers; +import com.insilicosoft.portal.svc.submission.exception.EntityNotAccessibleException; import com.insilicosoft.portal.svc.submission.exception.FileProcessingException; import com.insilicosoft.portal.svc.submission.exception.InputVerificationException; +import com.insilicosoft.portal.svc.submission.persistence.entity.Submission; import com.insilicosoft.portal.svc.submission.service.InputProcessorService; import com.insilicosoft.portal.svc.submission.service.SubmissionService; @@ -42,11 +51,14 @@ public class SubmissionControllerTest { @Mock private InputProcessorService mockInputProcessorService; @Mock + private Submission mockSubmission; + @Mock private SubmissionService mockSubmissionService; @BeforeEach void setUp() { controller = new SubmissionController(mockInputProcessorService, mockSubmissionService); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); } @DisplayName("Test GET method(s)") @@ -54,14 +66,14 @@ void setUp() { class GetMethods { @DisplayName("Success") @Test - void success() { - var message = "Test get message"; + void success() throws EntityNotAccessibleException { + var submissionId = 1l; - when(mockInputProcessorService.get()).thenReturn(message); + when(mockSubmissionService.retrieve(submissionId)).thenReturn(null); - var response = controller.get(); + var submission = controller.get(submissionId); - assertThat(response.getBody()).isEqualTo(message); + assertThat(submission).isNull(); } } @@ -79,20 +91,23 @@ void failOnBadParameterNaming() { @DisplayName("Success on upload") @Test - void failOn() throws FileProcessingException, InputVerificationException { + void successOnUpload() throws FileProcessingException, InputVerificationException, + URISyntaxException { var bytes = "Hello, World!".getBytes(); var fileName = "request.json"; MockMultipartFile file = new MockMultipartFile("file", fileName, MediaType.TEXT_PLAIN_VALUE, bytes); var submissionEntityId = 1l; - when(mockSubmissionService.submit()).thenReturn(submissionEntityId); + when(mockSubmissionService.create()).thenReturn(mockSubmission); + when(mockSubmission.getEntityId()).thenReturn(submissionEntityId); doNothing().when(mockInputProcessorService).process(anyLong(), any(byte[].class)); var response = controller.createSimulation(file); verify(mockInputProcessorService, only()).process(captorLong.capture(), captorBytes.capture()); - assertThat(response.getBody()).isEqualTo(String.valueOf(submissionEntityId)); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getHeaders().getLocation()).isEqualTo(new URI("http://localhost/1")); assertThat(captorLong.getValue()).isSameAs(submissionEntityId); assertThat(captorBytes.getValue()).isEqualTo(bytes); }