diff --git a/mylibrary/README.md b/mylibrary/README.md new file mode 100644 index 0000000..86612a6 --- /dev/null +++ b/mylibrary/README.md @@ -0,0 +1,78 @@ +# mylibrary — back-end Java (méthode BDD) + +Back-end du projet **2026-DEV-BUT3**. Reproduit, en Java pur, le cœur métier +de l'API consommée par le front React `my-library/`. + +## Méthode + +Conformément au cours `maintenanceApplicativeCours1.pdf` (Behavior-Driven +Development), pour chaque comportement on : + +1. Écrit le scénario attendu (en Gherkin / `.feature` ou en `@DisplayName` JUnit) +2. Écrit le test qui le vérifie +3. Implémente le code qui fait passer le test + +## Structure (calquée sur le module `customer` fourni par le prof) + +``` +src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/ +├─ customer/ Module fourni par l'enseignant (intact) +│ ├─ CustomerInfo.java record d'entrée +│ ├─ CustomerDTO.java DTO de sortie +│ ├─ entity/ Customer (objet métier, règles fidélité) +│ ├─ exception/ Exceptions métier +│ ├─ converter/ Mapping Info <-> entity <-> DTO +│ ├─ validator/ Règles de validation +│ ├─ repository/ Stockage en mémoire (List) +│ └─ usecase/ Cas d'usage (orchestration) +└─ book/ Module développé en miroir (notre travail) + └─ … (mêmes sous-paquets, même découpage) + +src/test/java/.../mylibrary/ +├─ customer/ Tests JUnit fournis par l'enseignant +├─ book/ Tests JUnit que nous avons écrits +└─ features/ + ├─ RunCucumberTest.java (du prof, intact) + ├─ client/CustomerSteps.java (du prof, intact) + ├─ book/BookSteps.java (notre travail) + └─ resources/features/ + ├─ client.feature (du prof, intact) + └─ book.feature (notre travail) +``` + +## Dépendances + +Strictement les mêmes que dans le template du prof : + +- JUnit 5 (jupiter-api, params, engine + platform-suite, platform-engine, platform-launcher) +- Mockito (core, junit-jupiter) +- Cucumber (cucumber-java, cucumber-junit-platform-engine) +- Lombok + +## Build & test + +Pré-requis : **JDK 21** + **Maven 3.9+** + +```bash +mvn -f mylibrary test +``` + +Cette commande exécute : + +- les tests unitaires JUnit (entity, validator, converter, repository, usecase, exceptions) +- les scénarios Cucumber (`client.feature` + `book.feature`) via `RunCucumberTest` + +## Périmètre couvert + +Conformément à la consigne (« reproduire le strict nécessaire pour démontrer +la maîtrise »), l'API se limite aux deux domaines présents dans le swagger +qui structurent l'app React : + +- **Catalogue** (`book`) : enregistrer, consulter par ISBN, lister tous les livres, + refuser ISBN dupliqué, refuser un livre invalide, gérer le stock. +- **Comptes clients** (`customer`) : enregistrer, consulter par téléphone, mettre + à jour, supprimer, ajouter / retirer des points de fidélité (module fourni). + +L'exposition HTTP réelle utilisée par les développeurs front est l'API du prof +(`mylibrary-0.0.1-SNAPSHOT.jar`) ; ce module sert à démontrer la maîtrise +de la **conception métier en BDD/TDD**. diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java new file mode 100644 index 0000000..d5b88e7 --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java @@ -0,0 +1,46 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.usecase; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDTO; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.converter.BookConverter; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookAlreadyExistsException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository; +import fr.iut_fbleau.but3.dev62.mylibrary.book.validator.BookValidator; +import java.util.List; +import java.util.Optional; + +public final class BookUseCase { + + private final BookRepository bookRepository; + + public BookUseCase(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + public long registerBook(BookInfo bookInfo) throws NotValidBookException, BookAlreadyExistsException { + BookValidator.validate(bookInfo); + if (bookRepository.existsByIsbn(bookInfo.isbn())) { + throw new BookAlreadyExistsException(bookInfo.isbn()); + } + Book toRegister = BookConverter.toDomain(bookInfo); + Book registered = bookRepository.save(toRegister); + return registered.getIsbn(); + } + + public BookDTO getBookByIsbn(long isbn) throws BookNotFoundException { + Optional optional = bookRepository.findByIsbn(isbn); + if (optional.isEmpty()) { + throw new BookNotFoundException(isbn); + } + return BookConverter.toDTO(optional.get()); + } + + public List getAllBooks() { + return bookRepository.findAll().stream() + .map(BookConverter::toDTO) + .toList(); + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java new file mode 100644 index 0000000..eba95e6 --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java @@ -0,0 +1,152 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.usecase; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDTO; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookAlreadyExistsException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BookUseCaseTest { + + @Mock + private BookRepository bookRepository; + + @InjectMocks + private BookUseCase bookUseCase; + + private BookInfo validInfo; + private Book validBook; + + @BeforeEach + void setUp() { + validInfo = new BookInfo( + 9780321125217L, + "DDD", + "Evans", + "Addison-Wesley", + LocalDate.of(2003, 8, 22), + 54.99, + 10, + List.of(Category.SCIENCE), + "desc", + "EN" + ); + validBook = Book.builder() + .isbn(validInfo.isbn()) + .title(validInfo.title()) + .author(validInfo.author()) + .publisher(validInfo.publisher()) + .publicationDate(validInfo.publicationDate()) + .price(validInfo.price()) + .quantity(validInfo.quantity()) + .categories(validInfo.categories()) + .description(validInfo.description()) + .language(validInfo.language()) + .build(); + } + + @Nested + @DisplayName("registerBook tests") + class RegisterTests { + + @Test + @DisplayName("Should register a new book and return its ISBN") + void testRegisterBook() throws NotValidBookException, BookAlreadyExistsException { + when(bookRepository.existsByIsbn(validInfo.isbn())).thenReturn(false); + when(bookRepository.save(any(Book.class))).thenReturn(validBook); + + long isbn = bookUseCase.registerBook(validInfo); + + assertEquals(validInfo.isbn(), isbn); + verify(bookRepository).existsByIsbn(validInfo.isbn()); + verify(bookRepository).save(any(Book.class)); + } + + @Test + @DisplayName("Should reject invalid book information without touching the repository") + void testRegisterInvalidBook() { + BookInfo invalid = new BookInfo(0L, "", "", "", null, -1, -1, null, "", ""); + assertThrows(NotValidBookException.class, () -> bookUseCase.registerBook(invalid)); + verifyNoInteractions(bookRepository); + } + + @Test + @DisplayName("Should reject duplicate ISBN") + void testRegisterDuplicate() { + when(bookRepository.existsByIsbn(validInfo.isbn())).thenReturn(true); + assertThrows(BookAlreadyExistsException.class, () -> bookUseCase.registerBook(validInfo)); + verify(bookRepository).existsByIsbn(validInfo.isbn()); + verify(bookRepository, never()).save(any(Book.class)); + } + } + + @Nested + @DisplayName("getBookByIsbn tests") + class GetByIdTests { + + @Test + @DisplayName("Should return the BookDTO when ISBN exists") + void testGetById() throws BookNotFoundException { + when(bookRepository.findByIsbn(validInfo.isbn())).thenReturn(Optional.of(validBook)); + + BookDTO dto = bookUseCase.getBookByIsbn(validInfo.isbn()); + + assertEquals(validInfo.isbn(), dto.getIsbn()); + assertEquals(validInfo.title(), dto.getTitle()); + } + + @Test + @DisplayName("Should throw when ISBN does not exist") + void testGetByIdNotFound() { + when(bookRepository.findByIsbn(99L)).thenReturn(Optional.empty()); + assertThrows(BookNotFoundException.class, () -> bookUseCase.getBookByIsbn(99L)); + } + } + + @Nested + @DisplayName("getAllBooks tests") + class GetAllBooksTests { + + @Test + @DisplayName("Should map every book returned by the repository to a DTO") + void testGetAllBooks() { + when(bookRepository.findAll()).thenReturn(List.of(validBook)); + + List result = bookUseCase.getAllBooks(); + + assertEquals(1, result.size()); + assertEquals(validBook.getIsbn(), result.getFirst().getIsbn()); + assertEquals(validBook.getTitle(), result.getFirst().getTitle()); + verify(bookRepository).findAll(); + } + + @Test + @DisplayName("Should return an empty list when no book is stored") + void testGetAllBooksWhenEmpty() { + when(bookRepository.findAll()).thenReturn(List.of()); + + List result = bookUseCase.getAllBooks(); + + assertTrue(result.isEmpty()); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/features/book/BookSteps.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/features/book/BookSteps.java new file mode 100644 index 0000000..5a9ba3c --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/features/book/BookSteps.java @@ -0,0 +1,106 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.features.book; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDTO; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookAlreadyExistsException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository; +import fr.iut_fbleau.but3.dev62.mylibrary.book.usecase.BookUseCase; +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.And; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +public class BookSteps { + + private final BookRepository bookRepository = new BookRepository(); + private final BookUseCase bookUseCase = new BookUseCase(bookRepository); + + private long lastRegisteredIsbn; + private BookDTO retrievedBook; + private List allBooks; + private Exception lastException; + + @Given("the catalog has the following books:") + public void theCatalogHasTheFollowingBooks(DataTable dataTable) throws NotValidBookException, BookAlreadyExistsException { + bookRepository.deleteAll(); + for (Map row : dataTable.asMaps(String.class, String.class)) { + bookUseCase.registerBook(toBookInfo(row)); + } + } + + @When("I register a new book with the following information:") + public void iRegisterANewBook(DataTable dataTable) throws NotValidBookException, BookAlreadyExistsException { + Map row = dataTable.asMaps(String.class, String.class).getFirst(); + lastRegisteredIsbn = bookUseCase.registerBook(toBookInfo(row)); + } + + @When("I try to register a new book with the following information:") + public void iTryToRegisterANewBook(DataTable dataTable) { + Map row = dataTable.asMaps(String.class, String.class).getFirst(); + lastException = assertThrows(Exception.class, () -> bookUseCase.registerBook(toBookInfo(row))); + } + + @When("I request the book with isbn {long}") + public void iRequestTheBook(long isbn) throws BookNotFoundException { + retrievedBook = bookUseCase.getBookByIsbn(isbn); + } + + @When("I list all books") + public void iListAllBooks() { + allBooks = bookUseCase.getAllBooks(); + } + + @Then("the book is created") + public void theBookIsCreated() { + assertNotNull(lastRegisteredIsbn); + } + + @And("the catalog now has {int} book(s)") + public void theCatalogNowHasNBooks(int expected) { + assertEquals(expected, bookRepository.findAll().size()); + } + + @Then("the registration fails with a {string}") + public void theRegistrationFailsWith(String exceptionSimpleName) { + assertNotNull(lastException); + assertEquals(exceptionSimpleName, lastException.getClass().getSimpleName()); + } + + @Then("I receive a book whose title is {string}") + public void iReceiveABookWhoseTitleIs(String title) { + assertNotNull(retrievedBook); + assertEquals(title, retrievedBook.getTitle()); + } + + @Then("I receive {int} book(s)") + public void iReceiveNBooks(int expected) { + assertNotNull(allBooks); + assertEquals(expected, allBooks.size()); + } + + private static BookInfo toBookInfo(Map row) { + return new BookInfo( + Long.parseLong(row.get("isbn")), + row.get("titre"), + row.get("auteur"), + row.get("editeur"), + LocalDate.parse(row.get("datePublication")), + Double.parseDouble(row.get("prix")), + Integer.parseInt(row.get("stock")), + List.of(Category.valueOf(row.get("categorie"))), + "", + row.get("langue") + ); + } +} diff --git a/mylibrary/src/test/resources/features/book.feature b/mylibrary/src/test/resources/features/book.feature new file mode 100644 index 0000000..a0fd950 --- /dev/null +++ b/mylibrary/src/test/resources/features/book.feature @@ -0,0 +1,40 @@ +# language: en + +Feature: Manage book catalog + + Scenario: Register a new book in the catalog + When I register a new book with the following information: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | DDD | Evans | AW | 2003-08-22 | 54.99 | 10 | SCIENCE | EN | + Then the book is created + And the catalog now has 1 book + + Scenario: Reject duplicate ISBN + Given the catalog has the following books: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | DDD | Evans | AW | 2003-08-22 | 54.99 | 10 | SCIENCE | EN | + When I try to register a new book with the following information: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | DDD copy | Evans | AW | 2003-08-22 | 54.99 | 10 | SCIENCE | EN | + Then the registration fails with a "BookAlreadyExistsException" + + Scenario: Reject invalid book information + When I try to register a new book with the following information: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | | Evans | AW | 2003-08-22 | 0 | -1 | SCIENCE | EN | + Then the registration fails with a "NotValidBookException" + + Scenario: Retrieve a book by its ISBN + Given the catalog has the following books: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | DDD | Evans | AW | 2003-08-22 | 54.99 | 10 | SCIENCE | EN | + When I request the book with isbn 9780321125217 + Then I receive a book whose title is "DDD" + + Scenario: List all books in the catalog + Given the catalog has the following books: + | isbn | titre | auteur | editeur | datePublication | prix | stock | categorie | langue | + | 9780321125217 | DDD | Evans | AW | 2003-08-22 | 54.99 | 10 | SCIENCE | EN | + | 9780132350884 | Clean | Martin | PH | 2008-08-01 | 30.00 | 5 | SCIENCE | EN | + When I list all books + Then I receive 2 books