diff --git a/spring-boot-with-integration-tests/.run/Spring Boot Application.run.xml b/spring-boot-with-integration-tests/.run/Spring Boot Application.run.xml
new file mode 100644
index 0000000..c0a2f79
--- /dev/null
+++ b/spring-boot-with-integration-tests/.run/Spring Boot Application.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-with-integration-tests/KV Database.postman_collection b/spring-boot-with-integration-tests/KV Database.postman_collection
new file mode 100644
index 0000000..5a2779e
--- /dev/null
+++ b/spring-boot-with-integration-tests/KV Database.postman_collection
@@ -0,0 +1,128 @@
+{
+ "info": {
+ "_postman_id": "b789b118-9f9f-4c1d-9fd5-497c7b2977a0",
+ "name": "InterviewApplication - KV Database",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "19593238"
+ },
+ "item": [
+ {
+ "name": "GET ALL",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8080/kv",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "kv"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET ONE",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "localhost:8080/kv",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "kv"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST CREATE",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "url": {
+ "raw": "localhost:8080/kv?key=KVONLine&value=test",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "kv"
+ ],
+ "query": [
+ {
+ "key": "key",
+ "value": "KVONLine"
+ },
+ {
+ "key": "value",
+ "value": "test"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "PUT UPDATE",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"kv_entity_id\": 2,\n \"kv_entity_key\": \"KVOnline\",\n \"kv_entity_value\": \"test\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8080/kv",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "kv"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "PUT UPDATE Copy",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"kv_entity_id\": 2,\n \"kv_entity_key\": \"KVOnline\",\n \"kv_entity_value\": \"test\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "localhost:8080/kv",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "kv"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/spring-boot-with-integration-tests/pom.xml b/spring-boot-with-integration-tests/pom.xml
new file mode 100644
index 0000000..ed45a76
--- /dev/null
+++ b/spring-boot-with-integration-tests/pom.xml
@@ -0,0 +1,123 @@
+
+
+ 4.0.0
+
+ netgloo
+ spring-boot-basewebapp
+ 0.0.1-SNAPSHOT
+ jar
+
+ spring-boot-with-integration-tests
+ Spring Boot base web application with integration tests
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.7.4
+
+
+
+
+ UTF-8
+ netgloo.Application
+ 1.8
+
+
+
+
+ org.mockito
+ mockito-core
+ 4.8.0
+ test
+
+
+ org.mockito
+ mockito-inline
+ 4.8.0
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+ 1.18.24
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.jetbrains
+ annotations
+ RELEASE
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-web-services
+ RELEASE
+
+
+ org.testng
+ testng
+ RELEASE
+ compile
+
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ compile
+
+
+ org.springframework
+ spring-test
+
+
+ org.springframework.boot
+ spring-boot-test-autoconfigure
+ 2.7.4
+ compile
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-with-integration-tests/readme.md b/spring-boot-with-integration-tests/readme.md
new file mode 100644
index 0000000..7b5c696
--- /dev/null
+++ b/spring-boot-with-integration-tests/readme.md
@@ -0,0 +1,29 @@
+## Spring Boot basic web application
+
+Basic Spring Boot web application serving static content.
+
+See here for more informations: http://blog.netgloo.com/2014/05/18/very-basic-web-application-with-spring-mvc-spring-boot-and-eclipse-sts/
+
+
+### Build and run
+
+#### Prerequisites
+
+- Java 8
+- Maven 3.0+
+
+#### Using the terminal
+
+Go on the project's root folder, then type:
+
+ $ mvn spring-boot:run
+
+#### From Eclipse (Spring Tool Suite)
+
+Import as *Existing Maven Project* and run it as *Spring Boot App*.
+
+
+### Usage
+
+- Launch the application and go on http://localhost:8080/
+- You can see the content from the static page `hello.html`
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/Application.java b/spring-boot-with-integration-tests/src/main/java/netgloo/Application.java
new file mode 100644
index 0000000..323244c
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/Application.java
@@ -0,0 +1,13 @@
+package netgloo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/controllers/KVController.java b/spring-boot-with-integration-tests/src/main/java/netgloo/controllers/KVController.java
new file mode 100644
index 0000000..dd7ae23
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/controllers/KVController.java
@@ -0,0 +1,101 @@
+package netgloo.controllers;
+
+
+import lombok.AllArgsConstructor;
+import lombok.extern.java.Log;
+import lombok.val;
+import netgloo.exceptions.APIException;
+import netgloo.models.KVEntity;
+import netgloo.service.KVService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController // Because of SpringBoot ResponseBody implementation. We are not returning a view.
+@AllArgsConstructor(onConstructor = @__(@Autowired))
+@Log
+public class KVController {
+
+ private final KVService kvService;
+
+ /**
+ * Retrieve a key value pair from the database
+ *
+ * @param id The key to retrieve from the database.
+ * @return the value associated with the key.
+ */
+ @GetMapping("/kv/{id}")
+ // Could use @GetMapping instead, but this is more explicit.
+ public ResponseEntity get(HttpServletRequest request, @PathVariable Long id) throws APIException {
+ log.entering(this.getClass().getName(), "get", id);
+ val result = kvService.get(id);
+ log.exiting(this.getClass().getName(), "get", result);
+ return new ResponseEntity<>(result, HttpStatus.OK);
+ }
+
+ /**
+ * This is a GET all request. It is used to retrieve all data from the server, but not to modify it.
+ *
+ * @return all records in the database.
+ */
+ @GetMapping("/kv")
+ public ResponseEntity> getAll() {
+ log.entering(this.getClass().getName(), "getAll");
+ val result = kvService.findAll();
+ log.exiting(this.getClass().getName(), "getAll", result);
+ return new ResponseEntity<>(result, HttpStatus.OK);
+ }
+
+ /**
+ * This is a POST request. It is used to create a whole new record for example, customer information, file upload, etc.
+ *
+ * @param key The key to store in the database.
+ * @param value The value to store in the database.
+ * @return the value associated with the key.
+ */
+ @PostMapping("/kv")
+ // Could use @PostMapping instead, but this is more explicit.
+ public ResponseEntity post(@RequestParam String key, @RequestParam String value) throws APIException {
+ log.entering(this.getClass().getName(), "post", key);
+ val result = kvService.post(key, value);
+ log.exiting(this.getClass().getName(), "post", result);
+ return new ResponseEntity<>(result, HttpStatus.CREATED);
+ }
+
+ /**
+ * This is a PUT request. It is used to update an existing record.
+ *
+ * @param kvEntity The entity to update
+ * @return the value associated with the key.
+ */
+ @PutMapping("/kv")
+ // Could use @PutMapping instead, but this is more explicit.
+ public ResponseEntity put(@RequestBody @Valid KVEntity kvEntity) throws APIException {
+ log.entering(this.getClass().getName(), "put", kvEntity);
+ kvService.put(kvEntity);
+ log.exiting(this.getClass().getName(), "put");
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+
+ /**
+ * This is a DELETE request. It is used to delete an existing record.
+ *
+ * @param id The key to delete from the database.
+ * @return the value associated with the key.
+ */
+ @DeleteMapping("/kv/{id}")
+ // Could use @DeleteMapping instead, but this is more explicit.
+ public ResponseEntity delete(@PathVariable Long id) throws APIException {
+ log.entering(this.getClass().getName(), "delete", id);
+ kvService.delete(id);
+ log.exiting(this.getClass().getName(), "delete");
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+
+
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIControllerAdvice.java b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIControllerAdvice.java
new file mode 100644
index 0000000..b0600e1
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIControllerAdvice.java
@@ -0,0 +1,41 @@
+package netgloo.exceptions;
+
+import lombok.extern.java.Log;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+import java.util.Optional;
+
+@ControllerAdvice(annotations = RestController.class)
+@RequestMapping(produces = "application/api.error+json")
+@Log
+public class APIControllerAdvice extends ResponseEntityExceptionHandler {
+ @ExceptionHandler(APIIdNotFoundException.class)
+ public ResponseEntity notFoundException(final APIIdNotFoundException e) {
+ log.info("InterviewKeyNotFoundException: " + e.getMessage() + " " + e.getId());
+ return error(e, HttpStatus.NOT_FOUND, e.getId().toString());
+ }
+
+ @ExceptionHandler(java.lang.IllegalArgumentException.class)
+ public ResponseEntity argumentNotValid(final APIIllegalArgumentException e) {
+ log.info("IllegalArgumentException handler executed");
+ return error(e, HttpStatus.BAD_REQUEST, e.getMessage());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity internalServerError(final Exception e) {
+ log.severe("Internal server error: " + e.getMessage() + " " + e.getStackTrace());
+ return error(e, HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+
+ private ResponseEntity error(final Exception exception, final HttpStatus httpStatus, final String logRef) {
+ final String message = Optional.of(exception.getMessage()).orElse(exception.getClass().getSimpleName());
+ return new ResponseEntity<>(new APIError(logRef, message, exception), httpStatus);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIError.java b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIError.java
new file mode 100644
index 0000000..ac308f2
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIError.java
@@ -0,0 +1,16 @@
+package netgloo.exceptions;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class APIError {
+ @JsonProperty("traceID")
+ private final String logRef;
+ @JsonProperty("message")
+ private final String message;
+ @JsonProperty("cause")
+ private final Exception cause;
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIException.java b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIException.java
new file mode 100644
index 0000000..2d18114
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIException.java
@@ -0,0 +1,12 @@
+package netgloo.exceptions;
+
+
+public abstract class APIException extends Exception {
+ protected APIException(String message) {
+ super(message);
+ }
+
+ protected APIException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIdNotFoundException.java b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIdNotFoundException.java
new file mode 100644
index 0000000..b999135
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIdNotFoundException.java
@@ -0,0 +1,17 @@
+package netgloo.exceptions;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@EqualsAndHashCode(of = "id", callSuper = true)
+public class APIIdNotFoundException extends APIException {
+ private final Long id;
+
+ public APIIdNotFoundException(final Long id) {
+ super("Interview with id " + id + " not found");
+ this.id = id;
+ }
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIllegalArgumentException.java b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIllegalArgumentException.java
new file mode 100644
index 0000000..f6ba205
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/exceptions/APIIllegalArgumentException.java
@@ -0,0 +1,11 @@
+package netgloo.exceptions;
+
+public class APIIllegalArgumentException extends APIException {
+ public APIIllegalArgumentException(String message) {
+ super(message);
+ }
+
+ public APIIllegalArgumentException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/models/KVEntity.java b/spring-boot-with-integration-tests/src/main/java/netgloo/models/KVEntity.java
new file mode 100644
index 0000000..2c1fe10
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/models/KVEntity.java
@@ -0,0 +1,50 @@
+package netgloo.models;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+import org.hibernate.validator.constraints.NotBlank;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.validation.annotation.Validated;
+
+import javax.persistence.*;
+
+@Entity(name = "KVEntity")
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Validated
+@NoArgsConstructor
+public class KVEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(name = "kv_entity_id", nullable = false, unique = true)
+ @JsonProperty("kv_entity_id")
+ private Long KVEntityId;
+
+ @Column(name = "kv_entity_key", nullable = false)
+ @JsonProperty("kv_entity_key")
+ private String KVEntityKey;
+
+
+ @Column(name = "kv_entity_value", length = 10000) // NULLABLE
+ @JsonProperty("kv_entity_value")
+ @Nullable
+ private String KVEntityValue;
+
+ public KVEntity(String key, @Nullable String value) {
+ this.KVEntityKey = key;
+ this.KVEntityValue = value;
+ }
+
+ @Override
+ public String toString() {
+ return "KVEntity{" +
+ "id=" + this.KVEntityId +
+ ", key='" + KVEntityKey + '\'' +
+ ", value='" + KVEntityValue + '\'' +
+ '}';
+ }
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/repository/KVDatabase.java b/spring-boot-with-integration-tests/src/main/java/netgloo/repository/KVDatabase.java
new file mode 100644
index 0000000..65dca28
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/repository/KVDatabase.java
@@ -0,0 +1,9 @@
+package netgloo.repository;
+
+import netgloo.models.KVEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface KVDatabase extends JpaRepository {
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/service/KVService.java b/spring-boot-with-integration-tests/src/main/java/netgloo/service/KVService.java
new file mode 100644
index 0000000..6bd8ce4
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/service/KVService.java
@@ -0,0 +1,73 @@
+package netgloo.service;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.java.Log;
+import lombok.val;
+import netgloo.exceptions.APIException;
+import netgloo.exceptions.APIIdNotFoundException;
+import netgloo.models.KVEntity;
+import netgloo.repository.KVDatabase;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleMatcher;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@AllArgsConstructor(onConstructor = @__(@Autowired))
+@Log
+public class KVService {
+
+ @Autowired
+ private final KVDatabase kvDatabase;
+
+
+ public KVEntity get(Long id) throws APIException {
+ log.entering(this.getClass().getName(), "post", id);
+ val result = kvDatabase.findById(id);
+ log.exiting(this.getClass().getName(), "post", result);
+ return result.orElseThrow(() -> new APIIdNotFoundException(id));
+ }
+
+ public Long post(String key, String value) throws APIException {
+ log.entering(this.getClass().getName(), "post", key);
+ val result = kvDatabase.save(new KVEntity(key, value));
+ log.exiting(this.getClass().getName(), "post", result);
+ return result.getKVEntityId().longValue();
+ }
+
+ public KVEntity put(KVEntity entity) throws APIException {
+ log.entering(this.getClass().getName(), "put");
+ val result = kvDatabase
+ .findById(entity.getKVEntityId())
+ .map(kvEntity -> {
+ kvEntity.setKVEntityKey(entity.getKVEntityKey());
+ kvEntity.setKVEntityValue(entity.getKVEntityValue());
+ return kvDatabase.save(kvEntity);
+ })
+ .orElseThrow(() -> new APIIdNotFoundException(entity.getKVEntityId()));
+ log.exiting(this.getClass().getName(), "post", result);
+ return result;
+ }
+
+ public void delete(Long id) {
+ log.entering(this.getClass().getName(), "delete", id);
+ kvDatabase.deleteById(id);
+ log.exiting(this.getClass().getName(), "delete");
+
+ }
+
+ public List findAll() {
+ return kvDatabase.findAll();
+ }
+
+ public List searchByKey(String key) {
+ KVEntity kvEntity = new KVEntity(null, key, null);
+ ExampleMatcher matcher = ExampleMatcher.matching()
+ .withMatcher("key", ExampleMatcher.GenericPropertyMatcher::contains)
+ .withIgnorePaths("id", "value");
+ Example example = Example.of(kvEntity, matcher);
+ return kvDatabase.findAll(example);
+ }
+}
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVControllerTest.java b/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVControllerTest.java
new file mode 100644
index 0000000..269d45d
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVControllerTest.java
@@ -0,0 +1,163 @@
+package netgloo.test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import lombok.val;
+import netgloo.controllers.KVController;
+import netgloo.models.KVEntity;
+import netgloo.repository.KVDatabase;
+import netgloo.service.KVService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+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.test.web.servlet.MockMvc;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class KVControllerTest {
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private static final SecureRandom random = new SecureRandom();
+
+ @Autowired
+ MockMvc mockMvc;
+ @InjectMocks
+ private KVController controller;
+
+ @MockBean
+ KVDatabase kvDatabase;
+
+ @MockBean
+ KVService kvService;
+
+ private static final KVEntity TEST_FOUND_KV_ENTITY =
+ new KVEntity(1L, "TEST", "TEST");
+
+ @BeforeEach
+ void setUp() {
+ assertNotNull(controller);
+ assertNotNull(mockMvc);
+
+ doReturn(Optional.of(TEST_FOUND_KV_ENTITY))
+ .when(kvDatabase)
+ .findById(any(Long.class));
+ }
+
+ @AfterEach
+ void tearDown() {
+ mockMvc = null;
+ controller = null;
+ kvDatabase = null;
+ kvService = null;
+ }
+
+ @Test
+ void contextLoads() throws Exception {
+ assertNotNull(controller);
+ assertNotNull(mockMvc);
+ }
+
+ @Test
+ void getTest() throws Exception {
+ when(kvService.get(any(Long.class))).thenReturn(TEST_FOUND_KV_ENTITY);
+
+ val result = mockMvc.perform(get("/kv/1"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andReturn();
+
+ assertNotNull(result);
+ assertEquals(TEST_FOUND_KV_ENTITY, mapper.readValue(result.getResponse().getContentAsString(), KVEntity.class));
+ }
+
+ @Test
+ void getAllTest() throws Exception {
+ when(kvService.findAll()).thenReturn(Arrays.asList(TEST_FOUND_KV_ENTITY));
+
+ val result = mockMvc.perform(
+ get("/kv"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andReturn();
+
+ assertNotNull(result);
+ assertEquals(mapper.readValue(mapper.writeValueAsString(Arrays.asList(TEST_FOUND_KV_ENTITY)), List.class),
+ mapper.readValue(result.getResponse().getContentAsString(), List.class));
+ verify(kvService, times(1)).findAll();
+ }
+
+ @Test
+ void postTest() throws Exception {
+ doReturn(1L)
+ .when(kvService)
+ .post(anyString(), anyString());
+
+ val result = mockMvc.perform(
+ post("/kv")
+ .param("key", "TEST")
+ .param("value", "TEST"))
+ .andDo(print())
+ .andExpect(status().isCreated())
+ .andReturn();
+
+ assertNotNull(result);
+ assertEquals("1", result.getResponse().getContentAsString());
+ verify(kvService, times(1)).post(TEST_FOUND_KV_ENTITY.getKVEntityKey(), TEST_FOUND_KV_ENTITY.getKVEntityValue());
+ }
+
+ @Test
+ void putTest() throws Exception {
+ doReturn(TEST_FOUND_KV_ENTITY)
+ .when(kvService)
+ .put(any(KVEntity.class));
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, false);
+ ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
+ String requestJson = ow.writeValueAsString(TEST_FOUND_KV_ENTITY);
+
+ val result = mockMvc.perform(
+ put("/kv")
+ .content(requestJson)
+ .contentType("application/json"))
+ .andDo(print())
+ .andExpect(status().isNoContent())
+ .andReturn();
+
+ assertNotNull(result);
+ assertEquals("", result.getResponse().getContentAsString());
+ verify(kvService, times(1)).put(TEST_FOUND_KV_ENTITY);
+ }
+
+ @Test
+ void deleteTest() throws Exception {
+ doNothing().when(kvService).delete(any(Long.class));
+
+ val result = mockMvc.perform(
+ delete("/kv/1"))
+ .andDo(print())
+ .andExpect(status().isNoContent())
+ .andReturn();
+
+ assertNotNull(result);
+ verify(kvService, times(1)).delete(any(Long.class));
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVServiceTest.java b/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVServiceTest.java
new file mode 100644
index 0000000..a89cc1f
--- /dev/null
+++ b/spring-boot-with-integration-tests/src/main/java/netgloo/test/KVServiceTest.java
@@ -0,0 +1,113 @@
+package netgloo.test;
+
+import lombok.val;
+import netgloo.exceptions.APIException;
+import netgloo.models.KVEntity;
+import netgloo.repository.KVDatabase;
+import netgloo.service.KVService;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.data.domain.Example;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+
+@SpringBootTest
+class KVServiceTest {
+
+ @MockBean
+ KVDatabase kvDatabase;
+
+ @InjectMocks
+ KVService kvService;
+
+ private static final KVEntity TEST_FOUND_KV_ENTITY =
+ new KVEntity(1L, "TEST", "TEST");
+
+ @BeforeEach
+ void setUp() {
+ doReturn(Collections.singletonList(TEST_FOUND_KV_ENTITY)).when(kvDatabase).saveAll(any());
+
+ ReflectionTestUtils.setField(kvService, "kvDatabase", kvDatabase);
+ }
+
+ @AfterEach
+ void tearDown() {
+ reset(kvDatabase);
+ }
+
+ @Test
+ void contextLoads() {
+ assertNotNull(kvService);
+ assertNotNull(kvDatabase);
+ }
+
+ @Test
+ void getTest() throws APIException {
+ when(kvDatabase.findById(1L)).thenReturn(Optional.of(TEST_FOUND_KV_ENTITY));
+
+ verify(kvDatabase, times(1)).findById(1L);
+ }
+
+ @Test
+ void post() throws APIException {
+ when(kvDatabase.save(any())).thenReturn(TEST_FOUND_KV_ENTITY);
+
+ Long kvEntityId = kvService.post(TEST_FOUND_KV_ENTITY.getKVEntityKey(), TEST_FOUND_KV_ENTITY.getKVEntityValue());
+
+ assertEquals(TEST_FOUND_KV_ENTITY.getKVEntityId(), kvEntityId);
+ }
+
+ @Test
+ void put() throws APIException {
+ doReturn(TEST_FOUND_KV_ENTITY).when(kvDatabase).save(any());
+ doReturn(Optional.of(TEST_FOUND_KV_ENTITY)).when(kvDatabase).findById(1L);
+
+ KVEntity kvEntity = kvService.put(TEST_FOUND_KV_ENTITY);
+
+ assertEquals(TEST_FOUND_KV_ENTITY.getKVEntityId(), kvEntity.getKVEntityId());
+ }
+
+ @Test
+ void delete() {
+ doNothing().when(kvDatabase).deleteById(TEST_FOUND_KV_ENTITY.getKVEntityId());
+
+ kvService.delete(TEST_FOUND_KV_ENTITY.getKVEntityId());
+
+ verify(kvDatabase, times(1)).deleteById(TEST_FOUND_KV_ENTITY.getKVEntityId());
+ }
+
+ @Test
+ void findAll() {
+ doReturn(Collections.singletonList(TEST_FOUND_KV_ENTITY)).when(kvDatabase).findAll();
+
+ val response =
+ kvService.findAll();
+
+ assertEquals(1, response.size());
+ verify(kvDatabase, times(1)).findAll();
+
+ }
+
+ @Test
+ void searchByKey() {
+ doReturn(Collections.singletonList(TEST_FOUND_KV_ENTITY)).when(kvDatabase)
+ .findAll(any(Example.class));
+
+ val response =
+ kvService.searchByKey(TEST_FOUND_KV_ENTITY.getKVEntityKey());
+
+ assertEquals(1, response.size());
+ verify(kvDatabase, times(1)).findAll(any(Example.class));
+ }
+}
\ No newline at end of file