From d1884f6c46ed44be6bfb0046d02b0ecfc244cac8 Mon Sep 17 00:00:00 2001 From: Chakib BENSARI Date: Tue, 5 Sep 2023 17:53:08 +0200 Subject: [PATCH 1/5] Feat : implementing service Layer with ITs OK --- .../test/api/TechnicalTestApiApplication.java | 9 +- .../test/api/mappers/AuthorMapper.java | 14 + .../test/api/mappers/BookMapper.java | 14 + .../representations/AuthorRepresentation.java | 23 ++ .../representations/BookRepresentation.java | 26 ++ .../test/api/services/LibraryService.java | 22 ++ .../test/api/services/LibraryServiceImpl.java | 116 ++++++++ .../test/api/storage/models/Author.java | 22 ++ .../test/api/storage/models/Book.java | 24 ++ .../repositories/AuthorRepository.java | 13 + .../storage/repositories/BookRepository.java | 15 + .../api/services/LibraryServiceImplTest.java | 266 ++++++++++-------- 12 files changed, 445 insertions(+), 119 deletions(-) create mode 100644 technical-test-api/src/main/java/technical/test/api/mappers/AuthorMapper.java create mode 100644 technical-test-api/src/main/java/technical/test/api/mappers/BookMapper.java create mode 100644 technical-test-api/src/main/java/technical/test/api/representations/AuthorRepresentation.java create mode 100644 technical-test-api/src/main/java/technical/test/api/representations/BookRepresentation.java create mode 100644 technical-test-api/src/main/java/technical/test/api/services/LibraryService.java create mode 100644 technical-test-api/src/main/java/technical/test/api/services/LibraryServiceImpl.java create mode 100644 technical-test-api/src/main/java/technical/test/api/storage/models/Author.java create mode 100644 technical-test-api/src/main/java/technical/test/api/storage/models/Book.java create mode 100644 technical-test-api/src/main/java/technical/test/api/storage/repositories/AuthorRepository.java create mode 100644 technical-test-api/src/main/java/technical/test/api/storage/repositories/BookRepository.java diff --git a/technical-test-api/src/main/java/technical/test/api/TechnicalTestApiApplication.java b/technical-test-api/src/main/java/technical/test/api/TechnicalTestApiApplication.java index c8b4f4d..1f4066b 100644 --- a/technical-test-api/src/main/java/technical/test/api/TechnicalTestApiApplication.java +++ b/technical-test-api/src/main/java/technical/test/api/TechnicalTestApiApplication.java @@ -2,12 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +@EnableReactiveMongoRepositories @SpringBootApplication public class TechnicalTestApiApplication { - public static void main(String[] args) { - SpringApplication.run(TechnicalTestApiApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(TechnicalTestApiApplication.class, args); + } } diff --git a/technical-test-api/src/main/java/technical/test/api/mappers/AuthorMapper.java b/technical-test-api/src/main/java/technical/test/api/mappers/AuthorMapper.java new file mode 100644 index 0000000..cdbf14e --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/mappers/AuthorMapper.java @@ -0,0 +1,14 @@ +package technical.test.api.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import technical.test.api.representations.AuthorRepresentation; +import technical.test.api.storage.models.Author; + +@Mapper(componentModel = "spring") +public interface AuthorMapper { + + AuthorMapper INSTANCE = Mappers.getMapper(AuthorMapper.class); + + AuthorRepresentation toAuthorRepresentation(Author book); +} diff --git a/technical-test-api/src/main/java/technical/test/api/mappers/BookMapper.java b/technical-test-api/src/main/java/technical/test/api/mappers/BookMapper.java new file mode 100644 index 0000000..9c25561 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/mappers/BookMapper.java @@ -0,0 +1,14 @@ +package technical.test.api.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import technical.test.api.representations.BookRepresentation; +import technical.test.api.storage.models.Book; + +@Mapper(componentModel = "spring") +public interface BookMapper { + + BookMapper INSTANCE = Mappers.getMapper(BookMapper.class); + + BookRepresentation toBookRepresentation(Book book); +} diff --git a/technical-test-api/src/main/java/technical/test/api/representations/AuthorRepresentation.java b/technical-test-api/src/main/java/technical/test/api/representations/AuthorRepresentation.java new file mode 100644 index 0000000..c60cb17 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/representations/AuthorRepresentation.java @@ -0,0 +1,23 @@ +package technical.test.api.representations; + +import java.io.Serial; +import java.io.Serializable; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class AuthorRepresentation implements Serializable { + + @Serial private static final long serialVersionUID = -5828406190179381271L; + + private String id; + + private Integer birthdate; + + private String firstname; + + private String lastname; +} diff --git a/technical-test-api/src/main/java/technical/test/api/representations/BookRepresentation.java b/technical-test-api/src/main/java/technical/test/api/representations/BookRepresentation.java new file mode 100644 index 0000000..baf5155 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/representations/BookRepresentation.java @@ -0,0 +1,26 @@ +package technical.test.api.representations; + +import java.io.Serial; +import java.io.Serializable; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +@Builder +@Getter +@Setter +public class BookRepresentation implements Serializable { + + @Serial private static final long serialVersionUID = -5822406190179321283L; + + @Id private String id; + + private String isbn; + + private String title; + + private Integer releaseDate; + + private String authorId; +} diff --git a/technical-test-api/src/main/java/technical/test/api/services/LibraryService.java b/technical-test-api/src/main/java/technical/test/api/services/LibraryService.java new file mode 100644 index 0000000..6257afb --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/services/LibraryService.java @@ -0,0 +1,22 @@ +package technical.test.api.services; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import technical.test.api.representations.AuthorRepresentation; +import technical.test.api.representations.BookRepresentation; + +public interface LibraryService { + + Mono registerBook( + String isbn, String title, Integer releaseDate, String authorId); + + Mono registerAuthor(String firstName, String lastname, Integer birthDate); + + Flux findAllBooks(); + + Flux findBookByAuthorAndDateBetween( + String authorId, Integer startDate, Integer endDate); + + Flux findAuthorByFirstnameAndLastnameAndBirthdate( + String firstname, String lastname, Integer birthdate); +} diff --git a/technical-test-api/src/main/java/technical/test/api/services/LibraryServiceImpl.java b/technical-test-api/src/main/java/technical/test/api/services/LibraryServiceImpl.java new file mode 100644 index 0000000..3e1ead1 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/services/LibraryServiceImpl.java @@ -0,0 +1,116 @@ +package technical.test.api.services; + +import java.time.Year; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import technical.test.api.mappers.AuthorMapper; +import technical.test.api.mappers.BookMapper; +import technical.test.api.representations.AuthorRepresentation; +import technical.test.api.representations.BookRepresentation; +import technical.test.api.storage.models.Author; +import technical.test.api.storage.models.Book; +import technical.test.api.storage.repositories.AuthorRepository; +import technical.test.api.storage.repositories.BookRepository; + +@Service +public class LibraryServiceImpl implements LibraryService { + + private final BookRepository bookRepository; + + private final AuthorRepository authorRepository; + + private final BookMapper bookMapper; + + private final AuthorMapper authorMapper; + + @Value("${library.release-dates.minimum-year:1800}") + private Integer minimumYear; + + public LibraryServiceImpl( + BookRepository bookRepository, + AuthorRepository authorRepository, + BookMapper bookMapper, + AuthorMapper authorMapper) { + this.bookRepository = bookRepository; + this.authorRepository = authorRepository; + this.bookMapper = bookMapper; + this.authorMapper = authorMapper; + } + + @Override + public Mono registerBook( + String isbn, String title, Integer releaseDate, String authorId) { + return bookRepository + .save( + Book.builder() + .isbn(isbn) + .title(title) + .authorId(authorId) + .releaseDate(releaseDate) + .build()) + .map(bookMapper::toBookRepresentation); + } + + @Override + public Mono registerAuthor( + String firstName, String lastname, Integer birthDate) { + return authorRepository + .save( + Author.builder() + .id( + String.join( + "_", StringUtils.lowerCase(firstName), StringUtils.lowerCase(lastname))) + .birthdate(birthDate) + .firstname(firstName) + .lastname(lastname) + .build()) + .map(authorMapper::toAuthorRepresentation); + } + + @Override + public Flux findAllBooks() { + return bookRepository.findAll().map(bookMapper::toBookRepresentation); + } + + @Override + public Flux findBookByAuthorAndDateBetween( + String authorId, Integer startDate, Integer endDate) { + Range releaseDateRange = buildReleaseDateRange(startDate, endDate); + if (StringUtils.isNotEmpty(authorId)) { + return bookRepository.findByAuthorIdAndReleaseDateBetween(authorId, releaseDateRange); + } + return bookRepository.findByReleaseDateBetween(releaseDateRange); + } + + @Override + public Flux findAuthorByFirstnameAndLastnameAndBirthdate( + String firstname, String lastname, Integer birthdate) { + if (StringUtils.isEmpty(firstname) + && StringUtils.isEmpty(lastname) + && Objects.isNull(birthdate)) { + return authorRepository.findAll().map(authorMapper::toAuthorRepresentation); + } + return authorRepository + .findByFirstnameAndLastnameAndBirthdate(firstname, lastname, birthdate) + .map(authorMapper::toAuthorRepresentation); + } + + private Range buildReleaseDateRange(Integer startDate, Integer endDate) { + Range range = + Range.from(Bound.inclusive(minimumYear)).to(Bound.inclusive(Year.now().getValue())); + if (!Objects.isNull(startDate) && !Objects.isNull(endDate)) { + range = Range.from(Bound.inclusive(startDate)).to(Bound.inclusive(endDate)); + } else if (Objects.isNull(startDate) && !Objects.isNull(endDate)) { + range = Range.from(Bound.inclusive(minimumYear)).to(Bound.inclusive(endDate)); + } else if (!Objects.isNull(startDate)) { + range = Range.from(Bound.inclusive(startDate)).to(Bound.inclusive(Year.now().getValue())); + } + return range; + } +} diff --git a/technical-test-api/src/main/java/technical/test/api/storage/models/Author.java b/technical-test-api/src/main/java/technical/test/api/storage/models/Author.java new file mode 100644 index 0000000..645ed36 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/storage/models/Author.java @@ -0,0 +1,22 @@ +package technical.test.api.storage.models; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Builder +@Getter +@Setter +@Document +public class Author { + + @Id private String id; + + private Integer birthdate; + + private String firstname; + + private String lastname; +} diff --git a/technical-test-api/src/main/java/technical/test/api/storage/models/Book.java b/technical-test-api/src/main/java/technical/test/api/storage/models/Book.java new file mode 100644 index 0000000..9fdb99b --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/storage/models/Book.java @@ -0,0 +1,24 @@ +package technical.test.api.storage.models; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Builder +@Getter +@Setter +@Document +public class Book { + + @Id private String id; + + private String isbn; + + private String title; + + private Integer releaseDate; + + private String authorId; +} diff --git a/technical-test-api/src/main/java/technical/test/api/storage/repositories/AuthorRepository.java b/technical-test-api/src/main/java/technical/test/api/storage/repositories/AuthorRepository.java new file mode 100644 index 0000000..01ae367 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/storage/repositories/AuthorRepository.java @@ -0,0 +1,13 @@ +package technical.test.api.storage.repositories; + +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import technical.test.api.storage.models.Author; + +@Repository +public interface AuthorRepository extends ReactiveMongoRepository { + + Flux findByFirstnameAndLastnameAndBirthdate( + String firstname, String lastname, Integer birthdate); +} diff --git a/technical-test-api/src/main/java/technical/test/api/storage/repositories/BookRepository.java b/technical-test-api/src/main/java/technical/test/api/storage/repositories/BookRepository.java new file mode 100644 index 0000000..f14e941 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/storage/repositories/BookRepository.java @@ -0,0 +1,15 @@ +package technical.test.api.storage.repositories; + +import org.springframework.data.domain.Range; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import technical.test.api.storage.models.Book; + +@Repository +public interface BookRepository extends ReactiveMongoRepository { + + Flux findByAuthorIdAndReleaseDateBetween(String authorId, Range openRange); + + Flux findByReleaseDateBetween(Range openRange); +} diff --git a/technical-test-api/src/test/java/technical/test/api/services/LibraryServiceImplTest.java b/technical-test-api/src/test/java/technical/test/api/services/LibraryServiceImplTest.java index 79a44ef..193fd6f 100644 --- a/technical-test-api/src/test/java/technical/test/api/services/LibraryServiceImplTest.java +++ b/technical-test-api/src/test/java/technical/test/api/services/LibraryServiceImplTest.java @@ -6,138 +6,174 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.utility.DockerImageName; import reactor.test.StepVerifier; import technical.test.api.TestSupport; +import technical.test.api.storage.repositories.AuthorRepository; import technical.test.api.storage.repositories.BookRepository; -@RunWith(SpringRunner.class) @SpringBootTest class LibraryServiceImplTest { - @Resource - private LibraryServiceImpl libraryServiceImpl; - @Resource - private BookRepository bookRepository; - @Resource - TestSupport testSupport; + @Resource private LibraryServiceImpl libraryServiceImpl; + @Resource private BookRepository bookRepository; + @Resource private AuthorRepository authorRepository; + @Resource TestSupport testSupport; - final static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:5")); + static final MongoDBContainer mongoDBContainer = + new MongoDBContainer(DockerImageName.parse("mongo:5")); - @BeforeAll - static void prepare() { - mongoDBContainer.start(); - } + @BeforeAll + static void prepare() { + mongoDBContainer.start(); + } - @AfterEach - void clear() { - bookRepository.deleteAll().block(); - } + @AfterEach + void clear() { - @Test - void registerAuthor() { - StepVerifier.create(libraryServiceImpl.registerAuthor("Isaac","Asimov", 1920)) - .expectSubscription() - .expectNextMatches(author -> author.getBirthdate() == 1920 - && author.getFirstname().equals("Isaac") - && author.getLastname().equals("Asimov") - && author.getId().equals("isaac_asimov")) - .verifyComplete(); - } + bookRepository.deleteAll().block(); + authorRepository.deleteAll().block(); + } - @Test - void registerBook() { - StepVerifier.create(libraryServiceImpl.registerBook("1234-5678-90","Fondation", 1951, "isaac_asimov")) - .expectSubscription() - .expectNextMatches(book -> StringUtils.equals(book.getIsbn(), "1234-5678-90") - && StringUtils.equals(book.getTitle(), "Fondation") - && book.getReleaseDate() == 1951 - && StringUtils.equals(book.getAuthorId(), "isaac_asimov")) - .verifyComplete(); - } + @Test + void registerAuthor() { + StepVerifier.create(libraryServiceImpl.registerAuthor("Isaac", "Asimov", 1920)) + .expectSubscription() + .expectNextMatches( + author -> + author.getBirthdate() == 1920 + && author.getFirstname().equals("Isaac") + && author.getLastname().equals("Asimov") + && author.getId().equals("isaac_asimov")) + .verifyComplete(); + } - @Test - void findAllBooks() { - StepVerifier.create(testSupport.loadBooks() - .then(testSupport.loadAuthor()) - .thenMany(libraryServiceImpl.findAllBooks())) - .expectSubscription() - .expectNextCount(14) - .verifyComplete(); - } + @Test + void registerBook() { + StepVerifier.create( + libraryServiceImpl.registerBook("1234-5678-90", "Fondation", 1951, "isaac_asimov")) + .expectSubscription() + .expectNextMatches( + book -> + StringUtils.equals(book.getIsbn(), "1234-5678-90") + && StringUtils.equals(book.getTitle(), "Fondation") + && book.getReleaseDate() == 1951 + && StringUtils.equals(book.getAuthorId(), "isaac_asimov")) + .verifyComplete(); + } - @Test - void findBookByAuthor() { - StepVerifier.create(testSupport.loadBooks() - .then(testSupport.loadAuthor()) - .then(libraryServiceImpl.findBookByAuthorAndDateBetween("douglas_adams", null, null).collectList())) - .expectSubscription() - .assertNext(books -> { - Assertions.assertThat(books).hasSize(1); - Assertions.assertThat(books) - .extracting("title") - .containsExactly("Le Guide du voyageur galactique"); - } - ) - .verifyComplete(); - } + @Test + void findAllBooks() { + StepVerifier.create( + testSupport + .loadBooks() + .then(testSupport.loadAuthor()) + .thenMany(libraryServiceImpl.findAllBooks())) + .expectSubscription() + .expectNextCount(14) + .verifyComplete(); + } - @Test - void findBookByAuthorAndDateBefore() { - StepVerifier.create(testSupport.loadBooks() - .then(testSupport.loadAuthor()) - .then(libraryServiceImpl.findBookByAuthorAndDateBetween("isaac_asimov", null, 1959).collectList())) - .expectSubscription() - .assertNext(books -> { - Assertions.assertThat(books).hasSize(6); - Assertions.assertThat(books) - .extracting("title") - .containsExactlyInAnyOrder("Fondation", "Fondation et Empire", "Seconde Fondation", - "Les Robots", "Les Cavernes d'acier", "Face aux feux du soleil"); - } - ) - .verifyComplete(); - } + @Test + void findBookByAuthor() { + StepVerifier.create( + testSupport + .loadBooks() + .then(testSupport.loadAuthor()) + .then( + libraryServiceImpl + .findBookByAuthorAndDateBetween("douglas_adams", null, null) + .collectList())) + .expectSubscription() + .assertNext( + books -> { + Assertions.assertThat(books).hasSize(1); + Assertions.assertThat(books) + .extracting("title") + .containsExactly("Le Guide du voyageur galactique"); + }) + .verifyComplete(); + } - @Test - void findBookByAuthorAndDateOver() { - StepVerifier.create(testSupport.loadBooks() - .then(testSupport.loadAuthor()) - .then(libraryServiceImpl.findBookByAuthorAndDateBetween("isaac_asimov", 1960, null).collectList())) - .expectSubscription() - .assertNext(books -> { - Assertions.assertThat(books).hasSize(7); - Assertions.assertThat(books) - .extracting("title") - .containsExactlyInAnyOrder("Fondation foudroyée", - "Terre et Fondation", - "Prélude à Fondation", - "L'Aube de Fondation", - "Un défilé de robots", - "Les Robots de l'aube", - "Les Robots et l'Empire"); - } - ) - .verifyComplete(); - } + @Test + void findBookByAuthorAndDateBefore() { + StepVerifier.create( + testSupport + .loadBooks() + .then(testSupport.loadAuthor()) + .then( + libraryServiceImpl + .findBookByAuthorAndDateBetween("isaac_asimov", null, 1959) + .collectList())) + .expectSubscription() + .assertNext( + books -> { + Assertions.assertThat(books).hasSize(6); + Assertions.assertThat(books) + .extracting("title") + .containsExactlyInAnyOrder( + "Fondation", + "Fondation et Empire", + "Seconde Fondation", + "Les Robots", + "Les Cavernes d'acier", + "Face aux feux du soleil"); + }) + .verifyComplete(); + } - @Test - void findBookByDateBetween() { - StepVerifier.create(testSupport.loadBooks() - .then(testSupport.loadAuthor()) - .then(libraryServiceImpl.findBookByAuthorAndDateBetween(null, 1950, 1955).collectList())) - .expectSubscription() - .assertNext(books -> { - Assertions.assertThat(books).hasSize(5); - Assertions.assertThat(books) - .extracting("title") - .containsExactlyInAnyOrder("Fondation", "Fondation et Empire", "Seconde Fondation", - "Les Robots", "Les Cavernes d'acier"); - } - ) - .verifyComplete(); - } + @Test + void findBookByAuthorAndDateOver() { + StepVerifier.create( + testSupport + .loadBooks() + .then(testSupport.loadAuthor()) + .then( + libraryServiceImpl + .findBookByAuthorAndDateBetween("isaac_asimov", 1960, null) + .collectList())) + .expectSubscription() + .assertNext( + books -> { + Assertions.assertThat(books).hasSize(7); + Assertions.assertThat(books) + .extracting("title") + .containsExactlyInAnyOrder( + "Fondation foudroyée", + "Terre et Fondation", + "Prélude à Fondation", + "L'Aube de Fondation", + "Un défilé de robots", + "Les Robots de l'aube", + "Les Robots et l'Empire"); + }) + .verifyComplete(); + } + + @Test + void findBookByDateBetween() { + StepVerifier.create( + testSupport + .loadBooks() + .then(testSupport.loadAuthor()) + .then( + libraryServiceImpl + .findBookByAuthorAndDateBetween(null, 1950, 1955) + .collectList())) + .expectSubscription() + .assertNext( + books -> { + Assertions.assertThat(books).hasSize(5); + Assertions.assertThat(books) + .extracting("title") + .containsExactlyInAnyOrder( + "Fondation", + "Fondation et Empire", + "Seconde Fondation", + "Les Robots", + "Les Cavernes d'acier"); + }) + .verifyComplete(); + } } From dea2a903f8da868e29a8605f356a8c143cf50a65 Mon Sep 17 00:00:00 2001 From: Chakib BENSARI Date: Tue, 5 Sep 2023 18:13:40 +0200 Subject: [PATCH 2/5] Feat : implementing controller endpoints Layer with ITs OK --- .../api/controller/LibraryController.java | 49 ++++ .../test/api/data/LoadInitialData.java | 25 ++ .../LibraryEndpointIntegrationTest.java | 254 +++++++++--------- 3 files changed, 205 insertions(+), 123 deletions(-) create mode 100644 technical-test-api/src/main/java/technical/test/api/controller/LibraryController.java create mode 100644 technical-test-api/src/main/java/technical/test/api/data/LoadInitialData.java diff --git a/technical-test-api/src/main/java/technical/test/api/controller/LibraryController.java b/technical-test-api/src/main/java/technical/test/api/controller/LibraryController.java new file mode 100644 index 0000000..fb7fce2 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/controller/LibraryController.java @@ -0,0 +1,49 @@ +package technical.test.api.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import technical.test.api.representations.AuthorRepresentation; +import technical.test.api.representations.BookRepresentation; +import technical.test.api.services.LibraryService; + +@CrossOrigin(origins = "http://localhost:8080") +@RequiredArgsConstructor +@RequestMapping("/library") +@RestController +public class LibraryController { + + private final LibraryService libraryService; + + @GetMapping("/authors") + public Flux findAuthors( + @RequestParam(name = "firstname", required = false) final String firstname, + @RequestParam(name = "lastname", required = false) final String lastname, + @RequestParam(name = "birthdate", required = false) final Integer birthdate) { + return libraryService.findAuthorByFirstnameAndLastnameAndBirthdate( + firstname, lastname, birthdate); + } + + @GetMapping("/books") + public Flux filterBooks( + @RequestParam(name = "authorRefId", required = false) final String authorRefId, + @RequestParam(name = "yearFrom", required = false) final Integer yearFrom, + @RequestParam(name = "yearTo", required = false) final Integer yearTo) { + return libraryService.findBookByAuthorAndDateBetween(authorRefId, yearFrom, yearTo); + } + + @PostMapping("/books") + public Mono filterBooks( + @RequestParam(name = "isbn") final String isbn, + @RequestParam(name = "title") final String title, + @RequestParam(name = "releaseDateYear") final Integer releaseDateYear, + @RequestParam(name = "authorRefId") final String authorRefId) { + return libraryService.registerBook(isbn, title, releaseDateYear, authorRefId); + } +} diff --git a/technical-test-api/src/main/java/technical/test/api/data/LoadInitialData.java b/technical-test-api/src/main/java/technical/test/api/data/LoadInitialData.java new file mode 100644 index 0000000..fa4dae1 --- /dev/null +++ b/technical-test-api/src/main/java/technical/test/api/data/LoadInitialData.java @@ -0,0 +1,25 @@ +package technical.test.api.data; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import technical.test.api.services.LibraryService; + +@RequiredArgsConstructor +@Component +public class LoadInitialData implements CommandLineRunner { + + private final LibraryService libraryService; + + @Override + public void run(String... args) { + libraryService.registerBook("123456", "Title1", 2023, "victor_hugo").block(); + libraryService.registerBook("12345", "Title2", 2023, "victor_hugo").block(); + libraryService.registerBook("1234", "Title3", 2023, "guillaume_musso").block(); + libraryService.registerBook("123", "Title4", 2023, "guillaume_musso").block(); + libraryService.registerBook("12", "Title5", 2023, "guillaume_musso").block(); + + libraryService.registerAuthor("Victor", "Hugo", 1989).block(); + libraryService.registerAuthor("Guillaume", "Musso", 1987).block(); + } +} diff --git a/technical-test-api/src/test/java/technical/test/api/endpoints/LibraryEndpointIntegrationTest.java b/technical-test-api/src/test/java/technical/test/api/endpoints/LibraryEndpointIntegrationTest.java index 0d0c7f6..5fb5f9c 100644 --- a/technical-test-api/src/test/java/technical/test/api/endpoints/LibraryEndpointIntegrationTest.java +++ b/technical-test-api/src/test/java/technical/test/api/endpoints/LibraryEndpointIntegrationTest.java @@ -1,146 +1,154 @@ package technical.test.api.endpoints; +import static org.assertj.core.api.Assertions.assertThat; import jakarta.annotation.Resource; +import java.util.List; import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.mongodb.core.ReactiveMongoOperations; -import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import technical.test.api.TestSupport; import technical.test.api.representations.AuthorRepresentation; import technical.test.api.representations.BookRepresentation; -import technical.test.api.storage.models.Author; -import technical.test.api.storage.models.Book; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import technical.test.api.storage.repositories.AuthorRepository; +import technical.test.api.storage.repositories.BookRepository; -@RunWith(SpringRunner.class) @AutoConfigureWebTestClient(timeout = "20000") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class LibraryEndpointIntegrationTest { - @Autowired - private WebTestClient webTestClient; - @Autowired - private ReactiveMongoOperations reactiveMongoOperations; - - @Resource - TestSupport testSupport; - - @Before - public void cleanup() { - reactiveMongoOperations.dropCollection(Book.class).block(); - reactiveMongoOperations.dropCollection(Author.class).block(); - } - - @Test - public void given_author_should_add_entry_in_database() { - // Given - AuthorRepresentation author = AuthorRepresentation.builder() - .firstname("isaac") - .lastname("asimov") - .birthdate(1920) - .id("isaac_asimov") - .build(); - - // When - final var authorRepresentationResponse = webTestClient - .post() - .uri(uri -> uri.path("/library/authors") - .queryParam("firstname", "isaac") - .queryParam("lastname", "asimov") - .queryParam("birthdate", "1920") - .build() - ).exchange() - .expectStatus() - .isOk() - .expectBody(AuthorRepresentation.class) - .returnResult() - .getResponseBody(); - - // Then - assertThat(authorRepresentationResponse).isEqualTo(author); - } - - @Test - public void given_book_should_add_entry_in_database() { - // Given - BookRepresentation book = BookRepresentation.builder() - .isbn("1234-5678-90") - .title("Fondation") - .releaseDate(1951) - .authorId("isaac_asimov") - .build(); - - // When - final var bookRepresentationResponse = webTestClient - .post() - .uri(uri -> uri.path("/library/books") + @Autowired private WebTestClient webTestClient; + @Resource private BookRepository bookRepository; + @Resource private AuthorRepository authorRepository; + @Resource TestSupport testSupport; + + @BeforeEach + public void load() { + testSupport.loadBooks().then(testSupport.loadAuthor()).block(); + } + + @AfterEach + public void clear() { + bookRepository.deleteAll().block(); + authorRepository.deleteAll().block(); + } + + @Test + public void given_author_should_add_entry_in_database() { + // Given + AuthorRepresentation author = + AuthorRepresentation.builder() + .firstname("Isaac") + .lastname("Asimov") + .birthdate(1920) + .id("isaac_asimov") + .build(); + + // When + List authorRepresentationResponse = + webTestClient + .get() + .uri( + uri -> + uri.path("/library/authors") + .queryParam("firstname", "Isaac") + .queryParam("lastname", "Asimov") + .queryParam("birthdate", 1920) + .build()) + .exchange() + .expectStatus() + .isOk() + .expectBodyList(AuthorRepresentation.class) + .returnResult() + .getResponseBody(); + + // Then + assertThat(authorRepresentationResponse).hasSize(1); + AuthorRepresentation authorRepresentation = authorRepresentationResponse.iterator().next(); + assertThat(authorRepresentation.getId()).isEqualTo(author.getId()); + assertThat(authorRepresentation.getFirstname()).isEqualTo(author.getFirstname()); + assertThat(authorRepresentation.getLastname()).isEqualTo(author.getLastname()); + assertThat(authorRepresentation.getBirthdate()).isEqualTo(author.getBirthdate()); + } + + @Test + public void given_book_should_add_entry_in_database() { + // Given + BookRepresentation book = + BookRepresentation.builder() + .isbn("1234-5678-90") + .title("Fondation") + .releaseDate(1951) + .authorId("isaac_asimov") + .build(); + + // When + final var bookRepresentationResponse = + webTestClient + .post() + .uri( + uri -> + uri.path("/library/books") .queryParam("isbn", "1234-5678-90") .queryParam("title", "Fondation") - .queryParam("releaseDateYear", "1951") + .queryParam("releaseDateYear", 1951) .queryParam("authorRefId", "isaac_asimov") - .build() - ).exchange() - .expectStatus() - .isOk() - .expectBody(BookRepresentation.class) - .returnResult() - .getResponseBody(); - - // Then - assertThat(bookRepresentationResponse).isEqualTo(book); - } - - @Test - public void given_books_in_database_should_return_all_books() { - // given - testSupport.loadBooks() - .then(testSupport.loadAuthor()).block(); - - // when - List books = webTestClient - .get() - .uri(uri -> uri.path("/library/books") - .build() - ).exchange() - .expectStatus() - .isOk() - .expectBodyList(BookRepresentation.class) - .returnResult() - .getResponseBody(); - - Assertions.assertThat(books).hasSize(14); - } - - @Test - public void given_books_in_database_should_return_books_based_on_criteria() { - // given - testSupport.loadBooks() - .then(testSupport.loadAuthor()).block(); - - // when - List books = webTestClient - .get() - .uri(uri -> uri.path("/library/books") + .build()) + .exchange() + .expectStatus() + .isOk() + .expectBody(BookRepresentation.class) + .returnResult() + .getResponseBody(); + + // Then + assertThat(bookRepresentationResponse.getIsbn()).isEqualTo(book.getIsbn()); + assertThat(bookRepresentationResponse.getTitle()).isEqualTo(book.getTitle()); + assertThat(bookRepresentationResponse.getReleaseDate()).isEqualTo(book.getReleaseDate()); + assertThat(bookRepresentationResponse.getAuthorId()).isEqualTo(book.getAuthorId()); + } + + @Test + public void given_books_in_database_should_return_all_books() { + // when + List books = + webTestClient + .get() + .uri(uri -> uri.path("/library/books").build()) + .exchange() + .expectStatus() + .isOk() + .expectBodyList(BookRepresentation.class) + .returnResult() + .getResponseBody(); + + Assertions.assertThat(books).hasSize(14); + } + + @Test + public void given_books_in_database_should_return_books_based_on_criteria() { + // when + List books = + webTestClient + .get() + .uri( + uri -> + uri.path("/library/books") .queryParam("authorRefId", "isaac_asimov") .queryParam("yearFrom", "1970") .queryParam("yearTo", "1990") - .build() - ).exchange() - .expectStatus() - .isOk() - .expectBodyList(BookRepresentation.class) - .returnResult() - .getResponseBody(); - - Assertions.assertThat(books).hasSize(5); - } + .build()) + .exchange() + .expectStatus() + .isOk() + .expectBodyList(BookRepresentation.class) + .returnResult() + .getResponseBody(); + + Assertions.assertThat(books).hasSize(5); + } } From 3bd833af49324a8b0aaa564c6b12bc42bbd7d7ad Mon Sep 17 00:00:00 2001 From: Chakib BENSARI Date: Tue, 5 Sep 2023 19:35:42 +0200 Subject: [PATCH 3/5] Feat (frontend): display books collection + add a single book --- technical-test-front/package.json | 3 +- technical-test-front/public/index.html | 33 +++--- technical-test-front/src/App.vue | 10 +- .../src/components/add-book/AddBook.vue | 56 +++++++++ .../src/components/home-page/LibraryHome.vue | 106 ++++++++++++++++++ .../src/components/single-book/SingleBook.vue | 19 ++++ technical-test-front/src/main.js | 14 ++- 7 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 technical-test-front/src/components/add-book/AddBook.vue create mode 100644 technical-test-front/src/components/home-page/LibraryHome.vue create mode 100644 technical-test-front/src/components/single-book/SingleBook.vue diff --git a/technical-test-front/package.json b/technical-test-front/package.json index 618ef3c..645a2dc 100644 --- a/technical-test-front/package.json +++ b/technical-test-front/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "core-js": "^3.8.3", - "vue": "^3.2.13" + "vue": "^3.2.13", + "vue-router": "^4.2.4" }, "devDependencies": { "@babel/core": "^7.12.16", diff --git a/technical-test-front/public/index.html b/technical-test-front/public/index.html index 3e5a139..1f030f3 100644 --- a/technical-test-front/public/index.html +++ b/technical-test-front/public/index.html @@ -1,17 +1,22 @@ - - - - - - <%= htmlWebpackPlugin.options.title %> - - - -
- - + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + +
+ + diff --git a/technical-test-front/src/App.vue b/technical-test-front/src/App.vue index 591a031..c4030d6 100644 --- a/technical-test-front/src/App.vue +++ b/technical-test-front/src/App.vue @@ -1,16 +1,14 @@ diff --git a/technical-test-front/src/components/add-book/AddBook.vue b/technical-test-front/src/components/add-book/AddBook.vue new file mode 100644 index 0000000..e879d6a --- /dev/null +++ b/technical-test-front/src/components/add-book/AddBook.vue @@ -0,0 +1,56 @@ + + + diff --git a/technical-test-front/src/components/home-page/LibraryHome.vue b/technical-test-front/src/components/home-page/LibraryHome.vue new file mode 100644 index 0000000..c43d724 --- /dev/null +++ b/technical-test-front/src/components/home-page/LibraryHome.vue @@ -0,0 +1,106 @@ + + + diff --git a/technical-test-front/src/components/single-book/SingleBook.vue b/technical-test-front/src/components/single-book/SingleBook.vue new file mode 100644 index 0000000..a1c5d95 --- /dev/null +++ b/technical-test-front/src/components/single-book/SingleBook.vue @@ -0,0 +1,19 @@ + + + diff --git a/technical-test-front/src/main.js b/technical-test-front/src/main.js index 01433bc..8d63d2d 100644 --- a/technical-test-front/src/main.js +++ b/technical-test-front/src/main.js @@ -1,4 +1,14 @@ -import { createApp } from 'vue' +import {createApp} from 'vue' import App from './App.vue' +import {createRouter, createWebHistory} from 'vue-router' +import LibraryHome from "@/components/home-page/LibraryHome"; +import NewBook from "@/components/add-book/AddBook"; -createApp(App).mount('#app') +const router = createRouter({ + history: createWebHistory(), + routes: [ + {path: '', component: LibraryHome}, + {path: '/new-book', component: NewBook} + ] +}) +createApp(App).use(router).mount('#app') From b51653aa36fd8c951e6af52b4abfed7a57e4e521 Mon Sep 17 00:00:00 2001 From: Chakib BENSARI Date: Wed, 20 Sep 2023 22:12:03 +0200 Subject: [PATCH 4/5] Containerize the application : Front + Back with local installed mongoDB --- technical-test-api/Dockerfile | 17 +++++++++++++++++ .../src/main/resources/application.yml | 7 ++++++- technical-test-front/.dockerignore | 5 +++++ technical-test-front/Dockerfile | 16 ++++++++++++++++ technical-test-front/src/App.vue | 2 +- .../src/components/add-book/AddBook.vue | 2 +- .../src/components/home-page/LibraryHome.vue | 6 +++--- 7 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 technical-test-api/Dockerfile create mode 100644 technical-test-front/.dockerignore create mode 100644 technical-test-front/Dockerfile diff --git a/technical-test-api/Dockerfile b/technical-test-api/Dockerfile new file mode 100644 index 0000000..37357ff --- /dev/null +++ b/technical-test-api/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.4-amazoncorretto-17 + +WORKDIR /app + +COPY pom.xml . + +RUN mvn clean install + +COPY . . + +ARG DEFAULT_PORT=80 + +ENV PORT $DEFAULT_PORT + +EXPOSE $PORT + +CMD ["mvn", "spring-boot:run"] diff --git a/technical-test-api/src/main/resources/application.yml b/technical-test-api/src/main/resources/application.yml index a62cf97..ee580e1 100644 --- a/technical-test-api/src/main/resources/application.yml +++ b/technical-test-api/src/main/resources/application.yml @@ -1,2 +1,7 @@ server: - port: 18080 + port: ${PORT} +spring: + data: + mongodb: + database: library + host: host.docker.internal diff --git a/technical-test-front/.dockerignore b/technical-test-front/.dockerignore new file mode 100644 index 0000000..bb1c1e2 --- /dev/null +++ b/technical-test-front/.dockerignore @@ -0,0 +1,5 @@ +node_modules + +Dockerfile +yarn.lock +.gitignore diff --git a/technical-test-front/Dockerfile b/technical-test-front/Dockerfile new file mode 100644 index 0000000..b0efd2a --- /dev/null +++ b/technical-test-front/Dockerfile @@ -0,0 +1,16 @@ +FROM node + +WORKDIR /app + +COPY package.json . + +RUN yarn install + +COPY . . + +ARG DEFAULT_PORT=8080 +ENV PORT $DEFAULT_PORT +EXPOSE $PORT + +CMD ["yarn", "serve"] + diff --git a/technical-test-front/src/App.vue b/technical-test-front/src/App.vue index c4030d6..38362ca 100644 --- a/technical-test-front/src/App.vue +++ b/technical-test-front/src/App.vue @@ -1,5 +1,5 @@ diff --git a/technical-test-front/src/components/add-book/AddBook.vue b/technical-test-front/src/components/add-book/AddBook.vue index e879d6a..1086caa 100644 --- a/technical-test-front/src/components/add-book/AddBook.vue +++ b/technical-test-front/src/components/add-book/AddBook.vue @@ -42,7 +42,7 @@ export default { methods: { addBook() { fetch( - `http://localhost:18080/library/books?isbn=${this.isbn}&title=${this.title}&releaseDateYear=${this.releasedDate}&authorRefId=${this.author}`, + `http://host.docker.internal:18080/library/books?isbn=${this.isbn}&title=${this.title}&releaseDateYear=${this.releasedDate}&authorRefId=${this.author}`, { method: 'POST', headers: { diff --git a/technical-test-front/src/components/home-page/LibraryHome.vue b/technical-test-front/src/components/home-page/LibraryHome.vue index c43d724..24f7e2e 100644 --- a/technical-test-front/src/components/home-page/LibraryHome.vue +++ b/technical-test-front/src/components/home-page/LibraryHome.vue @@ -60,7 +60,7 @@ export default { methods: { filter() { fetch( - `http://localhost:18080/library/books?yearFrom=${this.minDate}&yearTo=${this.maxDate}&authorRefId=${this.author}`) + `http://host.docker.internal:18080/library/books?yearFrom=${this.minDate}&yearTo=${this.maxDate}&authorRefId=${this.author}`) .then((response) => { if (response.ok) { return response.json(); @@ -76,7 +76,7 @@ export default { this.loadInitialBooks(); }, loadInitialBooks() { - fetch('http://localhost:18080/library/books', { + fetch('http://host.docker.internal:18080/library/books', { method: 'GET' }).then((response) => { if (response.ok) { @@ -87,7 +87,7 @@ export default { }); }, loadAuthors() { - fetch('http://localhost:18080/library/authors', { + fetch('http://host.docker.internal:18080/library/authors', { method: 'GET' }).then((response) => { if (response.ok) { From fc6c461487f5e4e3dcf7a9906bd0a30451c7fa26 Mon Sep 17 00:00:00 2001 From: Chakib BENSARI Date: Thu, 21 Sep 2023 00:54:04 +0200 Subject: [PATCH 5/5] Containerize the application : with network using docker container name as domain (not for the front) --- technical-test-api/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/technical-test-api/src/main/resources/application.yml b/technical-test-api/src/main/resources/application.yml index ee580e1..c897f01 100644 --- a/technical-test-api/src/main/resources/application.yml +++ b/technical-test-api/src/main/resources/application.yml @@ -4,4 +4,4 @@ spring: data: mongodb: database: library - host: host.docker.internal + host: technical-test-db