diff --git a/layout-server/pom.xml b/layout-server/pom.xml
index f617a4999..9ae3cf8e5 100644
--- a/layout-server/pom.xml
+++ b/layout-server/pom.xml
@@ -27,6 +27,10 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.springframework.boot
spring-boot-starter-web
@@ -54,6 +58,12 @@
modelmapper
3.1.0
+
+ org.jetbrains
+ annotations
+ 13.0
+ compile
+
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 d45ac6fcd..855392f6c 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
@@ -2,7 +2,6 @@
import lombok.RequiredArgsConstructor;
import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO;
-import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO;
import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO;
import org.finos.vuu.layoutserver.model.Layout;
import org.finos.vuu.layoutserver.model.Metadata;
@@ -29,16 +28,6 @@ public ModelMapper modelMapper() {
metadata -> layoutService.getLayoutByMetadataId(metadata.getId()),
MetadataResponseDTO::setLayoutId));
- mapper.typeMap(MetadataRequestDTO.class, Metadata.class)
- .addMappings(m -> m.map(
- MetadataRequestDTO::getBaseMetadata,
- Metadata::setBaseMetadata));
-
- mapper.typeMap(Metadata.class, MetadataResponseDTO.class)
- .addMappings(m -> m.map(
- Metadata::getBaseMetadata,
- MetadataResponseDTO::setBaseMetadata));
-
return mapper;
}
}
\ No newline at end of file
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 5adee9b72..3de15fa14 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
@@ -2,6 +2,7 @@
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;
@@ -11,6 +12,7 @@
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;
@@ -24,6 +26,7 @@
@RequiredArgsConstructor
@RestController
@RequestMapping("/layouts")
+@Validated
public class LayoutController {
private final LayoutService layoutService;
@@ -65,7 +68,7 @@ public List getMetadata() {
*/
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
- public LayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCreate) {
+ public LayoutResponseDTO createLayout(@Valid @RequestBody LayoutRequestDTO layoutToCreate) {
Layout layout = mapper.map(layoutToCreate, Layout.class);
Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout));
@@ -81,7 +84,7 @@ public LayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCrea
*/
@ResponseStatus(HttpStatus.NO_CONTENT)
@PutMapping("/{id}")
- public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layout) {
+ public void updateLayout(@PathVariable UUID id, @Valid @RequestBody 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
index 06c371450..60d50af11 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,14 +1,22 @@
package org.finos.vuu.layoutserver.dto.request;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class LayoutRequestDTO {
/**
- * The definition of the layout as a string (i.e. stringified JSON structure containing components)
+ * The definition of the layout as a string (e.g. stringified JSON structure containing
+ * components)
*/
+ @JsonProperty(value = "definition", required = true)
+ @NotBlank(message = "Definition must not be blank")
private String definition;
+ @JsonProperty(value = "metadata", required = true)
+ @NotNull(message = "Metadata must not be null")
private MetadataRequestDTO 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
index 8312a4758..5efa5abaa 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,7 +1,7 @@
package org.finos.vuu.layoutserver.dto.response;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
-import java.util.Date;
+import java.time.LocalDate;
import java.util.UUID;
import lombok.Data;
import org.finos.vuu.layoutserver.model.BaseMetadata;
@@ -14,6 +14,6 @@ public class MetadataResponseDTO {
@JsonUnwrapped
BaseMetadata baseMetadata;
- private Date created;
- private Date updated;
+ private LocalDate created;
+ private LocalDate updated;
}
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
new file mode 100644
index 000000000..b9838c681
--- /dev/null
+++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java
@@ -0,0 +1,40 @@
+package org.finos.vuu.layoutserver.exceptions;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(NoSuchElementException.class)
+ public ResponseEntity handleNotFound(NoSuchElementException ex) {
+ return new ResponseEntity<>(ex.getMessage(),
+ org.springframework.http.HttpStatus.NOT_FOUND);
+ }
+
+ @ExceptionHandler({
+ HttpMessageNotReadableException.class,
+ MethodArgumentTypeMismatchException.class})
+ public ResponseEntity handleBadRequest(Exception ex) {
+ return new ResponseEntity<>(ex.getMessage(),
+ org.springframework.http.HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
+ List errors = ex.getFieldErrors()
+ .stream()
+ .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
+ .collect(Collectors.toList());
+
+ return new ResponseEntity<>(errors.toString(),
+ org.springframework.http.HttpStatus.BAD_REQUEST);
+ }
+}
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 c83b54aa2..6251cbf2a 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,5 +1,6 @@
package org.finos.vuu.layoutserver.model;
+import java.util.UUID;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -9,7 +10,6 @@
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Data;
-import java.util.UUID;
@Data
@Entity
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 4b56db7ac..604779f23 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,6 +1,6 @@
package org.finos.vuu.layoutserver.model;
-import java.util.Date;
+import java.time.LocalDate;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Embedded;
@@ -28,8 +28,7 @@ public class Metadata {
@Embedded
private BaseMetadata baseMetadata;
- private Date created = new Date();
-
- private Date updated;
+ private final LocalDate created = LocalDate.now();
+ private LocalDate updated;
}
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 03f81b108..50cbe6288 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,10 +1,9 @@
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/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java
index 9e28a72c1..26dca49f5 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,6 +1,7 @@
package org.finos.vuu.layoutserver.service;
-import java.util.Date;
+import java.time.LocalDate;
+import java.util.NoSuchElementException;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.finos.vuu.layoutserver.model.Layout;
@@ -15,7 +16,8 @@ public class LayoutService {
private final LayoutRepository layoutRepository;
public Layout getLayout(UUID id) {
- return layoutRepository.findById(id).orElseThrow();
+ return layoutRepository.findById(id)
+ .orElseThrow(() -> new NoSuchElementException("Layout with ID '" + id + "' not found"));
}
public Layout getLayoutByMetadataId(UUID id) {
@@ -32,7 +34,7 @@ public void updateLayout(UUID layoutId, Layout newLayout) {
Metadata updatedMetadata = Metadata.builder()
.baseMetadata(newMetadata.getBaseMetadata())
- .updated(new Date())
+ .updated(LocalDate.now())
.build();
layoutToUpdate.setDefinition(newLayout.getDefinition());
@@ -42,6 +44,10 @@ public void updateLayout(UUID layoutId, Layout newLayout) {
}
public void deleteLayout(UUID id) {
- layoutRepository.deleteById(id);
+ try {
+ layoutRepository.deleteById(id);
+ } catch (Exception e) {
+ throw new NoSuchElementException("Layout with ID '" + id + "' not found");
+ }
}
}
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 08398edc4..de3eb095e 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,13 +1,12 @@
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/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java
new file mode 100644
index 000000000..6c9ecb75f
--- /dev/null
+++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java
@@ -0,0 +1,164 @@
+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.model.BaseMetadata;
+import org.finos.vuu.layoutserver.model.Layout;
+import org.finos.vuu.layoutserver.model.Metadata;
+import org.finos.vuu.layoutserver.service.LayoutService;
+import org.finos.vuu.layoutserver.service.MetadataService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.modelmapper.ModelMapper;
+
+@ExtendWith(MockitoExtension.class)
+class LayoutControllerTest {
+
+ private static final String LAYOUT_DEFINITION = "Test Definition";
+ private static final String LAYOUT_GROUP = "Test Group";
+ private static final String LAYOUT_NAME = "Test Layout";
+ private static final String LAYOUT_SCREENSHOT = "Test Screenshot";
+ private static final String LAYOUT_USER = "Test User";
+ private static final UUID VALID_LAYOUT_ID = UUID.randomUUID();
+ private static final UUID VALID_METADATA_ID = UUID.randomUUID();
+ private static final UUID DOES_NOT_EXIST_LAYOUT_ID = UUID.randomUUID();
+
+ @Mock
+ private LayoutService layoutService;
+
+ @Mock
+ private MetadataService metadataService;
+
+ @Mock
+ private ModelMapper modelMapper;
+
+ @InjectMocks
+ private LayoutController layoutController;
+
+ private Layout layout;
+ private Metadata metadata;
+ private BaseMetadata baseMetadata;
+ private LayoutRequestDTO layoutRequest;
+ private LayoutResponseDTO expectedLayoutResponse;
+ private MetadataResponseDTO metadataResponse;
+
+ @BeforeEach
+ public void setup() {
+ baseMetadata = new BaseMetadata();
+ baseMetadata.setName(LAYOUT_NAME);
+ baseMetadata.setUser(LAYOUT_USER);
+ baseMetadata.setGroup(LAYOUT_GROUP);
+ baseMetadata.setScreenshot(LAYOUT_SCREENSHOT);
+
+ metadata = Metadata.builder().id(VALID_METADATA_ID).baseMetadata(baseMetadata).build();
+
+ layout = new Layout();
+ layout.setId(VALID_LAYOUT_ID);
+ layout.setDefinition(LAYOUT_DEFINITION);
+ layout.setMetadata(metadata);
+
+ layoutRequest = new LayoutRequestDTO();
+ MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO();
+ metadataRequestDTO.setBaseMetadata(baseMetadata);
+ layoutRequest.setDefinition(layout.getDefinition());
+ layoutRequest.setMetadata(metadataRequestDTO);
+
+ metadataResponse = getMetadataResponseDTO();
+
+ expectedLayoutResponse = new LayoutResponseDTO();
+ expectedLayoutResponse.setId(layout.getId());
+ expectedLayoutResponse.setDefinition(layout.getDefinition());
+ expectedLayoutResponse.setMetadata(metadataResponse);
+
+ }
+
+
+ @Test
+ void getLayout_layoutExists_returnsLayout() {
+ when(layoutService.getLayout(VALID_LAYOUT_ID)).thenReturn(layout);
+ when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse);
+ assertThat(layoutController.getLayout(VALID_LAYOUT_ID)).isEqualTo(expectedLayoutResponse);
+ }
+
+ @Test
+ void getLayout_layoutDoesNotExist_throwsNoSuchElementException() {
+ when(layoutService.getLayout(DOES_NOT_EXIST_LAYOUT_ID))
+ .thenThrow(NoSuchElementException.class);
+
+ assertThrows(NoSuchElementException.class,
+ () -> layoutController.getLayout(DOES_NOT_EXIST_LAYOUT_ID));
+ }
+
+ @Test
+ void getMetadata_metadataExists_returnsMetadata() {
+ List metadataList = List.of(metadata);
+
+ when(metadataService.getMetadata()).thenReturn(metadataList);
+ when(modelMapper.map(metadata, MetadataResponseDTO.class))
+ .thenReturn(metadataResponse);
+
+ assertThat(layoutController.getMetadata()).isEqualTo(List.of(metadataResponse));
+ }
+
+ @Test
+ void getMetadata_noMetadataExists_returnsEmptyArray() {
+ when(metadataService.getMetadata()).thenReturn(List.of());
+ assertThat(layoutController.getMetadata()).isEmpty();
+ }
+
+ @Test
+ void createLayout_validLayout_returnsCreatedLayout() {
+ Layout layoutWithoutIds = layout;
+ layoutWithoutIds.setId(null);
+ layoutWithoutIds.getMetadata().setId(null);
+
+ 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);
+
+ assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(expectedLayoutResponse);
+ }
+
+ @Test
+ void updateLayout_validLayout_callsLayoutService() {
+ layout.setId(null);
+ layout.getMetadata().setId(null);
+
+ when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layout);
+
+ layoutController.updateLayout(VALID_LAYOUT_ID, layoutRequest);
+
+ verify(layoutService).updateLayout(VALID_LAYOUT_ID, layout);
+ }
+
+ @Test
+ void deleteLayout__validId_callsLayoutService() {
+ layoutController.deleteLayout(VALID_LAYOUT_ID);
+
+ verify(layoutService).deleteLayout(VALID_LAYOUT_ID);
+ }
+
+ private MetadataResponseDTO getMetadataResponseDTO() {
+ MetadataResponseDTO metadataResponse = new MetadataResponseDTO();
+ metadataResponse.setLayoutId(layout.getId());
+ metadataResponse.setBaseMetadata(baseMetadata);
+ metadataResponse.setCreated(layout.getMetadata().getCreated());
+ metadataResponse.setUpdated(layout.getMetadata().getUpdated());
+ return metadataResponse;
+ }
+}
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
new file mode 100644
index 000000000..a1689c9e4
--- /dev/null
+++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java
@@ -0,0 +1,440 @@
+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.model.BaseMetadata;
+import org.finos.vuu.layoutserver.model.Layout;
+import org.finos.vuu.layoutserver.model.Metadata;
+import org.finos.vuu.layoutserver.repository.LayoutRepository;
+import org.finos.vuu.layoutserver.repository.MetadataRepository;
+import org.junit.jupiter.api.BeforeEach;
+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.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+public class LayoutIntegrationTest {
+
+ private static final String DEFAULT_LAYOUT_DEFINITION = "Default layout definition";
+ private static final String DEFAULT_LAYOUT_NAME = "Default layout name";
+ private static final String DEFAULT_LAYOUT_GROUP = "Default layout group";
+ private static final String DEFAULT_LAYOUT_SCREENSHOT = "Default layout screenshot";
+ private static final String DEFAULT_LAYOUT_USER = "Default layout user";
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Autowired
+ private MockMvc mockMvc;
+ @Autowired
+ private LayoutRepository layoutRepository;
+ @Autowired
+ private MetadataRepository metadataRepository;
+
+ @BeforeEach
+ void tearDown() {
+ layoutRepository.deleteAll();
+ metadataRepository.deleteAll();
+ }
+
+ @Test
+ void getLayout_validIDAndLayoutExists_returns200WithLayout() throws Exception {
+ Layout layout = createDefaultLayoutInDatabase();
+ 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())));
+ }
+
+ @Test
+ void getLayout_validIDButLayoutDoesNotExist_returns404() throws Exception {
+ UUID layoutID = UUID.randomUUID();
+
+ mockMvc.perform(get("/layouts/{id}", layoutID)).andExpect(status().isNotFound());
+ }
+
+ @Test
+ 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"));
+ }
+
+ @Test
+ void getMetadata_singleMetadataExists_returnsMetadata() throws Exception {
+ Layout layout = createDefaultLayoutInDatabase();
+ 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())));
+ }
+
+ @Test
+ void getMetadata_multipleMetadataExists_returnsAllMetadata() throws Exception {
+ Layout layout1 = createDefaultLayoutInDatabase();
+ Layout layout2 = createDefaultLayoutInDatabase();
+ layout2.setDefinition("Different definition");
+ layout2.getMetadata().getBaseMetadata().setName("Different name");
+ layout2.getMetadata().getBaseMetadata().setGroup("Different group");
+ layout2.getMetadata().getBaseMetadata().setScreenshot("Different screenshot");
+ layout2.getMetadata().getBaseMetadata().setUser("Different user");
+ layoutRepository.save(layout2);
+
+ assertThat(layoutRepository.findById(layout1.getId()).orElseThrow()).isEqualTo(layout1);
+ 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())));
+ }
+
+ @Test
+ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception {
+ mockMvc.perform(get("/layouts/metadata"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$").isEmpty());
+ }
+
+ @Test
+ void createLayout_validRequest_returnsCreatedLayoutAndLayoutIsPersisted()
+ 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();
+
+ UUID createdLayoutId = UUID.fromString(
+ JsonPath.read(result.getResponse().getContentAsString(), "$.id"));
+ Layout createdLayout = layoutRepository.findById(createdLayoutId).orElseThrow();
+ Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId())
+ .orElseThrow();
+
+ // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in
+ // the DB
+ assertThat(layoutRepository.findAll()).containsExactly(createdLayout);
+ assertThat(metadataRepository.findAll()).containsExactly(createdMetadata);
+
+ assertThat(createdLayout.getDefinition())
+ .isEqualTo(layoutRequest.getDefinition());
+ assertThat(createdMetadata.getBaseMetadata().getName())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName());
+ assertThat(createdMetadata.getBaseMetadata().getGroup())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup());
+ assertThat(createdMetadata.getBaseMetadata().getScreenshot())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot());
+ assertThat(createdMetadata.getBaseMetadata().getUser())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser());
+ }
+
+ @Test
+ void createLayout_invalidRequestBodyDefinitionsIsBlank_returns400AndDoesNotCreateLayout()
+ 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]"));
+
+ assertThat(layoutRepository.findAll()).isEmpty();
+ assertThat(metadataRepository.findAll()).isEmpty();
+ }
+
+ @Test
+ void createLayout_invalidRequestBodyMetadataIsNull_returns400AndDoesNotCreateLayout()
+ 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]"));
+
+ assertThat(layoutRepository.findAll()).isEmpty();
+ assertThat(metadataRepository.findAll()).isEmpty();
+ }
+
+
+ @Test
+ void createLayout_invalidRequestBodyUnexpectedFormat_returns400() throws Exception {
+ 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]"));
+ }
+
+ @Test
+ void updateLayout_validIdAndValidRequest_returns204AndLayoutHasChanged() throws Exception {
+ Layout initialLayout = createDefaultLayoutInDatabase();
+ assertThat(layoutRepository.findById(initialLayout.getId()).orElseThrow()).isEqualTo(
+ initialLayout);
+
+ LayoutRequestDTO layoutRequest = createValidLayoutRequest();
+ layoutRequest.setDefinition("Updated definition");
+ layoutRequest.getMetadata().getBaseMetadata().setName("Updated name");
+ layoutRequest.getMetadata().getBaseMetadata().setGroup("Updated group");
+ layoutRequest.getMetadata().getBaseMetadata().setScreenshot("Updated screenshot");
+ 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());
+
+ Layout updatedLayout = layoutRepository.findById(initialLayout.getId()).orElseThrow();
+
+ assertThat(updatedLayout.getDefinition())
+ .isEqualTo(layoutRequest.getDefinition());
+ assertThat(updatedLayout.getMetadata().getBaseMetadata().getName())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName());
+ assertThat(updatedLayout.getMetadata().getBaseMetadata().getGroup())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup());
+ assertThat(updatedLayout.getMetadata().getBaseMetadata().getScreenshot())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot());
+ assertThat(updatedLayout.getMetadata().getBaseMetadata().getUser())
+ .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser());
+
+ assertThat(updatedLayout).isNotEqualTo(initialLayout);
+ }
+
+ @Test
+ void updateLayout_invalidRequestBodyDefinitionIsBlank_returns400AndLayoutDoesNotChange()
+ throws Exception {
+ Layout layout = createDefaultLayoutInDatabase();
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout);
+
+ 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]"));
+
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout);
+ }
+
+ @Test
+ void updateLayout_invalidRequestBodyMetadataIsNull_returns400AndLayoutDoesNotChange()
+ throws Exception {
+ Layout layout = createDefaultLayoutInDatabase();
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout);
+
+ 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]"));
+
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout);
+ }
+
+ @Test
+ void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotChange()
+ 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]"));
+
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(
+ layout);
+ }
+
+ @Test
+ void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception {
+ UUID layoutID = UUID.randomUUID();
+ LayoutRequestDTO layoutRequest = createValidLayoutRequest();
+
+ mockMvc.perform(put("/layouts/{id}", layoutID)
+ .content(objectMapper.writeValueAsString(layoutRequest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void updateLayout_invalidId_returns400() throws Exception {
+ String layoutID = "invalidUUID";
+ 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"));
+ }
+
+ @Test
+ void deleteLayout_validIdLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception {
+ Layout layout = createDefaultLayoutInDatabase();
+ assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout);
+
+ mockMvc.perform(delete("/layouts/{id}", layout.getId())).andExpect(status().isNoContent());
+
+ assertThat(layoutRepository.findById(layout.getId())).isEmpty();
+ }
+
+ @Test
+ void deleteLayout_validIdLayoutDoesNotExist_returnsNotFound() throws Exception {
+ UUID layoutID = UUID.randomUUID();
+
+ mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isNotFound());
+ }
+
+ @Test
+ 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"));
+ }
+
+ private Layout createDefaultLayoutInDatabase() {
+ Layout layout = new Layout();
+ Metadata metadata = new Metadata();
+ BaseMetadata baseMetadata = new BaseMetadata();
+
+ baseMetadata.setName(DEFAULT_LAYOUT_NAME);
+ baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP);
+ baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT);
+ baseMetadata.setUser(DEFAULT_LAYOUT_USER);
+
+ metadata.setBaseMetadata(baseMetadata);
+
+ layout.setDefinition(DEFAULT_LAYOUT_DEFINITION);
+ layout.setMetadata(metadata);
+
+ return layoutRepository.save(layout);
+ }
+
+ 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();
+ metadataRequest.setBaseMetadata(baseMetadata);
+
+ LayoutRequestDTO layoutRequest = new LayoutRequestDTO();
+ layoutRequest.setDefinition(DEFAULT_LAYOUT_DEFINITION);
+ layoutRequest.setMetadata(metadataRequest);
+
+ return layoutRequest;
+ }
+}
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
new file mode 100644
index 000000000..5ec50e5d7
--- /dev/null
+++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java
@@ -0,0 +1,111 @@
+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;
+import org.finos.vuu.layoutserver.repository.LayoutRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+@ExtendWith(MockitoExtension.class)
+class LayoutServiceTest {
+
+ private static final UUID LAYOUT_ID = UUID.randomUUID();
+ public static final UUID METADATA_ID = UUID.randomUUID();
+
+ @Mock
+ private LayoutRepository layoutRepository;
+
+ @InjectMocks
+ private LayoutService layoutService;
+
+ private Layout layout;
+
+ @BeforeEach
+ public void setup() {
+ BaseMetadata baseMetadata = new BaseMetadata();
+ baseMetadata.setName("Test Name");
+ baseMetadata.setGroup("Test Group");
+ baseMetadata.setScreenshot("Test Screenshot");
+ baseMetadata.setUser("Test User");
+
+ Metadata metadata = Metadata.builder().id(METADATA_ID).baseMetadata(baseMetadata).build();
+
+ layout = new Layout();
+ layout.setId(LAYOUT_ID);
+ layout.setDefinition("");
+ layout.setMetadata(metadata);
+ }
+
+ @Test
+ void getLayout_layoutExists_returnsLayout() {
+ when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout));
+
+ assertThat(layoutService.getLayout(LAYOUT_ID)).isEqualTo(layout);
+ }
+
+ @Test
+ void getLayout_noLayoutsExist_throwsNotFoundException() {
+ when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class,
+ () -> layoutService.getLayout(LAYOUT_ID));
+ }
+
+ @Test
+ void createLayout_anyLayout_returnsLayoutId() {
+ when(layoutRepository.save(layout)).thenReturn(layout);
+
+ assertThat(layoutService.createLayout(layout)).isEqualTo(LAYOUT_ID);
+ }
+
+ @Test
+ void updateLayout_layoutExists_callsRepositorySave() {
+ when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout));
+
+ layoutService.updateLayout(LAYOUT_ID, layout);
+
+ verify(layoutRepository, times(1)).save(layout);
+ }
+
+ @Test
+ void updateLayout_layoutDoesNotExist_throwsNoSuchElementException() {
+ when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty());
+
+ assertThrows(NoSuchElementException.class,
+ () -> layoutService.updateLayout(LAYOUT_ID, layout));
+ }
+
+ @Test
+ void deleteLayout_anyUUID_callsRepositoryDeleteById() {
+ layoutService.deleteLayout(LAYOUT_ID);
+
+ verify(layoutRepository, times(1)).deleteById(LAYOUT_ID);
+ }
+
+ @Test
+ void deleteLayout_noLayoutExists_throwsNoSuchElementException() {
+ doThrow(new EmptyResultDataAccessException(1))
+ .when(layoutRepository).deleteById(LAYOUT_ID);
+
+ assertThrows(NoSuchElementException.class,
+ () -> layoutService.deleteLayout(LAYOUT_ID));
+
+ verify(layoutRepository, times(1)).deleteById(LAYOUT_ID);
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..1539a9237
--- /dev/null
+++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java
@@ -0,0 +1,37 @@
+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;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class MetadataServiceTest {
+
+ @Mock
+ private MetadataRepository metadataRepository;
+
+ @InjectMocks
+ private MetadataService metadataService;
+
+ @Test
+ void getMetadata_metadataExists_returnsMetadata() {
+ Metadata metadata = Metadata.builder().build();
+
+ when(metadataRepository.findAll()).thenReturn(List.of(metadata));
+ assertThat(metadataService.getMetadata()).isEqualTo(List.of(metadata));
+ }
+
+ @Test
+ void getMetadata_noMetadataExists_returnsEmptyList() {
+ when(metadataRepository.findAll()).thenReturn(List.of());
+ assertThat(metadataService.getMetadata()).isEqualTo(List.of());
+ }
+}
\ No newline at end of file
diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties
new file mode 100644
index 000000000..2722b4aca
--- /dev/null
+++ b/layout-server/src/test/resources/application-test.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
\ No newline at end of file