Compare commits

...

20 Commits

Author SHA1 Message Date
Wojtek Krzywiec
6dc153309b add spock reports 2020-10-23 07:58:22 +02:00
Wojtek Krzywiec
3218fb056e small changes 2020-10-20 07:05:30 +02:00
Wojtek Krzywiec
8306b5158a move stubed services to a tests class 2020-10-14 06:42:34 +02:00
Wojtek Krzywiec
967a349b4c bring back JUnit4 deps 2020-10-13 06:34:26 +02:00
Wojtek Krzywiec
42b08c8995 BorrowingFacadeSpec added 2020-09-20 12:32:04 +02:00
Wojtek Krzywiec
58a9a0f95b add spock dependencies to pom.xml 2020-09-19 12:41:34 +02:00
Wojtek Krzywiec
9b0c37ffd7 Merge branch 'polishing' 2020-06-14 15:09:45 +02:00
Wojtek Krzywiec
53939113f2 fixes 2020-06-14 15:08:53 +02:00
Wojtek Krzywiec
a9fd37ec5a Merge pull request #18 from wkrzywiec/polishing
Polishing
2020-06-08 21:33:59 +02:00
wkrzywiec
18d1038a18 rename user table to library_user plus small type corrections 2020-06-08 21:31:32 +02:00
Wojtek Krzywiec
f071d557ff more restful controller 2020-06-08 07:07:26 +02:00
Wojtek Krzywiec
b7db201e7b database helper for tests 2020-06-08 06:41:30 +02:00
Wojtek Krzywiec
36316de3f7 database init for testing 2020-06-08 06:15:10 +02:00
Wojtek Krzywiec
428d281e38 actuator added & spring profiles introduced 2020-06-07 23:13:32 +02:00
Wojtek Krzywiec
62960ef444 Merge pull request #17 from wkrzywiec/give-back-book
Give back book
2020-06-07 11:51:13 +02:00
Wojtek Krzywiec
19ce80db76 add RowMapper for ReservedBook and BorrowedBook 2020-06-07 11:47:57 +02:00
Wojtek Krzywiec
8a1e7ae20e give back application layer 2020-06-07 11:03:28 +02:00
Wojtek Krzywiec
d2b7d08072 add give back business logic 2020-06-07 08:56:17 +02:00
Wojtek Krzywiec
2e5858d8a9 add give back business logic 2020-06-05 22:41:12 +02:00
Wojtek Krzywiec
5e221dd427 Merge pull request #16 from wkrzywiec/borrow
Borrow book
2020-06-04 08:18:13 +02:00
50 changed files with 813 additions and 455 deletions

View File

@@ -8,4 +8,4 @@ RUN mvn -B -f pom.xml clean package -DskipTests
FROM openjdk:11-jdk-slim
COPY --from=build /workspace/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
ENTRYPOINT ["java","-jar","app.jar", "--spring.profiles.active=prod"]

View File

