Compare commits

..

21 Commits

Author SHA1 Message Date
wkrzywiec
727481a520 ignore model classes for test coverage 2020-05-28 11:49:51 +02:00
wkrzywiec
5f4809cfd4 get book title & email address from db - email domain 2020-05-28 10:57:26 +02:00
wkrzywiec
c8b8c26d80 implement email sending adapter 2020-05-28 10:03:24 +02:00
wkrzywiec
9e5ba16a1c clean database after each integration test 2020-05-27 18:25:13 +02:00
wkrzywiec
3f07eac529 email domain tests 2020-05-25 08:35:00 +02:00
wkrzywiec
a234301f1d email address creation tests 2020-05-25 08:11:41 +02:00
wkrzywiec
14441bfbd6 email domain logic 2020-05-25 07:11:01 +02:00
wkrzywiec
648b877b86 publish book reserved event 2020-05-25 06:28:42 +02:00
Wojtek Krzywiec
fbeac054cd Merge pull request #11 from wkrzywiec/add-new-book-event
database cleanup
2020-05-24 12:05:09 +02:00
wkrzywiec
6435c351f9 database cleanup 2020-05-24 12:01:22 +02:00
Wojtek Krzywiec
c9de6efc28 Merge pull request #10 from wkrzywiec/add-new-book-event
Add new book event
2020-05-24 11:53:55 +02:00
wkrzywiec
fa0c0fe4f4 reservation REST endpoint added 2020-05-24 11:38:05 +02:00
wkrzywiec
672c6f6557 adjust AddNewBookComponentTest to make book available 2020-05-24 10:51:02 +02:00
wkrzywiec
d9b68daa94 architecture tests adjusted 2020-05-24 10:30:13 +02:00
wkrzywiec
1c59f79cde component test naming convention 2020-05-22 12:07:18 +02:00
wkrzywiec
d2f52f849d SpringInventoryEventPublisherAdpater added 2020-05-22 11:44:19 +02:00
wkrzywiec
6f998cf030 publish event added to domain 2020-05-21 16:35:09 +02:00
wkrzywiec
f613a67517 return savedBook by database 2020-05-21 16:24:58 +02:00
wkrzywiec
8bea3936b7 add missing integration test for InventoryDatabaseAdapter 2020-05-21 15:53:37 +02:00
Wojtek Krzywiec
bebd23d22b Merge pull request #9 from wkrzywiec/reserve-book
Reserve book
2020-05-21 12:35:59 +02:00
Wojtek Krzywiec
246071b743 Merge pull request #8 from wkrzywiec/reserve-book
Reserve book
2020-05-20 22:15:27 +02:00
60 changed files with 1039 additions and 139 deletions

View File

