diff --git a/.github/workflows/sonarqube-scanner.yaml b/.github/workflows/sonarqube-scanner.yaml index 44f32e8..627517b 100644 --- a/.github/workflows/sonarqube-scanner.yaml +++ b/.github/workflows/sonarqube-scanner.yaml @@ -25,7 +25,7 @@ jobs: if [ ! -z "${{ secrets.SONAR_TOKEN }}" ] && [ ! -z "${{ secrets.SONAR_HOST_URL }}" ]; then echo "::set-output name=ok::true" fi - + sonarqube: needs: - secrets-gate diff --git a/Makefile b/Makefile index 491d62e..cc3ecb1 100644 --- a/Makefile +++ b/Makefile @@ -9,4 +9,4 @@ up: test: docker-compose run --rm --no-deps -p "8080:8080" java-skeleton-api gradle test -coverage: test \ No newline at end of file +coverage: test diff --git a/build.gradle b/build.gradle index 38c066e..a4d4fe2 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'com.h2database:h2:2.1.212' @@ -92,4 +94,4 @@ tasks.withType(Test) { } } } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index f7be341..74c98bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,8 @@ services: restart: on-failure volumes: - rv-volume:/var/lib/postgresql/data + ports: + - "5432:5432" env_file: - postgres.dev.env networks: diff --git a/postgres.dev.env b/postgres.dev.env index 3029892..8581d0c 100644 --- a/postgres.dev.env +++ b/postgres.dev.env @@ -1,4 +1,4 @@ POSTGRES_NAME=postgres-skeleton-db POSTGRES_DB=postgres_rv_database POSTGRES_USER=rv_user -POSTGRES_PASSWORD=rv_password \ No newline at end of file +POSTGRES_PASSWORD=rv_password diff --git a/src/main/java/com/rviewer/skeletons/domain/model/Cart.java b/src/main/java/com/rviewer/skeletons/domain/model/Cart.java new file mode 100644 index 0000000..391ee65 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/domain/model/Cart.java @@ -0,0 +1,18 @@ +package com.rviewer.skeletons.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; + +import java.util.List; +import java.util.UUID; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@With +public class Cart { + private UUID id; + private List items; +} diff --git a/src/main/java/com/rviewer/skeletons/domain/model/Item.java b/src/main/java/com/rviewer/skeletons/domain/model/Item.java new file mode 100644 index 0000000..66f884d --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/domain/model/Item.java @@ -0,0 +1,27 @@ +package com.rviewer.skeletons.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; +import org.springframework.format.annotation.NumberFormat; + +import javax.validation.constraints.NotNull; +import java.util.UUID; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@With +public class Item { + @NotNull + private UUID id; + @NotNull + private String name; + @NotNull + @NumberFormat + private Integer quantity; + @NotNull + @NumberFormat + private Float price; +} diff --git a/src/main/java/com/rviewer/skeletons/domain/ports/primary/CartServicePort.java b/src/main/java/com/rviewer/skeletons/domain/ports/primary/CartServicePort.java new file mode 100644 index 0000000..e3d1ff6 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/domain/ports/primary/CartServicePort.java @@ -0,0 +1,11 @@ +package com.rviewer.skeletons.domain.ports.primary; + +import com.rviewer.skeletons.domain.model.Cart; + +import java.util.Optional; + +public interface CartServicePort { + Optional get(String id); + void save(Cart cart); + void delete(String id); +} diff --git a/src/main/java/com/rviewer/skeletons/domain/ports/secondary/DatabasePort.java b/src/main/java/com/rviewer/skeletons/domain/ports/secondary/DatabasePort.java new file mode 100644 index 0000000..8b40a64 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/domain/ports/secondary/DatabasePort.java @@ -0,0 +1,12 @@ +package com.rviewer.skeletons.domain.ports.secondary; + +import com.rviewer.skeletons.domain.model.Cart; + +import java.util.Optional; + +public interface DatabasePort { + + Optional get(String id); + void save(Cart cart); + void delete(String id); +} diff --git a/src/main/java/com/rviewer/skeletons/domain/responses/PongResponse.java b/src/main/java/com/rviewer/skeletons/domain/responses/PongResponse.java deleted file mode 100644 index 651efce..0000000 --- a/src/main/java/com/rviewer/skeletons/domain/responses/PongResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.rviewer.skeletons.domain.responses; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class PongResponse { - - private String message; - private int number; -} diff --git a/src/main/java/com/rviewer/skeletons/domain/service/CartService.java b/src/main/java/com/rviewer/skeletons/domain/service/CartService.java new file mode 100644 index 0000000..07ab3fd --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/domain/service/CartService.java @@ -0,0 +1,34 @@ +package com.rviewer.skeletons.domain.service; + +import com.rviewer.skeletons.domain.model.Cart; +import com.rviewer.skeletons.domain.ports.primary.CartServicePort; +import com.rviewer.skeletons.domain.ports.secondary.DatabasePort; +import org.springframework.stereotype.Component; + +import javax.transaction.Transactional; +import java.util.Optional; + +@Component +@Transactional +public class CartService implements CartServicePort { + private final DatabasePort databasePort; + + public CartService(DatabasePort databasePort) { + this.databasePort = databasePort; + } + + @Override + public Optional get(String id) { + return databasePort.get(id); + } + + @Override + public void save(Cart cart) { + databasePort.save(cart); + } + + @Override + public void delete(String id) { + databasePort.delete(id); + } +} diff --git a/src/main/java/com/rviewer/skeletons/domain/services/PongService.java b/src/main/java/com/rviewer/skeletons/domain/services/PongService.java deleted file mode 100644 index 1cc4269..0000000 --- a/src/main/java/com/rviewer/skeletons/domain/services/PongService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.rviewer.skeletons.domain.services; - -import com.rviewer.skeletons.domain.responses.PongResponse; -import com.rviewer.skeletons.domain.services.persistence.DatabaseConnector; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class PongService { - - @Autowired - DatabaseConnector postgresConnector; - - public PongResponse getPong() { - return new PongResponse("pong", postgresConnector.getConnectionStatus()); - } -} diff --git a/src/main/java/com/rviewer/skeletons/domain/services/persistence/DatabaseConnector.java b/src/main/java/com/rviewer/skeletons/domain/services/persistence/DatabaseConnector.java deleted file mode 100644 index 3065c03..0000000 --- a/src/main/java/com/rviewer/skeletons/domain/services/persistence/DatabaseConnector.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.rviewer.skeletons.domain.services.persistence; - -public interface DatabaseConnector { - - public int getConnectionStatus(); -} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/controllers/PingController.java b/src/main/java/com/rviewer/skeletons/infrastructure/controllers/PingController.java deleted file mode 100644 index 0c5356d..0000000 --- a/src/main/java/com/rviewer/skeletons/infrastructure/controllers/PingController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.rviewer.skeletons.infrastructure.controllers; - -import com.rviewer.skeletons.domain.services.PongService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/") -public class PingController { - - @Autowired - private PongService pongService; - - @GetMapping("/ping") - public ResponseEntity getPing() { - return ResponseEntity.ok(pongService.getPong()); - } -} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartController.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartController.java new file mode 100644 index 0000000..5f3a437 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartController.java @@ -0,0 +1,70 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.adapter; + +import com.rviewer.skeletons.domain.model.Cart; +import com.rviewer.skeletons.domain.model.Item; +import com.rviewer.skeletons.domain.service.CartService; +import com.rviewer.skeletons.infrastructure.inbound.api.exception.CartAlreadyExistsException; +import com.rviewer.skeletons.infrastructure.inbound.api.exception.CartNotFoundException; +import com.rviewer.skeletons.infrastructure.inbound.api.request.CreateCartReq; +import com.rviewer.skeletons.infrastructure.inbound.api.request.UpdateCartReq; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.UUID; + +@RestController +@RequestMapping("/carts") +public class CartController { + + private final CartService cartService; + + public CartController(CartService cartService) { + this.cartService = cartService; + } + + @GetMapping("/{id}") + public ResponseEntity getCart(@PathVariable String id) { + return ResponseEntity.status(HttpStatus.OK).body(cartService.get(id).orElseThrow(CartNotFoundException::new)); + } + + @PostMapping("/{id}") + public ResponseEntity saveCart(@PathVariable String id, @Valid @RequestBody CreateCartReq createCartReq) { + if (cartService.get(id).isPresent()) { + throw new CartAlreadyExistsException(); + } else { + cartService.save(new Cart(UUID.fromString(id), createCartReq.getItems() + .stream() + .map(item -> new Item(item.getId(), item.getName(), item.getQuantity(), item.getPrice())) + .toList())); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + } + + @PatchMapping("/{id}") + public ResponseEntity updateCart(@PathVariable String id, @Valid @RequestBody UpdateCartReq updateCartReq) { + Cart cart = cartService.get(id).orElseThrow(CartNotFoundException::new); + cart.setItems(updateCartReq.getItems()); + cartService.save(cart); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCart(@PathVariable String id) { + if (cartService.get(id).isEmpty()) { + throw new CartNotFoundException(); + } else { + cartService.delete(id); + return ResponseEntity.status(HttpStatus.OK).build(); + } + } + +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartAlreadyExistsException.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartAlreadyExistsException.java new file mode 100644 index 0000000..94b4def --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartAlreadyExistsException.java @@ -0,0 +1,13 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.CONFLICT) +public class CartAlreadyExistsException extends RuntimeException { + private static final String CART_ALREADY_EXISTS = "Invalid identifier."; + + public CartAlreadyExistsException() { + super(CART_ALREADY_EXISTS); + } +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartNotFoundException.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartNotFoundException.java new file mode 100644 index 0000000..5cb0609 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/CartNotFoundException.java @@ -0,0 +1,13 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class CartNotFoundException extends RuntimeException { + private static final String CART_NOT_FOUND = "Cart not found for the given ID"; + + public CartNotFoundException() { + super(CART_NOT_FOUND); + } +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/ControllerExceptionHandler.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/ControllerExceptionHandler.java new file mode 100644 index 0000000..66a6fa8 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/exception/ControllerExceptionHandler.java @@ -0,0 +1,32 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.exception; + +import org.springframework.http.HttpStatus; +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.bind.annotation.ResponseStatus; + +@ControllerAdvice +public class ControllerExceptionHandler { + + public static final String INVALID_BODY = "Invalid body provided, check the payload."; + + @ExceptionHandler(CartNotFoundException.class) + public ResponseEntity handleCartNotFound(CartNotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(CartAlreadyExistsException.class) + public ResponseEntity handleCartAlreadyExists(CartAlreadyExistsException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.CONFLICT); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleInvalidBody() { + return new ResponseEntity<>(INVALID_BODY, HttpStatus.BAD_REQUEST); + } + +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/CreateCartReq.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/CreateCartReq.java new file mode 100644 index 0000000..c2ef5b7 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/CreateCartReq.java @@ -0,0 +1,13 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.request; + +import com.rviewer.skeletons.domain.model.Item; +import lombok.Getter; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +public class CreateCartReq { + @NotNull + private List items; +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/UpdateCartReq.java b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/UpdateCartReq.java new file mode 100644 index 0000000..4594c30 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/inbound/api/request/UpdateCartReq.java @@ -0,0 +1,13 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.request; + +import com.rviewer.skeletons.domain.model.Item; +import lombok.Getter; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +public class UpdateCartReq { + @NotNull + private List items; +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/adapter/PostgresAdapter.java b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/adapter/PostgresAdapter.java new file mode 100644 index 0000000..1c30881 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/adapter/PostgresAdapter.java @@ -0,0 +1,35 @@ +package com.rviewer.skeletons.infrastructure.outbound.database.adapter; + +import com.rviewer.skeletons.domain.model.Cart; +import com.rviewer.skeletons.domain.ports.secondary.DatabasePort; +import com.rviewer.skeletons.infrastructure.outbound.database.repository.PostgresCartRepository; +import com.rviewer.skeletons.infrastructure.outbound.database.dto.PostgresCart; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Component +public class PostgresAdapter implements DatabasePort { + + private final PostgresCartRepository postgresCartRepository; + + public PostgresAdapter(PostgresCartRepository postgresCartRepository) { + this.postgresCartRepository = postgresCartRepository; + } + + @Override + public Optional get(String id) { + return postgresCartRepository.findById(UUID.fromString(id)).map(PostgresCart::toDomain); + } + + @Override + public void save(Cart cart) { + postgresCartRepository.save(PostgresCart.fromDomain(cart)); + } + + @Override + public void delete(String id) { + postgresCartRepository.deleteById(UUID.fromString(id)); + } +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresCart.java b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresCart.java new file mode 100644 index 0000000..b50905c --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresCart.java @@ -0,0 +1,42 @@ +package com.rviewer.skeletons.infrastructure.outbound.database.dto; + +import com.rviewer.skeletons.domain.model.Cart; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import java.util.List; +import java.util.UUID; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@With +public class PostgresCart { + @Id + private UUID id; + @OneToMany(cascade= CascadeType.ALL) + private List postgresItems; + + public static PostgresCart fromDomain(Cart cart) { + return new PostgresCart() + .withId(cart.getId()) + .withPostgresItems(cart.getItems() + .stream() + .map(PostgresItem::fromDomain) + .toList()); + } + + public Cart toDomain() { + return new Cart() + .withId(id) + .withItems(postgresItems + .stream() + .map(PostgresItem::toDomain) + .toList()); + } +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresItem.java b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresItem.java new file mode 100644 index 0000000..6d5ee2d --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/dto/PostgresItem.java @@ -0,0 +1,38 @@ +package com.rviewer.skeletons.infrastructure.outbound.database.dto; + +import com.rviewer.skeletons.domain.model.Item; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.With; + +import javax.persistence.Entity; +import javax.persistence.Id; +import java.util.UUID; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@With +public class PostgresItem { + @Id + private UUID id; + private String name; + private Integer quantity; + private Float price; + + public static PostgresItem fromDomain(Item item) { + return new PostgresItem() + .withId(item.getId()) + .withName(item.getName()) + .withQuantity(item.getQuantity()) + .withPrice(item.getPrice()); + } + + public Item toDomain() { + return new Item() + .withId(id) + .withName(name) + .withQuantity(quantity) + .withPrice(price); + } +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/repository/PostgresCartRepository.java b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/repository/PostgresCartRepository.java new file mode 100644 index 0000000..b116eb2 --- /dev/null +++ b/src/main/java/com/rviewer/skeletons/infrastructure/outbound/database/repository/PostgresCartRepository.java @@ -0,0 +1,14 @@ +package com.rviewer.skeletons.infrastructure.outbound.database.repository; + +import com.rviewer.skeletons.infrastructure.outbound.database.dto.PostgresCart; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface PostgresCartRepository extends CrudRepository { + Optional findById(UUID id); + void deleteById(UUID id); +} diff --git a/src/main/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnector.java b/src/main/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnector.java deleted file mode 100644 index a160b12..0000000 --- a/src/main/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnector.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.rviewer.skeletons.infrastructure.persistence; - -import com.rviewer.skeletons.domain.services.persistence.DatabaseConnector; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; - -@Component -public class PostgresConnector implements DatabaseConnector { - - @Autowired - JdbcTemplate jdbcTemplate; - - public int getConnectionStatus() { - return jdbcTemplate.queryForObject("SELECT 1+1", Integer.class); - } - -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 14a0616..e25079a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,11 @@ server.port=8080 -application.title=Java skeleton +application.title=Cartfidential application.version=1.0.0 spring.datasource.initialization-mode=always spring.datasource.username = rv_user spring.datasource.password = rv_password spring.datasource.driverClassName = org.postgresql.Driver -spring.datasource.url = jdbc:postgresql://postgres-skeleton-db:5432/postgres_rv_database +spring.jpa.hibernate.ddl-auto = update +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.datasource.url = jdbc:postgresql://localhost:5432/postgres_rv_database diff --git a/src/test/java/com/rviewer/skeletons/infrastructure/controllers/PingControllerTest.java b/src/test/java/com/rviewer/skeletons/infrastructure/controllers/PingControllerTest.java deleted file mode 100644 index 5eb8bdc..0000000 --- a/src/test/java/com/rviewer/skeletons/infrastructure/controllers/PingControllerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.rviewer.skeletons.infrastructure.controllers; - -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.test.web.servlet.MockMvc; - -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -public class PingControllerTest { - - @Autowired - private MockMvc mockMvc; - - @Test - public void getPing_success() throws Exception { - mockMvc - .perform(get("/ping")) - .andExpect(status().isOk()); - } - - @Test - public void getPing_returnsPong() throws Exception { - mockMvc - .perform(get("/ping")) - .andExpect(content().string(containsString("pong"))); - } -} diff --git a/src/test/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartControllerTest.java b/src/test/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartControllerTest.java new file mode 100644 index 0000000..93b8294 --- /dev/null +++ b/src/test/java/com/rviewer/skeletons/infrastructure/inbound/api/adapter/CartControllerTest.java @@ -0,0 +1,217 @@ +package com.rviewer.skeletons.infrastructure.inbound.api.adapter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rviewer.skeletons.domain.model.Cart; +import com.rviewer.skeletons.domain.model.Item; +import com.rviewer.skeletons.domain.service.CartService; +import com.rviewer.skeletons.infrastructure.inbound.api.exception.CartAlreadyExistsException; +import com.rviewer.skeletons.infrastructure.inbound.api.exception.CartNotFoundException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.web.bind.MethodArgumentNotValidException; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static com.rviewer.skeletons.infrastructure.inbound.api.exception.ControllerExceptionHandler.INVALID_BODY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class CartControllerTest { + + @Autowired + private MockMvc mockMvc; + @MockBean + private CartService cartService; + + @Test + void should_return_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart expectedCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.of(expectedCart)); + + mockMvc.perform(MockMvcRequestBuilders.get("/carts/" + id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(id.toString())) + .andExpect(jsonPath("$.items[0].id").value(item.getId().toString())) + .andExpect(jsonPath("$.items[0].name").value(item.getName())) + .andExpect(jsonPath("$.items[0].quantity").value(item.getQuantity())) + .andExpect(jsonPath("$.items[0].price").value(item.getPrice())); + } + + @Test + void should_not_return_non_existing_cart() throws Exception { + UUID id = UUID.randomUUID(); + + when(cartService.get(id.toString())).thenReturn(Optional.empty()); + + mockMvc.perform(MockMvcRequestBuilders.get("/carts/" + id)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof CartNotFoundException)); + } + + @Test + void should_save_new_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart newCart = new Cart(id, List.of(item)); + + mockMvc.perform(MockMvcRequestBuilders.post("/carts/" + id) + .content(asJsonString(newCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + @Test + void should_not_save_old_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart oldCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.of(oldCart)); + + mockMvc.perform(MockMvcRequestBuilders.post("/carts/" + id) + .content(asJsonString(oldCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof CartAlreadyExistsException)) + ; + } + + @Test + void should_not_save_null_cart() throws Exception { + UUID id = UUID.randomUUID(); + Cart nullCart = new Cart(id, null); + + mockMvc.perform(MockMvcRequestBuilders.post("/carts/" + id) + .content(asJsonString(nullCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof MethodArgumentNotValidException)) + .andExpect(result -> assertEquals(INVALID_BODY, result.getResponse().getContentAsString())); + } + + @Test + void should_not_save_bad_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart badCart = new Cart(id, List.of(item)); + + mockMvc.perform(MockMvcRequestBuilders.post("/carts/" + id) + .content(asJsonString(badCart).replace("100.0", "")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof HttpMessageNotReadableException)) + .andExpect(result -> assertEquals(INVALID_BODY, result.getResponse().getContentAsString())); + } + + @Test + void should_update_old_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart oldCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.of(oldCart)); + + mockMvc.perform(MockMvcRequestBuilders.patch("/carts/" + id) + .content(asJsonString(oldCart).replace("100.0", "1000.0")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(result -> assertEquals(1000F, cartService.get(id.toString()).orElseThrow().getItems().get(0).getPrice())); + } + + @Test + void should_not_update_non_existing_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart nonExistingCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.empty()); + + mockMvc.perform(MockMvcRequestBuilders.patch("/carts/" + id) + .content(asJsonString(nonExistingCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof CartNotFoundException)); + } + + @Test + void should_not_update_null_cart() throws Exception { + UUID id = UUID.randomUUID(); + Cart nullCart = new Cart(id, null); + + mockMvc.perform(MockMvcRequestBuilders.patch("/carts/" + id) + .content(asJsonString(nullCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof MethodArgumentNotValidException)) + .andExpect(result -> assertEquals(INVALID_BODY, result.getResponse().getContentAsString())); + } + + @Test + void should_not_update_bad_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart badCart = new Cart(id, List.of(item)); + + mockMvc.perform(MockMvcRequestBuilders.patch("/carts/" + id) + .content(asJsonString(badCart).replace("100.0", "")) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof HttpMessageNotReadableException)) + .andExpect(result -> assertEquals(INVALID_BODY, result.getResponse().getContentAsString())) + ; + } + + @Test + void should_delete_old_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart oldCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.of(oldCart)); + + mockMvc.perform(MockMvcRequestBuilders.delete("/carts/" + id) + .content(asJsonString(oldCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void should_not_delete_non_existing_cart() throws Exception { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart nonExistingCart = new Cart(id, List.of(item)); + + when(cartService.get(id.toString())).thenReturn(Optional.empty()); + + mockMvc.perform(MockMvcRequestBuilders.delete("/carts/" + id) + .content(asJsonString(nonExistingCart)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(result -> assertTrue(result.getResolvedException() instanceof CartNotFoundException)); + } + + private static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/rviewer/skeletons/infrastructure/outbound/adapter/PostgresAdapterTest.java b/src/test/java/com/rviewer/skeletons/infrastructure/outbound/adapter/PostgresAdapterTest.java new file mode 100644 index 0000000..1325ca4 --- /dev/null +++ b/src/test/java/com/rviewer/skeletons/infrastructure/outbound/adapter/PostgresAdapterTest.java @@ -0,0 +1,75 @@ +package com.rviewer.skeletons.infrastructure.outbound.adapter; + +import com.rviewer.skeletons.domain.model.Cart; +import com.rviewer.skeletons.domain.model.Item; +import com.rviewer.skeletons.infrastructure.outbound.database.adapter.PostgresAdapter; +import com.rviewer.skeletons.infrastructure.outbound.database.dto.PostgresCart; +import com.rviewer.skeletons.infrastructure.outbound.database.repository.PostgresCartRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest +class PostgresAdapterTest { + + @Autowired + private PostgresAdapter postgresAdapter; + @Mock + private PostgresCartRepository postgresCartRepository; + + @BeforeEach + void init() { + postgresAdapter = new PostgresAdapter(postgresCartRepository); + } + + @Test + void should_get_cart() { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart expectedCart = new Cart(id, List.of(item)); + + when(postgresCartRepository.findById(id)).thenReturn(Optional.ofNullable(PostgresCart.fromDomain(expectedCart))); + + Optional result = postgresAdapter.get(id.toString()); + assertThat(Optional.of(result)).hasValue(Optional.of(expectedCart)); + } + + @Test + void should_save_cart() { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart newCart = new Cart(id, List.of(item)); + + postgresAdapter.save(newCart); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PostgresCart.class); + verify(postgresCartRepository).save(captor.capture()); + assertEquals(captor.getValue().toDomain(), newCart); + } + + @Test + void should_delete_cart() { + UUID id = UUID.randomUUID(); + Item item = new Item(UUID.randomUUID(), "item", 1, 100F); + Cart cartToDelete = new Cart(id, List.of(item)); + + when(postgresCartRepository.findById(id)).thenReturn(Optional.ofNullable(PostgresCart.fromDomain(cartToDelete))); + + postgresAdapter.delete(cartToDelete.getId().toString()); + + verify(postgresCartRepository, times(1)).deleteById(cartToDelete.getId()); + } +} diff --git a/src/test/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnectorTest.java b/src/test/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnectorTest.java deleted file mode 100644 index 5d4da54..0000000 --- a/src/test/java/com/rviewer/skeletons/infrastructure/persistence/PostgresConnectorTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.rviewer.skeletons.infrastructure.persistence; - -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.jdbc.core.JdbcTemplate; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; - -@SpringBootTest -public class PostgresConnectorTest { - - @Mock - private JdbcTemplate jdbcTemplate; - @Autowired - private PostgresConnector postgresConnector; - - @Test - public void getConnectionStatus_shouldReturn2() { - when(jdbcTemplate.queryForObject("SELECT 1+1", Integer.class)).thenReturn(2); - assertEquals(2, postgresConnector.getConnectionStatus()); - } -} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 22896c4..29f6242 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,9 +1,9 @@ server.port=8080 -application.title=Java skeleton +application.title=Cartfidential application.version=1.0.0 spring.datasource.driver-class-name=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=user -spring.datasource.password=password \ No newline at end of file +spring.datasource.password=password