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