@@ -20,7 +20,9 @@
<spring-cloud.version>Hoxton.M3</spring-cloud.version>
<surefire.and.failsafe.report.dir>target/test-report</surefire.and.failsafe.report.dir>
<code.coverage.exclusions>
**/io/wkrzywiec/hexagonal/library/domain/book/dto/**,
**/io/wkrzywiec/hexagonal/library/borrowing/model/**,
**/io/wkrzywiec/hexagonal/library/email/model/**,
**/io/wkrzywiec/hexagonal/library/inventory/model/**,
**/io/wkrzywiec/hexagonal/library/infrastructure/repository/*Entity.java
</code.coverage.exclusions>
</properties>
@@ -131,6 +133,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,78 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
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 org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@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;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
@DisplayName("Reserve available book")
@Sql({"/book-and-user.sql", "/available-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
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,16 @@ 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 org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddNewBookTest {
public class AddNewBookComponentTest {
@LocalServerPort
private int port;
@@ -55,8 +54,9 @@ 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")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved() {
//given
AddNewBookCommand addNewBookCommand =
AddNewBookCommand.builder()
@@ -77,6 +77,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

@@ -4,4 +4,4 @@ spring:
driverClassName: org.h2.Driver
username: sa
password: password
jpa.database-platform: org.hibernate.dialect.H2Dialect
jpa.database-platform: org.hibernate.dialect.H2Dialect

View File

@@ -0,0 +1,29 @@
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.infrastructure.SpringBorrowingEventPublisherAdapter;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
public class BorrowingDomainConfig {
@Bean
public BorrowingDatabase borrowingDatabase(JdbcTemplate jdbcTemplate) {
return new BorrowingDatabaseAdapter(jdbcTemplate);
}
@Bean
public BorrowingEventPublisher borrowingEventPublisher(ApplicationEventPublisher applicationEventPublisher){
return new SpringBorrowingEventPublisherAdapter(applicationEventPublisher);
}
@Bean
public MakeBookAvailable makeBookAvailable(BorrowingDatabase database, BorrowingEventPublisher borrowingEventPublisher) {
return new BorrowingFacade(database, borrowingEventPublisher);
}
}

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library;
import io.wkrzywiec.hexagonal.library.email.EmailFacade;
import io.wkrzywiec.hexagonal.library.email.infrastructure.EmailDatabaseAdapter;
import io.wkrzywiec.hexagonal.library.email.infrastructure.SendGridEmailSender;
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
public class EmailDomainConfig {
@Bean
public EmailSender emailSender() {
return new SendGridEmailSender();
}
@Bean
public EmailDatabase libraryDatabase(JdbcTemplate jdbcTemplate){
return new EmailDatabaseAdapter(jdbcTemplate);
}
@Bean
public SendReservationConfirmation sendReservationConfirmation(EmailSender emailSender, EmailDatabase database){
return new EmailFacade(emailSender, database);
}
}

View File

@@ -0,0 +1,27 @@
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.web.client.RestTemplate;
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

@@ -2,8 +2,15 @@ package io.wkrzywiec.hexagonal.library;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import({
LibraryHexagonalConfig.class,
InventoryDomainConfig.class,
BorrowingDomainConfig.class,
EmailDomainConfig.class
})
public class LibraryHexagonalApplication {
public static void main(String[] args) {

View File

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

View File

@@ -1,6 +1,7 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.ActiveUserNotFoundException;
@@ -11,13 +12,16 @@ import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
public class BorrowingFacade implements MakeBookAvailable, ReserveBook {
private BorrowingDatabase database;
private final BorrowingDatabase database;
private final BorrowingEventPublisher eventPublisher;
public BorrowingFacade(BorrowingDatabase database) {
public BorrowingFacade(BorrowingDatabase database, BorrowingEventPublisher eventPublisher) {
this.database = database;
this.eventPublisher = eventPublisher;
}
@Override
@@ -37,6 +41,7 @@ public class BorrowingFacade implements MakeBookAvailable, ReserveBook {
ReservedBook reservedBook = activeUser.reserve(availableBook);
ReservationDetails reservationDetails = database.save(reservedBook);
eventPublisher.publish(new BookReservedEvent(reservationDetails));
return reservationDetails.getReservationId().getIdAsLong();
}
}

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

@@ -6,7 +6,7 @@ import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationId;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -14,10 +14,10 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@AllArgsConstructor
class BorrowingDatabaseAdapter implements BorrowingDatabase {
@RequiredArgsConstructor
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private JdbcTemplate jdbcTemplate;
private final JdbcTemplate jdbcTemplate;
@Override
public void setBookAvailable(Long bookId) {

View File

@@ -0,0 +1,19 @@
package io.wkrzywiec.hexagonal.library.borrowing.infrastructure;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class SpringBorrowingEventPublisherAdapter implements BorrowingEventPublisher {
private final ApplicationEventPublisher eventPublisher;
@Override
public void publish(BookReservedEvent event) {
eventPublisher.publishEvent(event);
}
}

View File

@@ -0,0 +1,35 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
import java.time.Instant;
public class BookReservedEvent {
private final ReservationId reservationId;
private final Long userId;
private final ReservedBook reservedBook;
private final Instant timestamp;
public BookReservedEvent(ReservationDetails reservationDetails) {
this.reservationId = reservationDetails.getReservationId();
this.userId = reservationDetails.getReservedBook().getAssignedUserIdAsLong();
this.reservedBook = reservationDetails.getReservedBook();
timestamp = Instant.now();
}
public Long getReservationIdAsLong() {
return reservationId.getIdAsLong();
}
public Long getUserIdAsLong() {
return userId;
}
public Long getBookIdAsLong() {
return reservedBook.getIdAsLong();
}
public String getEventTimeStampAsString() {
return timestamp.toString();
}
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
public interface BorrowingEventPublisher {
public void publish(BookReservedEvent event);
}

View File

@@ -1,4 +0,0 @@
package io.wkrzywiec.hexagonal.library.email;
public class EmailContent {
}

View File

@@ -1,10 +1,18 @@
package io.wkrzywiec.hexagonal.library.email;
import io.wkrzywiec.hexagonal.library.email.model.EmailAddress;
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
class EmailCreator {
// EmailContent prepareEmailContent() {
// // email to
// // email from
// //
// }
static ReservationConfirmEmail reservationEmail(Long reservationId, String bookTitle, String emailTo){
EmailAddress from = new EmailAddress("tom@library.com");
EmailAddress to = new EmailAddress(emailTo);
String subject = String.format("Library - book reservation confirmation (id - %d)", reservationId);
String content = String.format("Dear reader,%n you have reserved a %s book which will be waiting for you in our library for next 2 days. Your reservation id is %d. %n Have a nice day, %n Library",
bookTitle, reservationId);
return new ReservationConfirmEmail(from, to, subject, content);
}
}

View File

@@ -0,0 +1,32 @@
package io.wkrzywiec.hexagonal.library.email;
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class EmailFacade implements SendReservationConfirmation {
private final EmailSender emailSender;
private final EmailDatabase database;
@Override
public void handle(SendReservationConfirmationCommand sendReservationConfirmation) {
String bookTitle = database
.getTitleByBookId(sendReservationConfirmation.getBookId())
.orElseThrow(() -> new IllegalArgumentException("Can't get book title from database. Reason: there is no book with an id: " + sendReservationConfirmation.getBookId()));
String userEmailAddress = database
.getUserEmailAddress(sendReservationConfirmation.getUserId())
.orElseThrow(() -> new IllegalArgumentException("Can't get email address from database. Reason: there is no user with an id: " + sendReservationConfirmation.getUserId()));
ReservationConfirmEmail reservationConfirmEmail = EmailCreator.reservationEmail(
sendReservationConfirmation.getReservationId(),
bookTitle,
userEmailAddress
);
emailSender.sendReservationConfirmationEmail(reservationConfirmEmail);
}
}

View File

@@ -1,6 +0,0 @@
package io.wkrzywiec.hexagonal.library.email;
public interface EmailSender {
// void sendReservationConfirmationEmail(ReservationDetails reservationDetails);
}

View File

@@ -1,33 +0,0 @@
package io.wkrzywiec.hexagonal.library.email;
//import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
public class SendGridEmailSender implements EmailSender {
// @Override
// public void sendReservationConfirmationEmail(ReservationDetails reservationDetails) {
// email address - to
// repl9oservation id
// book title`
// Email from = new Email("test@example.com");
// String subject = "Sending with SendGrid is Fun";
// Email to = new Email("test@example.com");
// Content content = new Content("text/plain", "and easy to do anywhere, even with Java");
// Mail mail = new Mail(from, subject, to, content);
//
// SendGrid sg = new SendGrid(System.getenv("SENDGRID_API_KEY"));
// Request request = new Request();
// try {
// request.setMethod(Method.POST);
// request.setEndpoint("mail/send");
// request.setBody(mail.build());
// Response response = sg.api(request);
// System.out.println(response.getStatusCode());
// System.out.println(response.getBody());
// System.out.println(response.getHeaders());
// } catch (IOException ex) {
//
// }
// }
}

View File

@@ -0,0 +1,24 @@
package io.wkrzywiec.hexagonal.library.email.application;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
import io.wkrzywiec.hexagonal.library.email.ports.incoming.SendReservationConfirmation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class BookReservedEventHandler {
private final SendReservationConfirmation sendReservationConfirmation;
@EventListener
public void handle(BookReservedEvent event) {
sendReservationConfirmation.handle(
new SendReservationConfirmationCommand(
event.getReservationIdAsLong(),
event.getUserIdAsLong(),
event.getBookIdAsLong()));
}
}

View File

@@ -0,0 +1,38 @@
package io.wkrzywiec.hexagonal.library.email.infrastructure;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Optional;
@RequiredArgsConstructor
public class EmailDatabaseAdapter implements EmailDatabase {
private final JdbcTemplate jdbcTemplate;
@Override
public Optional<String> getTitleByBookId(Long bookId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT title FROM book WHERE id = ?",
String.class,
bookId));
} catch (DataAccessException ex){
return Optional.empty();
}
}
@Override
public Optional<String> getUserEmailAddress(Long userId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT email FROM user WHERE id = ?",
String.class,
userId));
} catch (DataAccessException ex){
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,38 @@
package io.wkrzywiec.hexagonal.library.email.infrastructure;
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
import java.io.IOException;
public class SendGridEmailSender implements EmailSender {
@Override
public void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail) {
Email from = new Email(reservationConfirmEmail.getFromEmailAddressAsString());
Email to = new Email(reservationConfirmEmail.getToEmailAddressAsString());
Content content = new Content("text/plain", reservationConfirmEmail.getContentAsString());
Mail mail = new Mail(
from,
reservationConfirmEmail.getSubjectAsString(),
to,
content);
SendGrid sg = new SendGrid(System.getenv("SENDGRID_API_KEY"));
Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
sg.api(request);
} catch (IOException ex) {
System.out.print(ex);
}
}
}

View File

@@ -0,0 +1,24 @@
package io.wkrzywiec.hexagonal.library.email.model;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EmailAddress {
private final String value;
public EmailAddress(String value) {
Pattern pattern = Pattern.compile("^(.+)@(.+)$");
Matcher matcher = pattern.matcher(value);
if(matcher.matches()){
this.value = value;
} else {
throw new IllegalArgumentException("Provided value is not an email address");
}
}
public String getAsString() {
return value;
}
}

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library.email.model;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ReservationConfirmEmail {
private final EmailAddress from;
private final EmailAddress to;
private final String subject;
private final String content;
public String getFromEmailAddressAsString(){
return this.from.getAsString();
}
public String getToEmailAddressAsString(){
return this.to.getAsString();
}
public String getSubjectAsString(){
return this.subject;
}
public String getContentAsString(){
return this.content;
}
}

View File

@@ -0,0 +1,17 @@
package io.wkrzywiec.hexagonal.library.email.model;
import lombok.Getter;
@Getter
public class SendReservationConfirmationCommand {
private final Long reservationId;
private final Long userId;
private final Long bookId;
public SendReservationConfirmationCommand(Long reservationId, Long userId, Long bookId) {
this.reservationId = reservationId;
this.userId = userId;
this.bookId = bookId;
}
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.email.ports.incoming;
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
public interface SendReservationConfirmation {
void handle(SendReservationConfirmationCommand reservationConfirmationCommand);
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.email.ports.outgoing;
import java.util.Optional;
public interface EmailDatabase {
Optional<String> getTitleByBookId(Long bookId);
Optional<String> getUserEmailAddress(Long userId);
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.email.ports.outgoing;
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
public interface EmailSender {
void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail);
}

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,29 @@
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.email.EmailFacade;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClass;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = {"io.wkrzywiec.hexagonal.library.email"},
importOptions = { ImportOption.DoNotIncludeTests.class })
public class EmailArchitectureTest {
@ArchTest
public static final ArchRule hexagonalArchInEmailDomain = onionArchitecture()
.domainModels("io.wkrzywiec.hexagonal.library.email.model..")
.domainServices("io.wkrzywiec.hexagonal.library.email..")
.applicationServices("io.wkrzywiec.hexagonal.library.email.application..")
.adapter("infrastructure", "io.wkrzywiec.hexagonal.library.email.infrastructure..");
@ArchTest
public static final ArchRule noSpringDependenciesInEmailFacade =
noClass(EmailFacade.class)
.should()
.dependOnClassesThat()
.resideInAPackage("org.springframework..");
}

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

@@ -0,0 +1,12 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
public class BorrowingEventPublisherFake implements BorrowingEventPublisher {
@Override
public void publish(BookReservedEvent event) {
}
}

View File

@@ -7,6 +7,7 @@ import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.ActiveUserNotFoundException;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.AvailableBookNotFoundExeption;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.TooManyBooksAssignedToUserException;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingEventPublisher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -19,11 +20,13 @@ public class BorrowingFacadeTest {
private BorrowingFacade facade;
private InMemoryBorrowingDatabase database;
private BorrowingEventPublisher eventPublisher;
@BeforeEach
public void init(){
database = new InMemoryBorrowingDatabase();
facade = new BorrowingFacade(database);
eventPublisher = new BorrowingEventPublisherFake();
facade = new BorrowingFacade(database, eventPublisher);
}
@Test

View File

@@ -9,15 +9,17 @@ 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.jdbc.JdbcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.jdbc.Sql;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@JdbcTest
@SpringBootTest
public class BorrowingDatabaseAdapterITCase {
@Autowired
@@ -28,21 +30,12 @@ public class BorrowingDatabaseAdapterITCase {
@BeforeEach
public void init(){
database = new BorrowingDatabaseAdapter(jdbcTemplate);
jdbcTemplate.update(
"INSERT INTO book (book_external_id, title) VALUES (?, ?)",
TestData.homoDeusBookGoogleId(),
TestData.homoDeusBookTitle());
jdbcTemplate.update(
"INSERT INTO user (first_name, last_name, email) VALUES (?, ?, ?)",
"John",
"Doe",
"john.doe@test.com");
}
@Test
@DisplayName("Save book as available")
@Sql("/book-and-user.sql")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldSaveAvailableBook(){
//given
Long bookId = jdbcTemplate.queryForObject(
@@ -63,6 +56,8 @@ public class BorrowingDatabaseAdapterITCase {
@Test
@DisplayName("Get available book by id")
@Sql({"/book-and-user.sql", "/available-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldGetAvailableBook(){
//given
Long bookId = jdbcTemplate.queryForObject(
@@ -70,10 +65,6 @@ public class BorrowingDatabaseAdapterITCase {
Long.class,
TestData.homoDeusBookTitle());
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
bookId);
//when
Optional<AvailableBook> availableBookOptional = database.getAvailableBook(bookId);
@@ -84,6 +75,8 @@ public class BorrowingDatabaseAdapterITCase {
@Test
@DisplayName("Get active user by id")
@Sql("/book-and-user.sql")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldGetActiveUser() {
//given
Long activeUserId = jdbcTemplate.queryForObject(
@@ -101,6 +94,8 @@ public class BorrowingDatabaseAdapterITCase {
@Test
@DisplayName("Save reserved book")
@Sql({"/book-and-user.sql", "/available-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldSaveReservedBook(){
//given
Long bookId = jdbcTemplate.queryForObject(
@@ -119,7 +114,6 @@ public class BorrowingDatabaseAdapterITCase {
ReservationDetails reservationDetails = database.save(reservedBook);
//then
assertEquals(bookId, reservationDetails.getReservedBook().getIdAsLong());
assertEquals(activeUserId, reservationDetails.getReservedBook().getAssignedUserIdAsLong());
assertTrue(reservationDetails.getReservationId().getIdAsLong() > 0);

View File

@@ -0,0 +1,39 @@
package io.wkrzywiec.hexagonal.library.email;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.email.model.SendReservationConfirmationCommand;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
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.assertDoesNotThrow;
public class EmailFacadeTest {
private EmailFacade facade;
private EmailSender emailSender;
private InMemoryEmailDatabase database;
@BeforeEach
public void init() {
database = new InMemoryEmailDatabase();
emailSender = new EmailSenderFake();
facade = new EmailFacade(emailSender, database);
database.bookTitles.put(1L, TestData.homoDeusBookTitle());
database.emailAddresses.put(1L, "john.doe@test.com");
}
@Test
@DisplayName("Prepare & send reservation confirmation email")
public void shouldPrepareAndSendReservationConfirmation(){
//given
SendReservationConfirmationCommand sendReservationConfirmationCommand
= new SendReservationConfirmationCommand(1L, 1L, 1L);
//when & then
assertDoesNotThrow(() -> facade.handle(sendReservationConfirmationCommand));
}
}

View File

@@ -0,0 +1,12 @@
package io.wkrzywiec.hexagonal.library.email;
import io.wkrzywiec.hexagonal.library.email.model.ReservationConfirmEmail;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailSender;
public class EmailSenderFake implements EmailSender {
@Override
public void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail) {
}
}

View File

@@ -0,0 +1,22 @@
package io.wkrzywiec.hexagonal.library.email;
import io.wkrzywiec.hexagonal.library.email.ports.outgoing.EmailDatabase;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public class InMemoryEmailDatabase implements EmailDatabase {
ConcurrentHashMap<Long, String> bookTitles = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, String> emailAddresses = new ConcurrentHashMap<>();
@Override
public Optional<String> getTitleByBookId(Long bookId) {
return Optional.of(bookTitles.get(bookId));
}
@Override
public Optional<String> getUserEmailAddress(Long userId) {
return Optional.of(emailAddresses.get(userId));
}
}

View File

@@ -0,0 +1,101 @@
package io.wkrzywiec.hexagonal.library.email.infrastructure;
import io.wkrzywiec.hexagonal.library.TestData;
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.jdbc.core.JdbcTemplate;
import org.springframework.test.context.jdbc.Sql;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@SpringBootTest
public class EmailDatabaseAdapterITCase {
@Autowired
private JdbcTemplate jdbcTemplate;
private EmailDatabaseAdapter emailDatabase;
@BeforeEach
public void init(){
emailDatabase = new EmailDatabaseAdapter(jdbcTemplate);
}
@Test
@DisplayName("Get book title from db by its id")
@Sql({"/book-and-user.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookId_whenGetBookTitle_thenGetBookTitle() {
//given
Long bookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
TestData.homoDeusBookTitle());
//when
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId);
//then
assertEquals(Optional.of(TestData.homoDeusBookTitle()), bookTitle);
}
@Test
@DisplayName("Get empty result when book is not in db")
@Sql({"/book-and-user.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenWrongBookId_whenGetBookTitle_thenGetEmptyResult() {
//given
Long bookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
TestData.homoDeusBookTitle());
//when
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId + 1124);
//then
assertEquals(Optional.empty(), bookTitle);
}
@Test
@DisplayName("Get email from db by user id")
@Sql({"/book-and-user.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenUserId_whenGetEmail_thenGetEmailAddress() {
//given
Long userId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
"john.doe@test.com");
//when
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId);
//then
assertEquals(Optional.of("john.doe@test.com"), emailAddress);
}
@Test
@DisplayName("Get empty result when book is not in db")
@Sql({"/book-and-user.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenWrongUserId_whenGetEmail_thenGetEmptyResult() {
//given
Long userId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
"john.doe@test.com");
//when
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId + 1124);
//then
assertEquals(Optional.empty(), emailAddress);
}
}

View File

@@ -0,0 +1,46 @@
package io.wkrzywiec.hexagonal.library.email.model;
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.assertThrows;
public class EmailAddressTest {
@Test
@DisplayName("Create correct EmailAddress")
public void givenCorrectEmailString_whenCreateEmailAddress_thenIsCreated(){
//given
String emailString = "john.doe@test.com";
//when
EmailAddress emailAddress = new EmailAddress(emailString);
//then
assertEquals(emailString, emailAddress.getAsString());
}
@Test
@DisplayName("Throw IllegalArgument exception for incorrect email")
public void givenInCorrectEmailString_whenCreateEmailAddress_thenThrowException(){
//given
String notAnEmailString = "not an email";
String emailWithoutAt = "tom[at]test.com";
String emailWithoutDomain = "tom@";
//when & then
assertThrows(
IllegalArgumentException.class,
() -> new EmailAddress(notAnEmailString));
assertThrows(
IllegalArgumentException.class,
() -> new EmailAddress(emailWithoutAt));
assertThrows(
IllegalArgumentException.class,
() -> new EmailAddress(emailWithoutDomain));
}
}

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,50 @@
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.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.jdbc.Sql;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
@SpringBootTest
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")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
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);
}
}

View File

@@ -0,0 +1,7 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password: password
jpa.database-platform: org.hibernate.dialect.H2Dialect

View File

@@ -0,0 +1,3 @@
INSERT INTO public.available (book_id) VALUES
SELECT id FROM public.book WHERE book.title = 'Homo Deus'
;

View File

@@ -0,0 +1,17 @@
INSERT INTO public.author (name) VALUES
('Yuval Noah Harari')
;
INSERT INTO public.book (book_external_id,isbn_10,isbn_13,title,publisher,published_date,description,page_count,image_link) VALUES
('dWYyCwAAQBAJ','1473545374','9781473545373','Homo Deus','Random House','2016-09-08','<p><b>**THE MILLION COPY BESTSELLER**</b><br> <b></b><br><b> <i>Sapiens </i>showed us where we came from. In uncertain times, <i>Homo Deus</i> shows us where were going.</b></p><p> Yuval Noah Harari envisions a near future in which we face a new set of challenges. <i>Homo Deus</i> explores the projects, dreams and nightmares that will shape the twenty-first century and beyond from overcoming death to creating artificial life.</p><p> It asks the fundamental questions: how can we protect this fragile world from our own destructive power? And what does our future hold?<br> <b></b><br><b> ''<i>Homo Deus</i> will shock you. It will entertain you. It will make you think in ways you had not thought before Daniel Kahneman, bestselling author of <i>Thinking, Fast and Slow</i></b></p>',528,'http://books.google.com/books/content?id=dWYyCwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE73PkLs4TNB-W2uhDvXJkIB4-9G9AJ_L1iYTYLEXa3zi2kahdsN9-_0tL7WRWgujNpjMA5ZuJO7_ykFUlCWAyLzcQVcGkqUS-NOkUkEcJ_ZRrgq48URpcfBrJWQCwSWtHo5pEGkp&source=gbs_api')
;
INSERT INTO public.book_author (book_id, author_id)
SELECT b.id, a.id
FROM public.book b, public.author a
WHERE b.title = 'Homo deus' AND a.name = 'Yuval Noah Harari'
;
INSERT INTO public.user (first_name, last_name, email) VALUES
('John','Doe','john.doe@test.com')
;

View File

@@ -0,0 +1,6 @@
DELETE FROM public.reserved;
DELETE FROM public.available;
DELETE FROM public.book_author;
DELETE FROM public.user;
DELETE FROM public.book;
DELETE FROM public.author;