@@ -19,14 +19,23 @@ In the terminal run the following command:
$ docker-compose up
```
#### Using Maven
#### Using Maven (with H2 or local Postgres database)
First make sure that you adjust the configuration file - `src/main/resources/application.yml` with connection details to your database.
First compile an application:
Then, in the terminal run the following command:
```console
$ mvn clean package
$ mvn spring-boot:run
```
Then, you have two options either run it with H2 database or with local Postgres database. For first approach just run:
```console
$ mvn spring-boot:run
```
For a second option, check in the configuration file - `src/main/resources/application.yml` for profile *local-postgres* if connection details are correct and if so, run the command:
```console
$ mvn spring-boot:run -P local-postgres
```
#### Inside IntelliJ (with H2 or Postgres database)

82
pom.xml
View File

@@ -33,6 +33,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@@ -87,27 +91,48 @@
<version>1.2.32</version>
</dependency>
<!-- TEST dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.athaydes</groupId>
<artifactId>spock-reports</artifactId>
<version>1.8.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
</dependency>
</dependencies>
<build>
@@ -140,8 +165,25 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reportsDirectory>${surefire.and.failsafe.report.dir}</reportsDirectory>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.10.0</version>
<executions>
<execution>
<goals>
<goal>addTestSources</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@@ -186,6 +228,33 @@
</build>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<active-profiles>default</active-profiles>
</properties>
</profile>
<profile>
<id>prod</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<active-profiles>prod</active-profiles>
</properties>
</profile>
<profile>
<id>local-postgres</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<active-profiles>local-postgres</active-profiles>
</properties>
</profile>
<profile>
<id>component-test</id>
<build>
@@ -232,6 +301,7 @@
<sonar.sources>.</sonar.sources>
<sonar.inclusions>src/main/java/**,src/main/resources/**</sonar.inclusions>
<sonar.exclusions>${code.coverage.exclusions}</sonar.exclusions>
<sonat.tests>src/test/groovy,src/test/java</sonat.tests>
<sonar.projectKey>wkrzywiec_library-hexagonal</sonar.projectKey>
<sonar.organization>wkrzywiec</sonar.organization>
<sonar.coverage.jacoco.xmlReportPaths>target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library.domain;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.DatabaseHelper;
import org.junit.jupiter.api.BeforeEach;
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;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseComponentTest {
@LocalServerPort
private int port;
protected String baseURL;
@Autowired
protected JdbcTemplate jdbcTemplate;
protected DatabaseHelper databaseHelper;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
databaseHelper = new DatabaseHelper(jdbcTemplate);
}
}

View File

@@ -1,83 +1,44 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.UserTestData;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
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 BorrowBookComponentTest {
@LocalServerPort
private int port;
private String baseURL;
@Autowired
private JdbcTemplate jdbcTemplate;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
public class BorrowBookComponentTest extends BaseComponentTest {
@Test
@Disabled
@DisplayName("Borrow reserved book")
@Sql({"/book-and-user.sql", "/available-book.sql"})
@Sql({"/book-and-user.sql", "/reserved-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookIsReserved_thenBorrowIt_thenBookIsBorrowed() {
//given
Long homoDeusBookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
BookTestData.homoDeusBookTitle());
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
Long activeUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
UserTestData.johnDoeEmail());
jdbcTemplate.update(
"INSERT INTO public.reserved (book_id, user_id) VALUES (?, ?)",
homoDeusBookId,
activeUserId);
BorrowBookCommand borrowBookCommand =
BorrowBookCommand.builder()
.bookId(homoDeusBookId )
.userId(activeUserId)
.build();
ChangeBookStatusRequest borrowRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.BORROWED)
.build();
//when
given()
.contentType("application/json")
.body(borrowBookCommand)
.body(borrowRequest)
.when()
.post( baseURL + "/borrow")
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM borrowed WHERE book_id = ?",
Long.class,
homoDeusBookId);
assertTrue(reservationId > 0);
Long borrowId = databaseHelper.getPrimaryKeyOfBorrowedByBookId(homoDeusBookId);
assertTrue(borrowId > 0);
}
}

View File

@@ -0,0 +1,45 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class GiveBackBookComponentTest extends BaseComponentTest {
@Test
@DisplayName("Give back borrowed book")
@Sql({"/book-and-user.sql", "/borrowed-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookIsBorrowed_thenGiveBackIt_thenItIsAvailable() {
//given
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
ChangeBookStatusRequest giveBackRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.AVAILABLE)
.build();
//when
given()
.contentType("application/json")
.body(giveBackRequest)
.when()
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long bookId = databaseHelper.getPrimaryKeyOfAvailableByBookBy(homoDeusBookId);
assertEquals(homoDeusBookId, bookId);
}
}

View File

@@ -1,79 +1,49 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.UserTestData;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.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;
public class MakeReservationComponentTest extends BaseComponentTest {
@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,
BookTestData.homoDeusBookTitle());
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
Long activeUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
UserTestData.johnDoeEmail());
BookReservationCommand reservationCommand =
BookReservationCommand.builder()
.bookId(homoDeusBookId )
.userId(activeUserId)
.build();
ChangeBookStatusRequest reservationRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.RESERVED)
.build();
//when
given()
.contentType("application/json")
.body(reservationCommand)
.body(reservationRequest)
.when()
.post( baseURL + "/reservations")
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
Long.class,
homoDeusBookId);
Long reservationId = databaseHelper.getPrimaryKeyOfReservationByBookId(homoDeusBookId);
assertTrue(reservationId > 0);
}
}

View File

@@ -1,17 +1,12 @@
package io.wkrzywiec.hexagonal.library.domain.inventory;
import io.restassured.RestAssured;
import io.restassured.response.ValidatableResponse;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.AddNewBookCommand;
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.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
@@ -19,22 +14,7 @@ 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 AddNewBookComponentTest {
@LocalServerPort
private int port;
@Autowired
private JdbcTemplate jdbc;
private String baseURL;
@BeforeEach
public void init(){
this.baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
public class AddNewBookComponentTest extends BaseComponentTest {
@Test
@DisplayName("Search for a new book in Google Books")
@@ -73,17 +53,10 @@ public class AddNewBookComponentTest {
.then();
//then
Long savedBookId = jdbc.queryForObject(
"SELECT id FROM book WHERE book_external_id = ?",
Long.class,
BookTestData.homoDeusBookGoogleId());
Long savedBookId = databaseHelper.getHomoDeusBookId();
assertTrue(savedBookId > 0);
Long availableBookId = jdbc.queryForObject(
"SELECT id FROM available WHERE book_id = ?",
Long.class,
savedBookId);
Long availableBookId = databaseHelper.getPrimaryKeyOfAvailableByBookBy(savedBookId);
assertTrue(availableBookId > 0);
}

View File

@@ -1,36 +1,16 @@
package io.wkrzywiec.hexagonal.library.domain.user;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.AddUserCommand;
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 AddNewUserComponentTest {
@LocalServerPort
private int port;
private String baseURL;
@Autowired
private JdbcTemplate jdbcTemplate;
@BeforeEach
public void init(){
baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
public class AddNewUserComponentTest extends BaseComponentTest {
@Test
@DisplayName("Create new user")
@@ -53,11 +33,7 @@ public class AddNewUserComponentTest {
.then();
//then
Long savedUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
"john.doe@test.com");
Long savedUserId = databaseHelper.getJohnDoeUserId();
assertTrue(savedUserId > 0);
}

View File

@@ -1,27 +0,0 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
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("/borrow")
@RequiredArgsConstructor
public class BorrowBookController {
@Qualifier("BorrowBook")
private final BorrowBook borrowBook;
@PostMapping("")
public ResponseEntity<String> borrowBook(@RequestBody BorrowBookCommand borrowBookCommand){
borrowBook.handle(borrowBookCommand);
return new ResponseEntity<>("Book with an id " + borrowBookCommand.getBookId() + " was borrowed", HttpStatus.CREATED);
}
}

View File

@@ -0,0 +1,50 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.GiveBackBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
@RequiredArgsConstructor
public class BorrowingDomainController {
@Qualifier("GiveBackBook")
private final GiveBackBook giveBackBook;
@Qualifier("ReserveBook")
private final ReserveBook reserveBook;
@Qualifier("BorrowBook")
private final BorrowBook borrowBook;
@PatchMapping("/{id}/status")
public ResponseEntity<String> borrowBook(@PathVariable("id") Long bookId, @RequestBody ChangeBookStatusRequest request){
switch (request.getStatus()){
case AVAILABLE:
giveBackBook.handle(new GiveBackBookCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Book with an id " + bookId + " was returned", HttpStatus.OK);
case RESERVED:
Long reservationId = reserveBook.handle(new BookReservationCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Reservation has been made with an id " + reservationId, HttpStatus.OK);
case BORROWED:
borrowBook.handle(new BorrowBookCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Book with an id " + bookId + " was borrowed", HttpStatus.OK);
default:
return new ResponseEntity<>("Book can't have status: " + request.getStatus(), HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -11,7 +11,7 @@ public class OverdueReservationScheduler {
@Qualifier("CancelOverdueReservations")
private final CancelOverdueReservations overdueReservations;
@Scheduled(fixedRate = 10 * 1000)
@Scheduled(fixedRate = 60 * 1000)
public void checkOverdueReservations(){
overdueReservations.cancelOverdueReservations();
}

View File

@@ -1,27 +0,0 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
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 {
@Qualifier("ReserveBook")
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

@@ -0,0 +1,5 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application.model;
public enum BookStatus {
AVAILABLE, RESERVED, BORROWED
}

View File

@@ -0,0 +1,12 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application.model;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ChangeBookStatusRequest {
private BookStatus status;
private Long userId;
}

View File

@@ -6,27 +6,26 @@ import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservatio
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.DueDate;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.ActiveUserNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.AvailableBookNotFoundExeption;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.BorrowedBookNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.ReservedBookNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.CancelOverdueReservations;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.GiveBackBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingEventPublisher;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOverdueReservations, BorrowBook {
public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOverdueReservations, BorrowBook, GiveBackBook {
private final BorrowingDatabase database;
private final BorrowingEventPublisher eventPublisher;
@@ -38,7 +37,7 @@ public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOv
@Override
public void handle(MakeBookAvailableCommand bookAvailableCommand) {
database.setBookAvailable(bookAvailableCommand.getBookId());
database.save(new AvailableBook(bookAvailableCommand.getBookId()));
}
@Override
@@ -59,10 +58,9 @@ public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOv
@Override
public void cancelOverdueReservations() {
DueDate dueDate = new DueDate(Instant.now().plus(3L, ChronoUnit.DAYS));
List<OverdueReservation> overdueReservationList = database.findReservationsAfter(dueDate);
List<OverdueReservation> overdueReservationList = database.findReservationsForMoreThan(3L);
overdueReservationList.forEach(
overdue -> database.setBookAvailable(overdue.getBookIdentificationAsLong()));
overdueBook -> database.save(new AvailableBook(overdueBook.getBookIdentificationAsLong())));
}
@Override
@@ -77,4 +75,17 @@ public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOv
BorrowedBook borrowedBook = activeUser.borrow(reservedBook);
database.save(borrowedBook);
}
@Override
public void handle(GiveBackBookCommand command) {
BorrowedBook borrowedBook =
database.getBorrowedBook(command.getBookId())
.orElseThrow(() -> new BorrowedBookNotFoundException(command.getBookId()));
ActiveUser activeUser =
database.getActiveUser(command.getUserId())
.orElseThrow(() -> new ActiveUserNotFoundException(command.getUserId()));
AvailableBook availableBook = activeUser.giveBack(borrowedBook);
database.save(availableBook);
}
}

View File

@@ -2,10 +2,14 @@ package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.TooManyBooksAssignedToUserException;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.LinkedHashMap;
import java.util.List;
@EqualsAndHashCode
@ToString
public class ActiveUser {
private final Long id;
@@ -38,6 +42,15 @@ public class ActiveUser {
}
}
public AvailableBook giveBack(BorrowedBook borrowedBook) {
boolean isBookRemovedFromUserAccount = borrowedBooks.removeIf(book -> book.equals(borrowedBook));
if (isBookRemovedFromUserAccount){
return new AvailableBook(borrowedBook.getIdAsLong());
} else {
throw new IllegalArgumentException("User with an id: " + id + " didn't borrow book with an id: " + borrowedBook.getIdAsLong());
}
}
public Long getIdAsLong(){
return id;
}

View File

@@ -1,8 +1,10 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@EqualsAndHashCode
@ToString
public class AvailableBook implements Book {
private final Long id;

View File

@@ -3,8 +3,10 @@ package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class BookReservationCommand {

View File

@@ -7,9 +7,10 @@ import java.time.Instant;
@EqualsAndHashCode
public class BorrowedBook implements Book {
private final Long bookId;
private final Long userId;
private final Instant borrowedDate;
private Long bookId;
private Long userId;
@EqualsAndHashCode.Exclude
private Instant borrowedDate;
public BorrowedBook(Long bookId, Long userId) {
this.bookId = bookId;
@@ -17,6 +18,12 @@ public class BorrowedBook implements Book {
this.borrowedDate = Instant.now();
}
public BorrowedBook(Long bookId, Long userId, Instant borrowedDate) {
this.bookId = bookId;
this.userId = userId;
this.borrowedDate = borrowedDate;
}
@Override
public Long getIdAsLong() {
return bookId;

View File

@@ -0,0 +1,13 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
@Getter
@Builder
public class GiveBackBookCommand {
private Long bookId;
private Long userId;
}

View File

@@ -1,6 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.Instant;
@@ -17,6 +18,12 @@ public class ReservedBook implements Book {
this.reservedDate = Instant.now();
}
public ReservedBook(Long bookId, Long userId, Instant reservedDate) {
this.bookId = bookId;
this.userId = userId;
this.reservedDate = reservedDate;
}
@Override
public Long getIdAsLong() {
return bookId;

View File

@@ -0,0 +1,10 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class BorrowedBookNotFoundException extends RuntimeException {
public BorrowedBookNotFoundException(Long bookId) {
super("There is no borrowed book with an ID: " + bookId,
null,
false,
false);
}
}

View File

@@ -1,4 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
public interface GiveBackBook {
void handle(GiveBackBookCommand command);
}

View File

@@ -12,11 +12,12 @@ import java.util.List;
import java.util.Optional;
public interface BorrowingDatabase {
void setBookAvailable(Long bookId);
Optional<AvailableBook> getAvailableBook(Long bookId);
Optional<ActiveUser> getActiveUser(Long userId);
void save(AvailableBook availableBook);
ReservationDetails save(ReservedBook reservedBook);
void save(BorrowedBook borrowedBook);
List<OverdueReservation> findReservationsAfter(DueDate dueDate);
Optional<AvailableBook> getAvailableBook(Long bookId);
Optional<ActiveUser> getActiveUser(Long userId);
List<OverdueReservation> findReservationsForMoreThan(Long days);
Optional<ReservedBook> getReservedBook(Long bookId);
Optional<BorrowedBook> getBorrowedBook(Long bookId);
}

View File

@@ -10,6 +10,8 @@ import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDet
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationId;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper.BorrowedBookRowMapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper.ReservedBookRowMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
@@ -27,18 +29,18 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private final JdbcTemplate jdbcTemplate;
@Override
public void setBookAvailable(Long bookId) {
public void save(AvailableBook availableBook) {
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
bookId);
availableBook.getIdAsLong());
jdbcTemplate.update(
"DELETE FROM reserved WHERE book_id = ?",
bookId);
availableBook.getIdAsLong());
jdbcTemplate.update(
"DELETE FROM borrowed WHERE book_id = ?",
bookId);
availableBook.getIdAsLong());
}
@Override
@@ -58,7 +60,7 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
public Optional<ActiveUser> getActiveUser(Long userId) {
try {
jdbcTemplate.queryForObject(
"SELECT id FROM public.user as u WHERE u.id = ?",
"SELECT id FROM public.library_user as u WHERE u.id = ?",
Long.class,
userId);
} catch (DataAccessException exception) {
@@ -76,7 +78,7 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
"INSERT INTO reserved (book_id, user_id, reserved_date) VALUES (?, ?, ?)",
reservedBook.getIdAsLong(),
reservedBook.getAssignedUserIdAsLong(),
reservedBook.getReservedDateAsInstant());
Timestamp.from(reservedBook.getReservedDateAsInstant()));
jdbcTemplate.update(
"DELETE FROM available WHERE book_id = ?",
@@ -95,7 +97,7 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
"INSERT INTO borrowed (book_id, user_id, borrowed_date) VALUES (?, ?, ?)",
borrowedBook.getIdAsLong(),
borrowedBook.getAssignedUserIdAsLong(),
borrowedBook.getBorrowedDateAsInstant());
Timestamp.from(borrowedBook.getBorrowedDateAsInstant()));
jdbcTemplate.update(
"DELETE FROM reserved WHERE book_id = ?",
@@ -108,11 +110,11 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
}
@Override
public List<OverdueReservation> findReservationsAfter(DueDate dueDate) {
public List<OverdueReservation> findReservationsForMoreThan(Long days) {
List<OverdueReservationEntity> entities = jdbcTemplate.query(
"SELECT id AS reservationId, book_id AS bookIdentification FROM reserved WHERE reserved_date > ?",
"SELECT id AS reservationId, book_id AS bookIdentification FROM reserved WHERE DATEADD(day, ?, reserved_date) > NOW()",
new BeanPropertyRowMapper<OverdueReservationEntity>(OverdueReservationEntity.class),
Timestamp.from(dueDate.asInstant()));
days);
return entities.stream()
.map(entity -> new OverdueReservation(entity.getReservationId(), entity.getBookIdentification()))
.collect(Collectors.toList());
@@ -123,8 +125,21 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id AS bookId, user_id AS userId, reserved_date AS reservedDate FROM reserved WHERE reserved.book_id = ?",
ReservedBook.class,
"SELECT book_id, user_id, reserved_date FROM reserved WHERE book_id = ?",
new ReservedBookRowMapper(),
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
}
}
@Override
public Optional<BorrowedBook> getBorrowedBook(Long bookId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id, user_id, borrowed_date FROM borrowed WHERE book_id = ?",
new BorrowedBookRowMapper(),
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
@@ -133,9 +148,9 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private List<ReservedBook> getReservedBooksByUser(Long userId) {
try {
return jdbcTemplate.queryForList(
"SELECT book_id FROM reserved WHERE reserved.user_id = ?",
ReservedBook.class,
return jdbcTemplate.query(
"SELECT book_id, user_id, reserved_date FROM reserved WHERE user_id = ?",
new ReservedBookRowMapper(),
userId
);
} catch (DataAccessException exception){
@@ -144,6 +159,14 @@ public class BorrowingDatabaseAdapter implements BorrowingDatabase {
}
private List<BorrowedBook> getBorrowedBooksByUser(Long userId) {
return new ArrayList<>();
try {
return jdbcTemplate.query(
"SELECT book_id, user_id, borrowed_date FROM borrowed WHERE user_id = ?",
new BorrowedBookRowMapper(),
userId
);
} catch (DataAccessException exception){
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,20 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class BorrowedBookRowMapper implements RowMapper<BorrowedBook> {
@Override
public BorrowedBook mapRow(ResultSet rs, int rowNum) throws SQLException {
return new BorrowedBook(
rs.getLong("book_id"),
rs.getLong("user_id"),
rs.getTimestamp("borrowed_date").toInstant()
);
}
}

View File

@@ -0,0 +1,19 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ReservedBookRowMapper implements RowMapper<ReservedBook> {
@Override
public ReservedBook mapRow(ResultSet rs, int rowNum) throws SQLException {
return new ReservedBook(
rs.getLong("book_id"),
rs.getLong("user_id"),
rs.getTimestamp("reserved_date").toInstant()
);
}
}

View File

@@ -28,7 +28,7 @@ public class EmailDatabaseAdapter implements EmailDatabase {
public Optional<String> getUserEmailAddress(Long userId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT email FROM user WHERE id = ?",
"SELECT email FROM library_user WHERE id = ?",
String.class,
userId));
} catch (DataAccessException ex){

View File

@@ -11,7 +11,7 @@ import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="user")
@Table(name="library_user")
@EqualsAndHashCode
public class User {

View File

@@ -3,6 +3,7 @@ package io.wkrzywiec.hexagonal.library.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.BorrowingFacade;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.CancelOverdueReservations;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.GiveBackBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
@@ -12,7 +13,6 @@ import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.SpringBorr
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
public class BorrowingDomainConfig {
@@ -45,6 +45,12 @@ public class BorrowingDomainConfig {
return new BorrowingFacade(database, borrowingEventPublisher);
}
@Bean
@Qualifier("GiveBackBook")
public GiveBackBook giveBackBook(BorrowingDatabase database, BorrowingEventPublisher borrowingEventPublisher){
return new BorrowingFacade(database, borrowingEventPublisher);
}
@Bean
@Qualifier("CancelOverdueReservations")
public CancelOverdueReservations cancelOverdueReservations(BorrowingDatabase database, BorrowingEventPublisher borrowingEventPublisher){

View File

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

View File

@@ -1,9 +0,0 @@
spring:
application:
name: library
datasource:
url: jdbc:postgresql://localhost:5432/library
driverClassName: org.postgresql.Driver
username: library
password: library
jpa.database-platform: org.hibernate.dialect.PostgreSQL9Dialect

View File

@@ -1,6 +1,39 @@
spring:
application:
name: library
profiles:
active: @active-profiles@
management:
endpoints:
web:
exposure:
include: "*"
---
spring:
profiles: default
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password: password
jpa.database-platform: org.hibernate.dialect.H2Dialect
---
spring:
profiles: local-postgres
datasource:
url: jdbc:postgresql://localhost:5432/library
driverClassName: org.postgresql.Driver
username: library
password: library
jpa.database-platform: org.hibernate.dialect.PostgreSQL9Dialect
---
spring:
profiles: prod
datasource:
url: jdbc:postgresql://${POSTGRES_SERVER}:5432/${POSTGRES_DB}
driverClassName: org.postgresql.Driver

View File

@@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS public.user (
CREATE TABLE IF NOT EXISTS library_user (
id BIGSERIAL PRIMARY KEY,
first_name CHARACTER VARYING(255) NOT NULL,
last_name CHARACTER VARYING(255) NOT NULL,

View File

@@ -7,12 +7,12 @@ CREATE TABLE IF NOT EXISTS public.reserved (
id BIGSERIAL PRIMARY KEY,
reserved_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
book_id BIGINT NOT NULL REFERENCES public.book,
user_id BIGINT NOT NULL REFERENCES public.user
user_id BIGINT NOT NULL REFERENCES public.library_user
);
CREATE TABLE IF NOT EXISTS public.borrowed (
id BIGSERIAL PRIMARY KEY,
borrowed_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
book_id BIGINT NOT NULL REFERENCES public.book,
user_id BIGINT NOT NULL REFERENCES public.user
user_id BIGINT NOT NULL REFERENCES public.library_user
);

View File

@@ -0,0 +1,42 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.BorrowingFacade
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingEventPublisher
import spock.lang.Narrative
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Title
@Title("Unit tests for borrowing book logic")
@Narrative("""
BorrowingFacade class encapsulate a logic of
managing books in a library. It contains methods
responsible for registering new book, borrowing it,
reserving it and taking it back.
""")
@Subject(BorrowingFacade)
class BorrowingFacadeSpec extends Specification {
private BorrowingFacade facade
private InMemoryBorrowingDatabase database
private BorrowingEventPublisher eventPublisher
def setup() {
database = new InMemoryBorrowingDatabase()
eventPublisher = new BorrowingEventPublisherFake()
facade = new BorrowingFacade(database, eventPublisher)
}
def "Make a book available"() {
given: "prepare a command"
def makeBookAvailableCommand = new MakeBookAvailableCommand(100)
when: "receive MakeBookAvailableCommand"
facade.handle(makeBookAvailableCommand)
then: "check database to have this book as available"
database.availableBooks[100L] == new AvailableBook(100)
}
}

View File

@@ -0,0 +1,45 @@
package io.wkrzywiec.hexagonal.library;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
@RequiredArgsConstructor
public class DatabaseHelper {
private final JdbcTemplate jdbcTemplate;
public Long getHomoDeusBookId(){
return jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
BookTestData.homoDeusBookTitle());
}
public Long getJohnDoeUserId(){
return jdbcTemplate.queryForObject(
"SELECT id FROM library_user WHERE email = ?",
Long.class,
UserTestData.johnDoeEmail());
}
public Long getPrimaryKeyOfAvailableByBookBy(Long bookId){
return jdbcTemplate.queryForObject(
"SELECT book_id FROM available WHERE book_id = ?",
Long.class,
bookId);
}
public Long getPrimaryKeyOfReservationByBookId(Long bookId){
return jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
Long.class,
bookId);
}
public Long getPrimaryKeyOfBorrowedByBookId(Long bookId){
return jdbcTemplate.queryForObject(
"SELECT book_id FROM borrowed WHERE book_id = ?",
Long.class,
bookId);
}
}

View File

@@ -3,9 +3,11 @@ package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import java.util.ArrayList;
import java.util.List;
public class BorrowTestData {
@@ -16,11 +18,31 @@ public class BorrowTestData {
.build();
}
public static GiveBackBookCommand anyGiveBookCommand(Long bookId, Long userId){
return GiveBackBookCommand.builder()
.bookId(bookId)
.userId(userId)
.build();
}
public static ReservedBook anyReservedBook(Long bookId, Long userId){
return new ReservedBook(bookId, userId);
}
public static BorrowedBook anyBorrowedBook(Long bookId, Long userId){
return new BorrowedBook(bookId, userId);
}
public static ActiveUser anyActiveUser(Long userId){
return new ActiveUser(userId, new ArrayList<ReservedBook>(), new ArrayList<BorrowedBook>());
}
public static ActiveUser anyActiveUserWithReservedBooks(Long userId, List<ReservedBook> reservedBookList){
return new ActiveUser(userId, reservedBookList, new ArrayList<BorrowedBook>());
}
public static ActiveUser anyActiveUserWithBorrowedBooks(Long userId, List<BorrowedBook> borrowedBooksList){
return new ActiveUser(userId, new ArrayList<ReservedBook>(), borrowedBooksList);
}
}

View File

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

View File

@@ -4,12 +4,19 @@ import io.wkrzywiec.hexagonal.library.domain.borrowing.core.BorrowingFacade;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationId;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.ActiveUserNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.AvailableBookNotFoundExeption;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.TooManyBooksAssignedToUserException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingEventPublisher;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.jupiter.api.BeforeEach;
@@ -18,12 +25,19 @@ import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BorrowingFacadeTest {
class BorrowingFacadeTest {
private BorrowingFacade facade;
private InMemoryBorrowingDatabase database;
@@ -138,7 +152,7 @@ public class BorrowingFacadeTest {
public void givenBookIsReserved_when3daysPass_thenBookIsAvailable(){
//given
ReservedBook reservedBook = ReservationTestData.anyReservedBook(100L, 100L);
changeReservationTimeFor(reservedBook, 4L);
changeReservationTimeFor(reservedBook, Instant.now().minus(4, ChronoUnit.DAYS));
database.reservedBooks.put(100L, reservedBook);
//when
@@ -153,7 +167,7 @@ public class BorrowingFacadeTest {
public void givenBookIsReserved_when2daysPass_thenBookIsStillReserved(){
//given
ReservedBook reservedBook = ReservationTestData.anyReservedBook(100L, 100L);
changeReservationTimeFor(reservedBook, 2L);
changeReservationTimeFor(reservedBook, Instant.now().minus(2, ChronoUnit.DAYS));
database.reservedBooks.put(100L, reservedBook);
//when
@@ -163,14 +177,6 @@ public class BorrowingFacadeTest {
assertEquals(1, database.reservedBooks.size());
}
private void changeReservationTimeFor(ReservedBook reservedBook, Long daysFromNow) {
try {
FieldUtils.writeField(reservedBook, "reservedDate", Instant.now().plus(daysFromNow, ChronoUnit.DAYS), true);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Test
@DisplayName("Successfully borrow a book")
public void givenReservedBookAndActiveUser_whenBorrowing_thenBookIsBorrowed(){
@@ -188,4 +194,109 @@ public class BorrowingFacadeTest {
//then
assertEquals(1, activeUser.getBorrowedBookList().size());
}
@Test
@DisplayName("Successful give back a book")
public void givenUserWithBorrowedBook_whenBookIsReturned_thenBookIsAvailable(){
//given
GiveBackBookCommand giveBackBookCommand = BorrowTestData.anyGiveBookCommand(100L, 100L);
BorrowedBook borrowedBook = BorrowTestData.anyBorrowedBook(giveBackBookCommand.getBookId(), giveBackBookCommand.getUserId());
ActiveUser activeUser = BorrowTestData.anyActiveUserWithBorrowedBooks(giveBackBookCommand.getUserId(), new ArrayList<BorrowedBook>(Arrays.asList(borrowedBook)));
database.borrowedBooks.put(borrowedBook.getIdAsLong(), borrowedBook);
database.activeUsers.put(activeUser.getIdAsLong(), activeUser);
//when
facade.handle(giveBackBookCommand);
//then
assertEquals(0, database.borrowedBooks.size());
assertEquals(1, database.availableBooks.size());
assertEquals(0, database.activeUsers.get(activeUser.getIdAsLong()).getBorrowedBookList().size());
}
private void changeReservationTimeFor(ReservedBook reservedBook, Instant reservationDate) {
try {
FieldUtils.writeField(reservedBook, "reservedDate", reservationDate, true);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
class InMemoryBorrowingDatabase implements BorrowingDatabase {
ConcurrentHashMap<Long, ActiveUser> activeUsers = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, AvailableBook> availableBooks = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, ReservedBook> reservedBooks = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, BorrowedBook> borrowedBooks = new ConcurrentHashMap<>();
@Override
public void save(AvailableBook availableBook) {
availableBooks.put(availableBook.getIdAsLong(), availableBook);
reservedBooks.remove(availableBook.getIdAsLong());
borrowedBooks.remove(availableBook.getIdAsLong());
}
@Override
public Optional<AvailableBook> getAvailableBook(Long bookId) {
if (availableBooks.containsKey(bookId)) {
return Optional.of(availableBooks.get(bookId));
} else {
return Optional.empty();
}
}
@Override
public Optional<ActiveUser> getActiveUser(Long userId) {
if (activeUsers.containsKey(userId)) {
return Optional.of(activeUsers.get(userId));
} else {
return Optional.empty();
}
}
@Override
public ReservationDetails save(ReservedBook reservedBook) {
Long reservationId = new Random().nextLong();
availableBooks.remove(reservedBook.getIdAsLong());
reservedBooks.put(reservationId, reservedBook);
return new ReservationDetails(new ReservationId(reservationId), reservedBook);
}
@Override
public void save(BorrowedBook borrowedBook) {
reservedBooks.remove(borrowedBook.getIdAsLong());
borrowedBooks.put(borrowedBook.getIdAsLong(), borrowedBook);
}
@Override
public List<OverdueReservation> findReservationsForMoreThan(Long days) {
return reservedBooks.values().stream()
.filter(reservedBook ->
Instant.now().isAfter(
reservedBook.getReservedDateAsInstant().plus(days, ChronoUnit.DAYS)))
.map(reservedBook ->
new OverdueReservation(
1L,
reservedBook.getIdAsLong()))
.collect(Collectors.toList());
}
@Override
public Optional<ReservedBook> getReservedBook(Long bookId) {
return Optional.of(reservedBooks.get(bookId));
}
@Override
public Optional<BorrowedBook> getBorrowedBook(Long bookId) {
return Optional.of(borrowedBooks.get(bookId));
}
}
class BorrowingEventPublisherFake implements BorrowingEventPublisher {
@Override
public void publish(BookReservedEvent event) {
}
}

View File

@@ -1,81 +0,0 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.DueDate;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationId;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class InMemoryBorrowingDatabase implements BorrowingDatabase {
ConcurrentHashMap<Long, ActiveUser> activeUsers = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, AvailableBook> availableBooks = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, ReservedBook> reservedBooks = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, BorrowedBook> borrowedBooks = new ConcurrentHashMap<>();
@Override
public void setBookAvailable(Long bookId) {
availableBooks.put(bookId, new AvailableBook(bookId));
reservedBooks.remove(bookId);
}
@Override
public Optional<AvailableBook> getAvailableBook(Long bookId) {
if (availableBooks.containsKey(bookId)) {
return Optional.of(availableBooks.get(bookId));
} else {
return Optional.empty();
}
}
@Override
public Optional<ActiveUser> getActiveUser(Long userId) {
if (activeUsers.containsKey(userId)) {
return Optional.of(activeUsers.get(userId));
} else {
return Optional.empty();
}
}
@Override
public ReservationDetails save(ReservedBook reservedBook) {
Long reservationId = new Random().nextLong();
availableBooks.remove(reservedBook.getIdAsLong());
reservedBooks.put(reservationId, reservedBook);
return new ReservationDetails(new ReservationId(reservationId), reservedBook);
}
@Override
public void save(BorrowedBook borrowedBook) {
reservedBooks.remove(borrowedBook.getIdAsLong());
borrowedBooks.put(borrowedBook.getIdAsLong(), borrowedBook);
}
@Override
public List<OverdueReservation> findReservationsAfter(DueDate dueDate) {
return reservedBooks.values().stream()
.filter(reservedBook ->
reservedBook.getReservedDateAsInstant()
.isAfter(dueDate.asInstant()))
.map(reservedBook ->
new OverdueReservation(
1L,
reservedBook.getIdAsLong()))
.collect(Collectors.toList());
}
@Override
public Optional<ReservedBook> getReservedBook(Long bookId) {
return Optional.of(reservedBooks.get(bookId));
}
}

View File

@@ -1,7 +1,6 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.UserTestData;
import io.wkrzywiec.hexagonal.library.DatabaseHelper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
@@ -10,7 +9,6 @@ import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReserva
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -31,12 +29,13 @@ public class BorrowingDatabaseAdapterITCase {
@Autowired
private JdbcTemplate jdbcTemplate;
private DatabaseHelper databaseHelper;
private BorrowingDatabaseAdapter database;
@BeforeEach
public void init(){
database = new BorrowingDatabaseAdapter(jdbcTemplate);
databaseHelper = new DatabaseHelper(jdbcTemplate);
}
@Test
@@ -45,17 +44,14 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldSaveAvailableBook(){
//given
Long bookId = getHomoDeusBookId();
Long bookId = databaseHelper.getHomoDeusBookId();
//when
database.setBookAvailable(bookId);
database.save(new AvailableBook(bookId));
//then
Long savedBookId = jdbcTemplate.queryForObject(
"SELECT book_id FROM available WHERE book_id = ?",
Long.class,
bookId);
assertEquals(bookId, savedBookId);
Long id = databaseHelper.getPrimaryKeyOfAvailableByBookBy(bookId);
assertTrue(id > 0);
}
@Test
@@ -64,7 +60,7 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldGetAvailableBook(){
//given
Long bookId = getHomoDeusBookId();
Long bookId = databaseHelper.getHomoDeusBookId();
//when
Optional<AvailableBook> availableBookOptional = database.getAvailableBook(bookId);
@@ -80,7 +76,7 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldGetActiveUser() {
//given
Long activeUserId = getJohnDoeUserId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
//when
Optional<ActiveUser> activeUserOptional = database.getActiveUser(activeUserId);
@@ -96,9 +92,8 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldSaveReservedBook(){
//given
Long bookId = getHomoDeusBookId();
Long activeUserId = getJohnDoeUserId();
Long bookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
ReservedBook reservedBook = new ReservedBook(bookId, activeUserId);
@@ -112,18 +107,12 @@ public class BorrowingDatabaseAdapterITCase {
}
@Test
@Disabled
@DisplayName("Get reserved book by its id")
@Sql({"/book-and-user.sql"})
@Sql({"/book-and-user.sql", "/reserved-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldFindReservedBook(){
//given
Long bookId = getHomoDeusBookId();
Long johnDoeUserId = getJohnDoeUserId();
jdbcTemplate.update(
"INSERT INTO public.reserved (book_id, user_id) VALUES (?, ?)",
bookId,
johnDoeUserId);
Long bookId = databaseHelper.getHomoDeusBookId();
//when
Optional<ReservedBook> reservedBook = database.getReservedBook(bookId);
@@ -139,8 +128,8 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldSaveBorrowedBook(){
//given
Long bookId = getHomoDeusBookId();
Long activeUserId = getJohnDoeUserId();
Long bookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
BorrowedBook borrowedBook = new BorrowedBook(bookId, activeUserId);
@@ -148,10 +137,7 @@ public class BorrowingDatabaseAdapterITCase {
database.save(borrowedBook);
//then
Long savedBookId = jdbcTemplate.queryForObject(
"SELECT book_id FROM borrowed WHERE book_id = ?",
Long.class,
bookId);
Long savedBookId = databaseHelper.getPrimaryKeyOfBorrowedByBookId(bookId);
assertEquals(bookId, savedBookId);
}
@@ -161,33 +147,34 @@ public class BorrowingDatabaseAdapterITCase {
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldFindOverdueReservations(){
//given
DueDate thirdDayAfterReservation = new DueDate(Instant.now().plus(3, ChronoUnit.DAYS));
Long overdueBookId = getHomoDeusBookId();
Long johnDoeUserId = getJohnDoeUserId();
Long overdueBookId = databaseHelper.getHomoDeusBookId();
Long johnDoeUserId = databaseHelper.getJohnDoeUserId();
jdbcTemplate.update(
"INSERT INTO public.reserved (book_id, user_id, reserved_date) VALUES (?, ?, ?)",
overdueBookId,
johnDoeUserId,
Instant.now().plus(3, ChronoUnit.DAYS));
Instant.now().plus(4, ChronoUnit.DAYS));
//when
OverdueReservation overdueReservation = database.findReservationsAfter(thirdDayAfterReservation).get(0);
OverdueReservation overdueReservation = database.findReservationsForMoreThan(3L).get(0);
//then
assertEquals(overdueBookId, overdueReservation.getBookIdentificationAsLong());
}
private Long getHomoDeusBookId(){
return jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
BookTestData.homoDeusBookTitle());
}
@Test
@DisplayName("Find borrowed book by id")
@Sql({"/book-and-user.sql", "/borrowed-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldFindBorrowedBook(){
//given
Long bookId = databaseHelper.getHomoDeusBookId();
private Long getJohnDoeUserId(){
return jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
UserTestData.johnDoeEmail());
//when
Optional<BorrowedBook> borrowedBook = database.getBorrowedBook(bookId);
//then
assertTrue(borrowedBook.isPresent());
assertEquals(bookId, borrowedBook.get().getIdAsLong());
}
}

View File

@@ -1,6 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.email.infrastructure;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.DatabaseHelper;
import io.wkrzywiec.hexagonal.library.UserTestData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -20,12 +21,13 @@ public class EmailDatabaseAdapterITCase {
@Autowired
private JdbcTemplate jdbcTemplate;
private DatabaseHelper databaseHelper;
private EmailDatabaseAdapter emailDatabase;
@BeforeEach
public void init(){
emailDatabase = new EmailDatabaseAdapter(jdbcTemplate);
databaseHelper = new DatabaseHelper(jdbcTemplate);
}
@Test
@@ -34,10 +36,7 @@ public class EmailDatabaseAdapterITCase {
@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,
BookTestData.homoDeusBookTitle());
Long bookId = databaseHelper.getHomoDeusBookId();
//when
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId);
@@ -52,10 +51,7 @@ public class EmailDatabaseAdapterITCase {
@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,
BookTestData.homoDeusBookTitle());
Long bookId = databaseHelper.getHomoDeusBookId();
//when
Optional<String> bookTitle = emailDatabase.getTitleByBookId(bookId + 1124);
@@ -70,10 +66,7 @@ public class EmailDatabaseAdapterITCase {
@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,
UserTestData.johnDoeEmail());
Long userId = databaseHelper.getJohnDoeUserId();
//when
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId);
@@ -88,10 +81,7 @@ public class EmailDatabaseAdapterITCase {
@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,
UserTestData.johnDoeEmail());
Long userId = databaseHelper.getJohnDoeUserId();
//when
Optional<String> emailAddress = emailDatabase.getUserEmailAddress(userId + 1124);

View File

@@ -1,5 +1,6 @@
package io.wkrzywiec.hexagonal.library.domain.user.infrastructure;
import io.wkrzywiec.hexagonal.library.DatabaseHelper;
import io.wkrzywiec.hexagonal.library.UserTestData;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.EmailAddress;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.User;
@@ -20,6 +21,7 @@ public class UserDatabaseAdapterITCase {
@Autowired
private JdbcTemplate jdbcTemplate;
private DatabaseHelper databaseHelper;
@Autowired
private UserRepository userRepository;
@@ -29,6 +31,7 @@ public class UserDatabaseAdapterITCase {
@BeforeEach
public void init(){
userDatabase = new UserDatabaseAdapter(userRepository);
databaseHelper = new DatabaseHelper(jdbcTemplate);
}
@Test
@@ -45,10 +48,7 @@ public class UserDatabaseAdapterITCase {
UserIdentifier userIdentifier = userDatabase.save(user);
//then
Long savedUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
UserTestData.johnDoeEmail());
Long savedUserId = databaseHelper.getJohnDoeUserId();
assertEquals(userIdentifier.getAsLong(), savedUserId);
}

View File

@@ -0,0 +1,47 @@
# Name of the implementation class(es) of report creator(s) to enable (separate multiple entries with commas)
# Currently supported classes are:
# 1. com.athaydes.spockframework.report.internal.HtmlReportCreator
# 2. com.athaydes.spockframework.report.template.TemplateReportCreator
com.athaydes.spockframework.report.IReportCreator=com.athaydes.spockframework.report.internal.HtmlReportCreator
# Set properties of the report creator
# For the HtmlReportCreator, the only properties available are
# (the location of the css files is relative to the classpath):
com.athaydes.spockframework.report.internal.HtmlReportCreator.featureReportCss=spock-feature-report.css
com.athaydes.spockframework.report.internal.HtmlReportCreator.summaryReportCss=spock-summary-report.css
com.athaydes.spockframework.report.internal.HtmlReportCreator.printThrowableStackTrace=false
com.athaydes.spockframework.report.internal.HtmlReportCreator.inlineCss=true
com.athaydes.spockframework.report.internal.HtmlReportCreator.enabled=true
# options are: "class_name_and_title", "class_name", "title"
com.athaydes.spockframework.report.internal.HtmlReportCreator.specSummaryNameOption=class_name_and_title
# exclude Specs Table of Contents
com.athaydes.spockframework.report.internal.HtmlReportCreator.excludeToc=false
# Output directory (where the spock reports will be created) - relative to working directory
com.athaydes.spockframework.report.outputDir=target/spock-reports
# Output directory where to store the aggregated JSON report (used to support parallel builds)
com.athaydes.spockframework.report.aggregatedJsonReportDir=
# If set to true, hides blocks which do not have any description
com.athaydes.spockframework.report.hideEmptyBlocks=false
# Set the name of the project under test so it can be displayed in the report
com.athaydes.spockframework.report.projectName=
# Set the version of the project under test so it can be displayed in the report
com.athaydes.spockframework.report.projectVersion=Unknown
# Show the source code for each block
com.athaydes.spockframework.report.showCodeBlocks=true
# Set the root location of the Spock test source code (only used if showCodeBlocks is 'true')
com.athaydes.spockframework.report.testSourceRoots=src/test/groovy
# Set properties specific to the TemplateReportCreator
com.athaydes.spockframework.report.template.TemplateReportCreator.specTemplateFile=/templateReportCreator/spec-template.md
com.athaydes.spockframework.report.template.TemplateReportCreator.reportFileExtension=md
com.athaydes.spockframework.report.template.TemplateReportCreator.summaryTemplateFile=/templateReportCreator/summary-template.md
com.athaydes.spockframework.report.template.TemplateReportCreator.summaryFileName=summary.md
com.athaydes.spockframework.report.template.TemplateReportCreator.enabled=true

View File

@@ -1,17 +1,17 @@
INSERT INTO public.author (name) VALUES
INSERT INTO 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
INSERT INTO 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)
INSERT INTO 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
INSERT INTO library_user (first_name, last_name, email) VALUES
('John','Doe','john.doe@test.com')
;

View File

@@ -0,0 +1,5 @@
INSERT INTO public.borrowed (book_id, user_id)
VALUES (
(SELECT id FROM book WHERE title = 'Homo Deus'),
(SELECT id FROM library_user WHERE email = 'john.doe@test.com')
);

View File

@@ -2,6 +2,6 @@ DELETE FROM public.borrowed;
DELETE FROM public.reserved;
DELETE FROM public.available;
DELETE FROM public.book_author;
DELETE FROM public.user;
DELETE FROM public.library_user;
DELETE FROM public.book;
DELETE FROM public.author;

View File

@@ -0,0 +1,5 @@
INSERT INTO public.reserved (book_id, user_id)
VALUES (
(SELECT id FROM book WHERE title = 'Homo Deus'),
(SELECT id FROM library_user WHERE email = 'john.doe@test.com')
);