diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index 855392f6c..6fa9ab1de 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -1,8 +1,8 @@ package org.finos.vuu.layoutserver.config; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.service.LayoutService; @@ -20,14 +20,14 @@ public class MappingConfig { public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); - mapper.typeMap(LayoutRequestDTO.class, Layout.class) + mapper.typeMap(LayoutRequestDto.class, Layout.class) .addMappings(m -> m.skip(Layout::setId)); - mapper.typeMap(Metadata.class, MetadataResponseDTO.class) + mapper.typeMap(Metadata.class, MetadataResponseDto.class) .addMappings(m -> m.map( metadata -> layoutService.getLayoutByMetadataId(metadata.getId()), - MetadataResponseDTO::setLayoutId)); + MetadataResponseDto::setLayoutId)); return mapper; } -} \ No newline at end of file +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java new file mode 100644 index 000000000..cec6ed70a --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -0,0 +1,55 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.modelmapper.ModelMapper; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/application-layouts") +public class ApplicationLayoutController { + + private final ApplicationLayoutService service; + private final ModelMapper mapper; + + /** + * Gets the persisted application layout for the requesting user. If the requesting user does not have an + * application layout persisted, a default layout with a null username is returned instead. No more than one + * application layout can be persisted for a given user. + * + * @return the application layout + */ + @ResponseStatus(HttpStatus.OK) + @GetMapping + public ApplicationLayoutDto getApplicationLayout(@RequestHeader("username") String username) { + return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class); + } + + /** + * Creates or updates the unique application layout for the requesting user. + * + * @param layoutDefinition JSON representation of the application layout to be created + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.CREATED) + @PutMapping + public void persistApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { + service.persistApplicationLayout(username, layoutDefinition); + } + + /** + * Deletes the application layout for the requesting user. A 404 will be returned if there is no existing + * application layout. + * + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping + public void deleteApplicationLayout(@RequestHeader("username") String username) { + service.deleteApplicationLayout(username); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 3de15fa14..9d5c36fec 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,27 +1,20 @@ package org.finos.vuu.layoutserver.controller; -import java.util.List; -import java.util.UUID; -import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; import org.finos.vuu.layoutserver.service.MetadataService; import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; @RequiredArgsConstructor @RestController @@ -41,8 +34,8 @@ public class LayoutController { */ @ResponseStatus(HttpStatus.OK) @GetMapping("/{id}") - public LayoutResponseDTO getLayout(@PathVariable UUID id) { - return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); + public LayoutResponseDto getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), LayoutResponseDto.class); } /** @@ -52,11 +45,11 @@ public LayoutResponseDTO getLayout(@PathVariable UUID id) { */ @ResponseStatus(HttpStatus.OK) @GetMapping("/metadata") - public List getMetadata() { + public List getMetadata() { return metadataService.getMetadata() .stream() - .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .map(metadata -> mapper.map(metadata, MetadataResponseDto.class)) .collect(java.util.stream.Collectors.toList()); } @@ -68,12 +61,12 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public LayoutResponseDTO createLayout(@Valid @RequestBody LayoutRequestDTO layoutToCreate) { + public LayoutResponseDto createLayout(@RequestBody @Valid LayoutRequestDto layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); - return mapper.map(createdLayout, LayoutResponseDTO.class); + return mapper.map(createdLayout, LayoutResponseDto.class); } /** @@ -84,7 +77,7 @@ public LayoutResponseDTO createLayout(@Valid @RequestBody LayoutRequestDTO layou */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") - public void updateLayout(@PathVariable UUID id, @Valid @RequestBody LayoutRequestDTO layout) { + public void updateLayout(@PathVariable UUID id, @RequestBody @Valid LayoutRequestDto layout) { Layout newLayout = mapper.map(layout, Layout.class); layoutService.updateLayout(id, newLayout); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java similarity index 89% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java index 60d50af11..6255692b1 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import lombok.Data; @Data -public class LayoutRequestDTO { +public class LayoutRequestDto { /** * The definition of the layout as a string (e.g. stringified JSON structure containing @@ -18,5 +19,5 @@ public class LayoutRequestDTO { @JsonProperty(value = "metadata", required = true) @NotNull(message = "Metadata must not be null") - private MetadataRequestDTO metadata; + private MetadataRequestDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java similarity index 87% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java index 30bb62e9a..abbf99430 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java @@ -5,7 +5,7 @@ import org.finos.vuu.layoutserver.model.BaseMetadata; @Data -public class MetadataRequestDTO { +public class MetadataRequestDto { @JsonUnwrapped BaseMetadata baseMetadata; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java new file mode 100644 index 000000000..2b34c6151 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -0,0 +1,10 @@ +package org.finos.vuu.layoutserver.dto.response; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +@Data +public class ApplicationLayoutDto { + private String username; + private JsonNode definition; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java new file mode 100644 index 000000000..0daf9168c --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java @@ -0,0 +1,24 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Data; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import java.util.Date; +import java.util.List; + +@Data +public class ErrorResponse { + private Date timestamp = new Date(); + private int status; + private String error; + private List messages; + private String path; + + public ErrorResponse(HttpServletRequest request, List messages, HttpStatus status) { + this.status = status.value(); + this.error = status.getReasonPhrase(); + this.path = request.getRequestURI(); + this.messages = messages; + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java similarity index 79% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java index 93d725e2a..9c689833b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java @@ -5,7 +5,7 @@ import java.util.UUID; @Data -public class LayoutResponseDTO { +public class LayoutResponseDto { private UUID id; @@ -15,5 +15,5 @@ public class LayoutResponseDTO { */ private String definition; - private MetadataResponseDTO metadata; + private MetadataResponseDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java similarity index 91% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java index 5efa5abaa..178636558 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java @@ -1,13 +1,14 @@ package org.finos.vuu.layoutserver.dto.response; import com.fasterxml.jackson.annotation.JsonUnwrapped; -import java.time.LocalDate; -import java.util.UUID; import lombok.Data; import org.finos.vuu.layoutserver.model.BaseMetadata; +import java.time.LocalDate; +import java.util.UUID; + @Data -public class MetadataResponseDTO { +public class MetadataResponseDto { private UUID layoutId; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java index b9838c681..e0dd4b5c8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java @@ -1,8 +1,7 @@ package org.finos.vuu.layoutserver.exceptions; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.stream.Collectors; +import org.finos.vuu.layoutserver.dto.response.ErrorResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -10,31 +9,42 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException ex) { - return new ResponseEntity<>(ex.getMessage(), - org.springframework.http.HttpStatus.NOT_FOUND); + public ResponseEntity handleNotFound(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.NOT_FOUND; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); } @ExceptionHandler({ - HttpMessageNotReadableException.class, - MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), - org.springframework.http.HttpStatus.BAD_REQUEST); + HttpMessageNotReadableException.class, + MethodArgumentTypeMismatchException.class}) + public ResponseEntity handleBadRequest(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.BAD_REQUEST; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + public ResponseEntity handleMethodArgumentNotValid(HttpServletRequest request, MethodArgumentNotValidException ex) { + HttpStatus status = HttpStatus.BAD_REQUEST; List errors = ex.getFieldErrors() - .stream() - .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) - .collect(Collectors.toList()); + .stream() + .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) + .collect(Collectors.toList()); + return new ResponseEntity<>(new ErrorResponse(request, errors, status), status); + } + + @ExceptionHandler(InternalServerErrorException.class) + public ResponseEntity handleInternalServerError(HttpServletRequest request, Exception ex) { + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return new ResponseEntity<>(new ErrorResponse(request, List.of(ex.getMessage()), status), status); - return new ResponseEntity<>(errors.toString(), - org.springframework.http.HttpStatus.BAD_REQUEST); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java new file mode 100644 index 000000000..b1164eab6 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java @@ -0,0 +1,7 @@ +package org.finos.vuu.layoutserver.exceptions; + +public class InternalServerErrorException extends RuntimeException { + public InternalServerErrorException(String message) { + super(message); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java new file mode 100644 index 000000000..a8cef5ad6 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -0,0 +1,25 @@ +package org.finos.vuu.layoutserver.model; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.utils.JsonNodeConverter; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Data +@Entity +@RequiredArgsConstructor +@AllArgsConstructor +public class ApplicationLayout { + @Id + private String username; + + @Convert(converter = JsonNodeConverter.class) + @Column(columnDefinition = "JSON") + private JsonNode definition; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java index 05389012b..2500eb247 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java @@ -1,8 +1,9 @@ package org.finos.vuu.layoutserver.model; +import lombok.Data; + import javax.persistence.Embeddable; import javax.persistence.Lob; -import lombok.Data; @Data @Embeddable diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 6251cbf2a..29963bd55 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -1,16 +1,10 @@ package org.finos.vuu.layoutserver.model; -import java.util.UUID; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; import lombok.Data; +import javax.persistence.*; +import java.util.UUID; + @Data @Entity public class Layout { diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 604779f23..90c64f70b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,18 +1,14 @@ package org.finos.vuu.layoutserver.model; -import java.time.LocalDate; -import java.util.UUID; -import javax.persistence.Column; -import javax.persistence.Embedded; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.persistence.*; +import java.time.LocalDate; +import java.util.UUID; + @Data @Builder @AllArgsConstructor diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java new file mode 100644 index 000000000..c553e7751 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java @@ -0,0 +1,9 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ApplicationLayoutRepository extends CrudRepository { +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java index 50cbe6288..03f81b108 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java @@ -1,9 +1,10 @@ package org.finos.vuu.layoutserver.repository; -import java.util.UUID; import org.finos.vuu.layoutserver.model.Metadata; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.UUID; + @Repository public interface MetadataRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java new file mode 100644 index 000000000..8c9a2c622 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -0,0 +1,41 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.NoSuchElementException; + +@RequiredArgsConstructor +@Service +public class ApplicationLayoutService { + + private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private final ApplicationLayoutRepository repository; + private final DefaultApplicationLayoutLoader defaultLoader; + + public void persistApplicationLayout(String username, JsonNode layoutDefinition) { + repository.save(new ApplicationLayout(username, layoutDefinition)); + } + + public ApplicationLayout getApplicationLayout(String username) { + return repository.findById(username).orElseGet(() -> { + logger.info("No application layout for user, returning default"); + return defaultLoader.getDefaultLayout(); + }); + } + + public void deleteApplicationLayout(String username) { + try { + repository.deleteById(username); + } catch (EmptyResultDataAccessException e) { + throw new NoSuchElementException("No layout found for user: " + username); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 26dca49f5..0c6717c77 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -1,19 +1,21 @@ package org.finos.vuu.layoutserver.service; -import java.time.LocalDate; -import java.util.NoSuchElementException; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.util.NoSuchElementException; +import java.util.UUID; + @RequiredArgsConstructor @Service public class LayoutService { private final LayoutRepository layoutRepository; + private final MetadataService metadataService; public Layout getLayout(UUID id) { return layoutRepository.findById(id) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java index de3eb095e..08398edc4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.service; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; + @RequiredArgsConstructor @Service public class MetadataService { diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java new file mode 100644 index 000000000..0921acebf --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java @@ -0,0 +1,40 @@ +package org.finos.vuu.layoutserver.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class DefaultApplicationLayoutLoader { + private static final String DEFAULT_LAYOUT_FILE = "defaultApplicationLayout.json"; + private static ApplicationLayout defaultLayout; + + @Bean + public ApplicationLayout getDefaultLayout() { + if (defaultLayout == null) { + loadDefaultLayout(); + } + return defaultLayout; + } + + private void loadDefaultLayout() { + JsonNode definition = loadDefaultLayoutJsonFile(); + defaultLayout = new ApplicationLayout(null, definition); + } + + private JsonNode loadDefaultLayoutJsonFile() { + ObjectMapper objectMapper = new ObjectMapper(); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); + try { + return objectMapper.readTree(resource.getInputStream()); + } catch (IOException e) { + throw new InternalServerErrorException("Failed to read default application layout"); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java new file mode 100644 index 000000000..1984aa56b --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java @@ -0,0 +1,43 @@ +package org.finos.vuu.layoutserver.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.AttributeConverter; +import java.io.IOException; + +public class JsonNodeConverter implements AttributeConverter { + private static final Logger logger = LoggerFactory.getLogger(JsonNodeConverter.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(JsonNode definition) { + try { + return objectMapper.writeValueAsString(definition); + } catch (final JsonProcessingException e) { + logger.error("JSON writing error", e); + return null; + } + } + + @Override + public JsonNode convertToEntityAttribute(String definition) { + try { + return objectMapper.readValue(extractDefinition(definition), new TypeReference<>() {}); + } catch (final IOException e) { + logger.error("JSON reading error", e); + return null; + } + } + + private String extractDefinition(String definition) { + if (definition.startsWith("\"") && definition.endsWith("\"")) { + definition = definition.substring(1, definition.length() - 1); + } + return definition.replaceAll("\\\\", ""); + } +} diff --git a/layout-server/src/main/resources/defaultApplicationLayout.json b/layout-server/src/main/resources/defaultApplicationLayout.json new file mode 100644 index 000000000..871b11b44 --- /dev/null +++ b/layout-server/src/main/resources/defaultApplicationLayout.json @@ -0,0 +1,22 @@ +{ + "id": "main-tabs", + "type": "Stack", + "props": { + "className": "vuuShell-mainTabs", + "TabstripProps": { + "allowAddTab": true, + "allowRenameTab": true, + "animateSelectionThumb": false, + "className": "vuuShellMainTabstrip", + "location": "main-tab" + }, + "preserve": true, + "active": 0 + }, + "children": [ + { + "type": "Placeholder", + "title": "Page 1" + } + ] +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java new file mode 100644 index 000000000..24284d2aa --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -0,0 +1,63 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.modelmapper.ModelMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class ApplicationLayoutControllerTest { + private static ApplicationLayoutService mockService; + private static ApplicationLayoutController controller; + private static final ModelMapper modelMapper = new ModelMapper(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void setup() { + mockService = Mockito.mock(ApplicationLayoutService.class); + controller = new ApplicationLayoutController(mockService, modelMapper); + } + + @Test + public void getApplicationLayout_anyUsername_returnsLayoutFromService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + when(mockService.getApplicationLayout(user)) + .thenReturn(new ApplicationLayout(user, definition)); + + ApplicationLayoutDto response = controller.getApplicationLayout(user); + + assertThat(response.getUsername()).isEqualTo(user); + assertThat(response.getDefinition()).isEqualTo(definition); + + verify(mockService, times(1)).getApplicationLayout(user); + } + + @Test + public void persistApplicationLayout_anyInput_callsService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + controller.persistApplicationLayout(user, definition); + + verify(mockService, times(1)).persistApplicationLayout(user, definition); + } + + @Test + public void deleteApplicationLayout_anyUsername_callsService() { + String user = "user"; + + controller.deleteApplicationLayout(user); + + verify(mockService, times(1)).deleteApplicationLayout(user); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 6c9ecb75f..414dcb1b1 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -1,17 +1,9 @@ package org.finos.vuu.layoutserver.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.NoSuchElementException; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -25,6 +17,15 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class LayoutControllerTest { @@ -52,9 +53,9 @@ class LayoutControllerTest { private Layout layout; private Metadata metadata; private BaseMetadata baseMetadata; - private LayoutRequestDTO layoutRequest; - private LayoutResponseDTO expectedLayoutResponse; - private MetadataResponseDTO metadataResponse; + private LayoutRequestDto layoutRequest; + private LayoutResponseDto expectedLayoutResponse; + private MetadataResponseDto metadataResponse; @BeforeEach public void setup() { @@ -71,15 +72,15 @@ public void setup() { layout.setDefinition(LAYOUT_DEFINITION); layout.setMetadata(metadata); - layoutRequest = new LayoutRequestDTO(); - MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO(); - metadataRequestDTO.setBaseMetadata(baseMetadata); + layoutRequest = new LayoutRequestDto(); + MetadataRequestDto metadataRequestDto = new MetadataRequestDto(); + metadataRequestDto.setBaseMetadata(baseMetadata); layoutRequest.setDefinition(layout.getDefinition()); - layoutRequest.setMetadata(metadataRequestDTO); + layoutRequest.setMetadata(metadataRequestDto); - metadataResponse = getMetadataResponseDTO(); + metadataResponse = getMetadataResponseDto(); - expectedLayoutResponse = new LayoutResponseDTO(); + expectedLayoutResponse = new LayoutResponseDto(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); expectedLayoutResponse.setMetadata(metadataResponse); @@ -90,7 +91,7 @@ public void setup() { @Test void getLayout_layoutExists_returnsLayout() { when(layoutService.getLayout(VALID_LAYOUT_ID)).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); assertThat(layoutController.getLayout(VALID_LAYOUT_ID)).isEqualTo(expectedLayoutResponse); } @@ -108,7 +109,7 @@ void getMetadata_metadataExists_returnsMetadata() { List metadataList = List.of(metadata); when(metadataService.getMetadata()).thenReturn(metadataList); - when(modelMapper.map(metadata, MetadataResponseDTO.class)) + when(modelMapper.map(metadata, MetadataResponseDto.class)) .thenReturn(metadataResponse); assertThat(layoutController.getMetadata()).isEqualTo(List.of(metadataResponse)); @@ -129,7 +130,7 @@ void createLayout_validLayout_returnsCreatedLayout() { when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout.getId()); when(layoutService.getLayout(layout.getId())).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(expectedLayoutResponse); } @@ -153,8 +154,8 @@ void deleteLayout__validId_callsLayoutService() { verify(layoutService).deleteLayout(VALID_LAYOUT_ID); } - private MetadataResponseDTO getMetadataResponseDTO() { - MetadataResponseDTO metadataResponse = new MetadataResponseDTO(); + private MetadataResponseDto getMetadataResponseDto() { + MetadataResponseDto metadataResponse = new MetadataResponseDto(); metadataResponse.setLayoutId(layout.getId()); metadataResponse.setBaseMetadata(baseMetadata); metadataResponse.setCreated(layout.getMetadata().getCreated()); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java new file mode 100644 index 000000000..e4724bbb1 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -0,0 +1,181 @@ +package org.finos.vuu.layoutserver.integration; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +import org.junit.jupiter.api.Test; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class ApplicationLayoutIntegrationTest { + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String BASE_URL = "/application-layouts"; + private static final String MISSING_USERNAME_ERROR_MESSAGE = + "Required request header 'username' for method parameter type String is not present"; + + @Autowired + private MockMvc mockMvc; + @Autowired + private ApplicationLayoutRepository repository; + @MockBean + private DefaultApplicationLayoutLoader mockLoader; + private final DefaultApplicationLayoutLoader realLoader = new DefaultApplicationLayoutLoader(); + + @Test + public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { + when(mockLoader.getDefaultLayout()).thenReturn(realLoader.getDefaultLayout()); + + mockMvc.perform(get(BASE_URL).header("username", "new user")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username", nullValue())) + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json + .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); + } + + @Test + public void getApplicationLayout_defaultFailsToLoad_returns500() throws Exception { + String errorMessage = "Failed to read default application layout"; + doThrow(new InternalServerErrorException(errorMessage)).when(mockLoader).getDefaultLayout(); + + mockMvc.perform(get(BASE_URL).header("username", "new user")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains(errorMessage))); + } + + @Test + public void getApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(get(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + @Test + public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() throws Exception { + String user = "user"; + + Map definition = new HashMap<>(); + definition.put("defKey", "defVal"); + + persistApplicationLayout(user, definition); + + mockMvc.perform(get(BASE_URL).header("username", user)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username", is(user))) + .andExpect(jsonPath("$.definition", is(definition))); + } + + @Test + public void persistApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\": \"value\"}"; + + mockMvc.perform(put(BASE_URL).header("username", user) + .content(definition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); + + assertThat(persistedLayout.getUsername()).isEqualTo(user); + assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); + } + + @Test + public void persistApplicationLayout_layoutExists_returns201AndOverwritesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + String newDefinition = "{\"new-key\": \"new-value\"}"; + + mockMvc.perform(put(BASE_URL).header("username", user) + .content(newDefinition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(1); + + ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); + + assertThat(retrievedLayout.getUsername()).isEqualTo(user); + assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); + } + + @Test + public void persistApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(put(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + @Test + public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { + String user = "user"; + + mockMvc.perform(delete(BASE_URL).header("username", user)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("No layout found for user: " + user))); + } + + @Test + public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + mockMvc.perform(delete(BASE_URL).header("username", user)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(0); + } + + @Test + public void deleteApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(delete(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + + private void persistApplicationLayout(String user, Map definition) { + repository.save(new ApplicationLayout(user, objectMapper.convertValue(definition, JsonNode.class))); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index a1689c9e4..cc54e216f 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -1,20 +1,9 @@ package org.finos.vuu.layoutserver.integration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -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 com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -30,6 +19,14 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") @@ -62,17 +59,17 @@ void getLayout_validIDAndLayoutExists_returns200WithLayout() throws Exception { assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); mockMvc.perform(get("/layouts/{id}", layout.getId())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.definition", - is(layout.getDefinition()))) - .andExpect(jsonPath("$.metadata.name", - is(layout.getMetadata().getBaseMetadata().getName()))) - .andExpect(jsonPath("$.metadata.group", - is(layout.getMetadata().getBaseMetadata().getGroup()))) - .andExpect(jsonPath("$.metadata.screenshot", - is(layout.getMetadata().getBaseMetadata().getScreenshot()))) - .andExpect(jsonPath("$.metadata.user", - is(layout.getMetadata().getBaseMetadata().getUser()))); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.definition", + is(layout.getDefinition()))) + .andExpect(jsonPath("$.metadata.name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layout.getMetadata().getBaseMetadata().getUser()))); } @Test @@ -87,11 +84,12 @@ void getLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; mockMvc.perform(get("/layouts/{id}", layoutID)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "Failed to convert value of type 'java.lang.String' to required type 'java.util" - + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " - + "UUID string: invalidUUID")); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", + contains("Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } @Test @@ -100,15 +98,15 @@ void getMetadata_singleMetadataExists_returnsMetadata() throws Exception { assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); mockMvc.perform(get("/layouts/metadata")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].name", - is(layout.getMetadata().getBaseMetadata().getName()))) - .andExpect(jsonPath("$[0].group", - is(layout.getMetadata().getBaseMetadata().getGroup()))) - .andExpect(jsonPath("$[0].screenshot", - is(layout.getMetadata().getBaseMetadata().getScreenshot()))) - .andExpect(jsonPath("$[0].user", - is(layout.getMetadata().getBaseMetadata().getUser()))); + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout.getMetadata().getBaseMetadata().getUser()))); } @Test @@ -126,58 +124,58 @@ void getMetadata_multipleMetadataExists_returnsAllMetadata() throws Exception { assertThat(layoutRepository.findById(layout2.getId()).orElseThrow()).isEqualTo(layout2); mockMvc.perform(get("/layouts/metadata")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].name", - is(layout1.getMetadata().getBaseMetadata().getName()))) - .andExpect(jsonPath("$[0].group", - is(layout1.getMetadata().getBaseMetadata().getGroup()))) - .andExpect(jsonPath("$[0].screenshot", - is(layout1.getMetadata().getBaseMetadata().getScreenshot()))) - .andExpect(jsonPath("$[0].user", - is(layout1.getMetadata().getBaseMetadata().getUser()))) - .andExpect(jsonPath("$[1].name", - is(layout2.getMetadata().getBaseMetadata().getName()))) - .andExpect(jsonPath("$[1].group", - is(layout2.getMetadata().getBaseMetadata().getGroup()))) - .andExpect(jsonPath("$[1].screenshot", - is(layout2.getMetadata().getBaseMetadata().getScreenshot()))) - .andExpect(jsonPath("$[1].user", - is(layout2.getMetadata().getBaseMetadata().getUser()))); + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", + is(layout1.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout1.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout1.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout1.getMetadata().getBaseMetadata().getUser()))) + .andExpect(jsonPath("$[1].name", + is(layout2.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[1].group", + is(layout2.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[1].screenshot", + is(layout2.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[1].user", + is(layout2.getMetadata().getBaseMetadata().getUser()))); } @Test void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { mockMvc.perform(get("/layouts/metadata")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$").isEmpty()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); } @Test void createLayout_validRequest_returnsCreatedLayoutAndLayoutIsPersisted() - throws Exception { - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); MvcResult result = mockMvc.perform(post("/layouts") - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").isNotEmpty()) - .andExpect(jsonPath("$.definition", is(layoutRequest.getDefinition()))) - .andExpect(jsonPath("$.metadata.name", - is(layoutRequest.getMetadata().getBaseMetadata().getName()))) - .andExpect(jsonPath("$.metadata.group", - is(layoutRequest.getMetadata().getBaseMetadata().getGroup()))) - .andExpect(jsonPath("$.metadata.screenshot", - is(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()))) - .andExpect(jsonPath("$.metadata.user", - is(layoutRequest.getMetadata().getBaseMetadata().getUser()))) - .andReturn(); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.definition", is(layoutRequest.getDefinition()))) + .andExpect(jsonPath("$.metadata.name", + is(layoutRequest.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layoutRequest.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layoutRequest.getMetadata().getBaseMetadata().getUser()))) + .andReturn(); UUID createdLayoutId = UUID.fromString( - JsonPath.read(result.getResponse().getContentAsString(), "$.id")); + JsonPath.read(result.getResponse().getContentAsString(), "$.id")); Layout createdLayout = layoutRepository.findById(createdLayoutId).orElseThrow(); Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId()) - .orElseThrow(); + .orElseThrow(); // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in // the DB @@ -185,29 +183,29 @@ void createLayout_validRequest_returnsCreatedLayoutAndLayoutIsPersisted() assertThat(metadataRepository.findAll()).containsExactly(createdMetadata); assertThat(createdLayout.getDefinition()) - .isEqualTo(layoutRequest.getDefinition()); + .isEqualTo(layoutRequest.getDefinition()); assertThat(createdMetadata.getBaseMetadata().getName()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); assertThat(createdMetadata.getBaseMetadata().getGroup()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); assertThat(createdMetadata.getBaseMetadata().getScreenshot()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); assertThat(createdMetadata.getBaseMetadata().getUser()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); } @Test void createLayout_invalidRequestBodyDefinitionsIsBlank_returns400AndDoesNotCreateLayout() - throws Exception { - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); layoutRequest.setDefinition(""); mockMvc.perform(post("/layouts") - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "[definition: Definition must not be blank]")); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("definition: Definition must not be blank"))); assertThat(layoutRepository.findAll()).isEmpty(); assertThat(metadataRepository.findAll()).isEmpty(); @@ -215,16 +213,16 @@ void createLayout_invalidRequestBodyDefinitionsIsBlank_returns400AndDoesNotCreat @Test void createLayout_invalidRequestBodyMetadataIsNull_returns400AndDoesNotCreateLayout() - throws Exception { - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); layoutRequest.setMetadata(null); mockMvc.perform(post("/layouts") - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "[metadata: Metadata must not be null]")); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("metadata: Metadata must not be null"))); assertThat(layoutRepository.findAll()).isEmpty(); assertThat(metadataRepository.findAll()).isEmpty(); @@ -236,26 +234,27 @@ void createLayout_invalidRequestBodyUnexpectedFormat_returns400() throws Excepti String invalidLayout = "invalidLayout"; mockMvc.perform(post("/layouts") - .content(invalidLayout) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "JSON parse error: Unrecognized token 'invalidLayout': was expecting (JSON " - + "String, Number, Array, Object or token 'null', 'true' or 'false'); nested " - + "exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized " - + "token 'invalidLayout': was expecting (JSON String, Number, Array, Object " - + "or token 'null', 'true' or 'false')\n" - + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" - + " line: 1, column: 14]")); + .content(invalidLayout) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "JSON parse error: Unrecognized token 'invalidLayout': was expecting (JSON " + + "String, Number, Array, Object or token 'null', 'true' or 'false'); nested " + + "exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized " + + "token 'invalidLayout': was expecting (JSON String, Number, Array, Object " + + "or token 'null', 'true' or 'false')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 14]"))); } @Test void updateLayout_validIdAndValidRequest_returns204AndLayoutHasChanged() throws Exception { Layout initialLayout = createDefaultLayoutInDatabase(); assertThat(layoutRepository.findById(initialLayout.getId()).orElseThrow()).isEqualTo( - initialLayout); + initialLayout); - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); layoutRequest.setDefinition("Updated definition"); layoutRequest.getMetadata().getBaseMetadata().setName("Updated name"); layoutRequest.getMetadata().getBaseMetadata().setGroup("Updated group"); @@ -263,115 +262,119 @@ void updateLayout_validIdAndValidRequest_returns204AndLayoutHasChanged() throws layoutRequest.getMetadata().getBaseMetadata().setUser("Updated user"); mockMvc.perform(put("/layouts/{id}", initialLayout.getId()) - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()) - .andExpect(jsonPath("$").doesNotExist()); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); Layout updatedLayout = layoutRepository.findById(initialLayout.getId()).orElseThrow(); assertThat(updatedLayout.getDefinition()) - .isEqualTo(layoutRequest.getDefinition()); + .isEqualTo(layoutRequest.getDefinition()); assertThat(updatedLayout.getMetadata().getBaseMetadata().getName()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); assertThat(updatedLayout.getMetadata().getBaseMetadata().getGroup()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); assertThat(updatedLayout.getMetadata().getBaseMetadata().getScreenshot()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); assertThat(updatedLayout.getMetadata().getBaseMetadata().getUser()) - .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); assertThat(updatedLayout).isNotEqualTo(initialLayout); } @Test void updateLayout_invalidRequestBodyDefinitionIsBlank_returns400AndLayoutDoesNotChange() - throws Exception { + throws Exception { Layout layout = createDefaultLayoutInDatabase(); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); - LayoutRequestDTO request = createValidLayoutRequest(); + LayoutRequestDto request = createValidLayoutRequest(); request.setDefinition(""); mockMvc.perform(put("/layouts/{id}", layout.getId()) - .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string("[definition: Definition must not be blank]")); + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("definition: Definition must not be blank"))); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); } @Test void updateLayout_invalidRequestBodyMetadataIsNull_returns400AndLayoutDoesNotChange() - throws Exception { + throws Exception { Layout layout = createDefaultLayoutInDatabase(); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); - LayoutRequestDTO request = createValidLayoutRequest(); + LayoutRequestDto request = createValidLayoutRequest(); request.setMetadata(null); mockMvc.perform(put("/layouts/{id}", layout.getId()) - .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string("[metadata: Metadata must not be null]")); + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains("metadata: Metadata must not be null"))); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); } @Test void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotChange() - throws Exception { + throws Exception { Layout layout = createDefaultLayoutInDatabase(); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); String request = "invalidRequest"; mockMvc.perform(put("/layouts/{id}", layout.getId()) - .content(objectMapper.writeValueAsString(request)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "JSON parse error: Cannot construct instance of `org.finos.vuu.layoutserver.dto" - + ".request.LayoutRequestDTO` (although at least one Creator exists): no " - + "String-argument constructor/factory method to deserialize from String " - + "value ('invalidRequest'); nested exception is com.fasterxml.jackson" - + ".databind.exc.MismatchedInputException: Cannot construct instance of `org" - + ".finos.vuu.layoutserver.dto.request.LayoutRequestDTO` (although at least " - + "one Creator exists): no String-argument constructor/factory method to " - + "deserialize from String value ('invalidRequest')\n" - + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" - + " line: 1, column: 1]")); + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "JSON parse error: Cannot construct instance of `org.finos.vuu.layoutserver.dto" + + ".request.LayoutRequestDto` (although at least one Creator exists): no " + + "String-argument constructor/factory method to deserialize from String " + + "value ('invalidRequest'); nested exception is com.fasterxml.jackson" + + ".databind.exc.MismatchedInputException: Cannot construct instance of `org" + + ".finos.vuu.layoutserver.dto.request.LayoutRequestDto` (although at least " + + "one Creator exists): no String-argument constructor/factory method to " + + "deserialize from String value ('invalidRequest')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 1]"))); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo( - layout); + layout); } @Test void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); } @Test void updateLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - LayoutRequestDTO layoutRequest = createValidLayoutRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) - .content(objectMapper.writeValueAsString(layoutRequest)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "Failed to convert value of type 'java.lang.String' to required type 'java.util" - + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " - + "UUID string: invalidUUID")); + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } @Test @@ -396,11 +399,12 @@ void deleteLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; mockMvc.perform(delete("/layouts/{id}", layoutID)) - .andExpect(status().isBadRequest()) - .andExpect(content().string( - "Failed to convert value of type 'java.lang.String' to required type 'java.util" - + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " - + "UUID string: invalidUUID")); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.messages", iterableWithSize(1))) + .andExpect(jsonPath("$.messages", contains( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } private Layout createDefaultLayoutInDatabase() { @@ -421,17 +425,17 @@ private Layout createDefaultLayoutInDatabase() { return layoutRepository.save(layout); } - private LayoutRequestDTO createValidLayoutRequest() { + private LayoutRequestDto createValidLayoutRequest() { BaseMetadata baseMetadata = new BaseMetadata(); baseMetadata.setName(DEFAULT_LAYOUT_NAME); baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); baseMetadata.setUser(DEFAULT_LAYOUT_USER); - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + MetadataRequestDto metadataRequest = new MetadataRequestDto(); metadataRequest.setBaseMetadata(baseMetadata); - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + LayoutRequestDto layoutRequest = new LayoutRequestDto(); layoutRequest.setDefinition(DEFAULT_LAYOUT_DEFINITION); layoutRequest.setMetadata(metadataRequest); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java new file mode 100644 index 000000000..e6c8f0414 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -0,0 +1,103 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class ApplicationLayoutServiceTest { + + private static ApplicationLayoutRepository mockRepo; + private static ApplicationLayoutService service; + private static final DefaultApplicationLayoutLoader defaultLoader = new DefaultApplicationLayoutLoader(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void setup() { + mockRepo = Mockito.mock(ApplicationLayoutRepository.class); + service = new ApplicationLayoutService(mockRepo, defaultLoader); + } + + @Test + public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { + when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); + + ApplicationLayout actualLayout = service.getApplicationLayout("new user"); + + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json + JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); + + assertThat(actualLayout.getUsername()).isNull(); + assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); + } + + @Test + public void getApplicationLayout_layoutExists_returnsLayout() throws JsonProcessingException { + String user = "user"; + + JsonNode expectedDefinition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); + + when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); + + ApplicationLayout actualLayout = service.getApplicationLayout(user); + + assertThat(actualLayout).isEqualTo(expectedLayout); + } + + @Test + public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + service.persistApplicationLayout(user, definition); + + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, definition)); + } + + @Test + public void createApplicationLayout_invalidDefinition_throwsJsonException() { + String definition = "invalid JSON"; + + assertThrows(JsonProcessingException.class, () -> + service.persistApplicationLayout("user", objectMapper.readTree(definition)) + ); + } + + @Test + public void deleteApplicationLayout_entryExists_callsRepoDelete() { + String user = "user"; + + service.deleteApplicationLayout(user); + + verify(mockRepo, times(1)).deleteById(user); + } + + @Test + public void deleteApplicationLayout_deleteFails_throwsException() { + String user = "user"; + + doThrow(EmptyResultDataAccessException.class).when(mockRepo).deleteById(user); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> + service.deleteApplicationLayout(user) + ); + + assertThat(exception.getMessage()).isEqualTo("No layout found for user: " + user); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java index 5ec50e5d7..543641182 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -1,15 +1,5 @@ package org.finos.vuu.layoutserver.service; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.UUID; import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -22,6 +12,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.EmptyResultDataAccessException; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) class LayoutServiceTest { diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java index 1539a9237..74bbb4844 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java @@ -1,9 +1,5 @@ package org.finos.vuu.layoutserver.service; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.Mockito.when; - -import java.util.List; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.junit.jupiter.api.Test; @@ -12,6 +8,11 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class MetadataServiceTest { diff --git a/layout-server/src/test/resources/defaultApplicationLayout.json b/layout-server/src/test/resources/defaultApplicationLayout.json new file mode 100644 index 000000000..87a79e544 --- /dev/null +++ b/layout-server/src/test/resources/defaultApplicationLayout.json @@ -0,0 +1,3 @@ +{ + "defaultLayoutKey": "default-layout-value" +}