Merge pull request #10 from wkrzywiec/add-new-book-event

Add new book event
This commit is contained in:
Wojtek Krzywiec
2020-05-24 11:53:55 +02:00
committed by GitHub
26 changed files with 381 additions and 65 deletions

View File

@@ -131,6 +131,10 @@
</executions>
<configuration>
<reportsDirectory>${surefire.and.failsafe.report.dir}</reportsDirectory>
<includes>
<include>**/*ITCase.java</include>
<include>**/*ComponentTest.java</include>
</includes>
</configuration>
</plugin>
<plugin>

View File

@@ -0,0 +1,85 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.jdbc.core.JdbcTemplate;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MakeReservationComponentTest {
@LocalServerPort
private int port;
@Autowired
private BookRepository bookRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
private String baseURL;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
Book book = bookRepository.save(TestData.homoDeusBook());
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
book.getIdAsLong());
jdbcTemplate.update(
"INSERT INTO user (first_name, last_name, email) VALUES (?, ?, ?)",
"John",
"Doe",
"john.doe@test.com");
}
@Test
@DisplayName("Reserve available book")
public void givenBookIsAvailable_thenMakeReservation_thenBookIsReserved() {
//given
Long homoDeusBookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
TestData.homoDeusBookTitle());
Long activeUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
"john.doe@test.com");
BookReservationCommand reservationCommand =
BookReservationCommand.builder()
.bookId(homoDeusBookId )
.userId(activeUserId)
.build();
//when
given()
.contentType("application/json")
.body(reservationCommand)
.when()
.post( baseURL + "/reservations")
.prettyPeek()
.then();
Long reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
Long.class,
homoDeusBookId);
assertTrue(reservationId > 0);
}
}

View File

@@ -4,7 +4,6 @@ import io.restassured.RestAssured;
import io.restassured.response.ValidatableResponse;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -12,16 +11,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddNewBookTest {
public class AddNewBookComponentTest {
@LocalServerPort
private int port;
@@ -55,8 +52,8 @@ public class AddNewBookTest {
}
@Test
@DisplayName("Add new book to a database")
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved(){
@DisplayName("Add new book to a database & make it available")
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved() {
//given
AddNewBookCommand addNewBookCommand =
AddNewBookCommand.builder()
@@ -77,6 +74,14 @@ public class AddNewBookTest {
"SELECT id FROM book WHERE book_external_id = ?",
Long.class,
TestData.homoDeusBookGoogleId());
assertTrue(savedBookId > 0);
Long availableBookId = jdbc.queryForObject(
"SELECT id FROM available WHERE book_id = ?",
Long.class,
savedBookId);
assertTrue(availableBookId > 0);
}
}

View File

@@ -0,0 +1,23 @@
package io.wkrzywiec.hexagonal.library;
import io.wkrzywiec.hexagonal.library.borrowing.BorrowingFacade;
import io.wkrzywiec.hexagonal.library.borrowing.infrastructure.BorrowingDatabaseAdapter;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class BorrowingDomainConfig {
@Bean
public BorrowingDatabase borrowingDatabase(JdbcTemplate jdbcTemplate) {
return new BorrowingDatabaseAdapter(jdbcTemplate);
}
@Bean
public MakeBookAvailable makeBookAvailable(BorrowingDatabase database) {
return new BorrowingFacade(database);
}
}

View File

@@ -0,0 +1,29 @@
package io.wkrzywiec.hexagonal.library;
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.GoogleBooksAdapter;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.InventoryDatabaseAdapter;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.SpringInventoryEventPublisherAdapter;
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
class InventoryDomainConfig {
@Bean
SpringInventoryEventPublisherAdapter springInventoryEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
return new SpringInventoryEventPublisherAdapter(applicationEventPublisher);
}
@Bean
AddNewBook addNewBook(BookRepository repository, RestTemplate restTemplate, ApplicationEventPublisher applicationEventPublisher){
return new InventoryFacade(
new InventoryDatabaseAdapter(repository),
new GoogleBooksAdapter(restTemplate),
springInventoryEventPublisher(applicationEventPublisher));
}
}

View File

@@ -0,0 +1,14 @@
package io.wkrzywiec.hexagonal.library;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class LibraryHexagonalConfig {
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}

View File

@@ -1,4 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.application;
public class BorrowingController {
}

View File

@@ -0,0 +1,20 @@
package io.wkrzywiec.hexagonal.library.borrowing.application;
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class NewBookWasAddedEventHandler {
private final MakeBookAvailable makeBookAvailable;
@EventListener
public void handle(NewBookWasAddedEvent event){
makeBookAvailable.handle(new MakeBookAvailableCommand(event.getBookIdAsLong()));
}
}

View File

@@ -0,0 +1,25 @@
package io.wkrzywiec.hexagonal.library.borrowing.application;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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;
@RestController
@RequestMapping("/reservations")
@RequiredArgsConstructor
public class ReservationController {
private final ReserveBook reserveBook;
@PostMapping("")
public ResponseEntity<String> makeReservation(@RequestBody BookReservationCommand reservationCommand){
Long reservationId = reserveBook.handle(reservationCommand);
return new ResponseEntity<>("Reservation has been made with an id " + reservationId, HttpStatus.CREATED);
}
}

View File

@@ -15,7 +15,7 @@ import java.util.List;
import java.util.Optional;
@AllArgsConstructor
class BorrowingDatabaseAdapter implements BorrowingDatabase {
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private JdbcTemplate jdbcTemplate;

View File

@@ -2,23 +2,29 @@ package io.wkrzywiec.hexagonal.library.inventory;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
public class InventoryFacade implements AddNewBook{
private InventoryDatabase database;
private GetBookDetails getBookDetails;
private InventoryEventPublisher eventPublisher;
public InventoryFacade(InventoryDatabase database, GetBookDetails getBookDetails) {
public InventoryFacade(InventoryDatabase database, GetBookDetails getBookDetails, InventoryEventPublisher eventPublisher) {
this.database = database;
this.getBookDetails = getBookDetails;
this.eventPublisher = eventPublisher;
}
@Override
public void handle(AddNewBookCommand addNewBookCommand){
Book book = getBookDetails.handle(addNewBookCommand.getGoogleBookId());
database.save(book);
Book savedBook = database.save(book);
eventPublisher.publishNewBookWasAddedEvent(new NewBookWasAddedEvent(savedBook.getIdAsLong()));
}
}

View File

@@ -5,5 +5,5 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
interface BookRepository extends CrudRepository<Book, Long> {
public interface BookRepository extends CrudRepository<Book, Long> {
}

View File

@@ -24,7 +24,7 @@ import java.util.stream.StreamSupport;
import static java.util.Optional.ofNullable;
@RequiredArgsConstructor
class GoogleBooksAdapter implements GetBookDetails {
public class GoogleBooksAdapter implements GetBookDetails {
private final RestTemplate restTemplate;

View File

@@ -5,12 +5,12 @@ import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
class InventoryDatabaseAdapter implements InventoryDatabase {
public class InventoryDatabaseAdapter implements InventoryDatabase {
private final BookRepository repository;
@Override
public void save(Book book) {
repository.save(book);
public Book save(Book book) {
return repository.save(book);
}
}

View File

@@ -1,23 +0,0 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
class InventoryDomainConfig {
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
AddNewBook addNewBook(BookRepository repository, RestTemplate restTemplate){
return new InventoryFacade(
new InventoryDatabaseAdapter(repository),
new GoogleBooksAdapter(restTemplate));
}
}

View File

@@ -0,0 +1,17 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
@RequiredArgsConstructor
public class SpringInventoryEventPublisherAdapter implements InventoryEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
@Override
public void publishNewBookWasAddedEvent(NewBookWasAddedEvent event) {
applicationEventPublisher.publishEvent(event);
}
}

View File

@@ -67,6 +67,10 @@ public class Book {
this.imageLink = imageLink;
}
public Long getIdAsLong(){
return id;
}
private Book() {
}
}

View File

@@ -0,0 +1,22 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
import java.time.Instant;
public class NewBookWasAddedEvent {
private final Long bookId;
private final Instant timestamp;
public NewBookWasAddedEvent(Long bookId) {
this.bookId = bookId;
timestamp = Instant.now();
}
public Long getBookIdAsLong() {
return bookId;
}
public String getEventTimeStampAsString() {
return timestamp.toString();
}
}

View File

@@ -3,5 +3,5 @@ package io.wkrzywiec.hexagonal.library.inventory.ports.outgoing;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
public interface InventoryDatabase {
void save(Book book);
Book save(Book book);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.inventory.ports.outgoing;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
public interface InventoryEventPublisher {
void publishNewBookWasAddedEvent(NewBookWasAddedEvent event);
}

View File

@@ -5,22 +5,13 @@ import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import io.wkrzywiec.hexagonal.library.borrowing.BorrowingFacade;
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.inventory",
"io.wkrzywiec.hexagonal.library.borrowing"},
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.borrowing"},
importOptions = { ImportOption.DoNotIncludeTests.class })
public class HexagonalArchitectureTest {
@ArchTest
public static final ArchRule hexagonalArchInInventoryDomain = onionArchitecture()
.domainModels("io.wkrzywiec.hexagonal.library.inventory.model..")
.domainServices("io.wkrzywiec.hexagonal.library.inventory..")
.applicationServices("io.wkrzywiec.hexagonal.library.inventory.application..")
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.inventory.infrastructure..");
public class BorrowingArchitectureTest {
@ArchTest
public static final ArchRule hexagonalArchInBorrowingDomain = onionArchitecture()
@@ -29,13 +20,6 @@ public class HexagonalArchitectureTest {
.applicationServices("io.wkrzywiec.hexagonal.library.borrowing.application..")
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.borrowing.infrastructure..");
@ArchTest
public static final ArchRule noSpringDependenciesInInventoryFacade =
noClass(InventoryFacade.class)
.should()
.dependOnClassesThat()
.resideInAPackage("org.springframework..");
@ArchTest
public static final ArchRule noSpringDependenciesInBorrowingFacade =
noClass(BorrowingFacade.class)

View File

@@ -0,0 +1,30 @@
package io.wkrzywiec.hexagonal.library.architecture;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import io.wkrzywiec.hexagonal.library.inventory.InventoryFacade;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.inventory"},
importOptions = { ImportOption.DoNotIncludeTests.class })
public class InventoryArchitectureTest {
@ArchTest
public static final ArchRule hexagonalArchInInventoryDomain = onionArchitecture()
.domainModels("io.wkrzywiec.hexagonal.library.inventory.model..")
.domainServices("io.wkrzywiec.hexagonal.library.inventory..")
.applicationServices("io.wkrzywiec.hexagonal.library.inventory.application..")
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.inventory.infrastructure..");
@ArchTest
public static final ArchRule noSpringDependenciesInInventoryFacade =
noClass(InventoryFacade.class)
.should()
.dependOnClassesThat()
.resideInAPackage("org.springframework..");
}

View File

@@ -2,6 +2,7 @@ package io.wkrzywiec.hexagonal.library.inventory;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
import org.apache.commons.lang3.reflect.FieldUtils;
import java.util.concurrent.ConcurrentHashMap;
@@ -11,8 +12,15 @@ public class InMemoryInventoryDatabase implements InventoryDatabase {
ConcurrentHashMap<Long, Book> books = new ConcurrentHashMap<>();
@Override
public void save(Book book) {
public Book save(Book book) {
Long id = books.size() + 1L;
try {
FieldUtils.writeField(book, "id", id, true);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
books.put(id, book);
return book;
}
}

View File

@@ -0,0 +1,10 @@
package io.wkrzywiec.hexagonal.library.inventory;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
public class InvenotryEventPublisherFake implements InventoryEventPublisher {
@Override
public void publishNewBookWasAddedEvent(NewBookWasAddedEvent event) { }
}

View File

@@ -4,30 +4,33 @@ import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class InventoryFacadeTest {
private GetBookDetails getBookDetails;
private InMemoryInventoryDatabase database;
private InventoryEventPublisher eventPublisher;
private InventoryFacade facade;
@BeforeEach
public void init() {
database = new InMemoryInventoryDatabase();
getBookDetails = new GetBookDetailsFake();
facade = new InventoryFacade(database, getBookDetails);
eventPublisher = new InvenotryEventPublisherFake();
facade = new InventoryFacade(database, getBookDetails, eventPublisher);
}
@Test
@DisplayName("Correctly save a new book in a repository")
public void correctlySaveBook(){
//given
Book expectedBook = TestData.homoDeusBook();
AddNewBookCommand externalBookId = AddNewBookCommand
.builder()
.googleBookId(TestData.homoDeusBookGoogleId())
@@ -38,6 +41,6 @@ public class InventoryFacadeTest {
//then
Book actualBook = database.books.get(1L);
assertEquals(expectedBook, actualBook);
assertNotNull(actualBook);
}
}

View File

@@ -0,0 +1,47 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DataJpaTest
public class InventoryDatabaseAdapterITCase {
@Autowired
private BookRepository bookRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
private InventoryDatabaseAdapter database;
@BeforeEach
public void init() {
database = new InventoryDatabaseAdapter(bookRepository);
}
@Test
@DisplayName("Save new book in database")
public void givenBook_whenSaveIt_thenBookIsSaved() {
//given
Book homoDeusBook = TestData.homoDeusBook();
//when
Book savedBook = database.save(homoDeusBook);
//then
Long savedBookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE id = ?",
Long.class,
savedBook.getIdAsLong());
assertEquals(savedBook.getIdAsLong(), savedBookId);
}
}