diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java new file mode 100644 index 0000000..8e3a505 --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java @@ -0,0 +1,22 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category; +import java.time.LocalDate; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class BookDTO { + private final long isbn; + private final String title; + private final String author; + private final String publisher; + private final LocalDate publicationDate; + private final double price; + private final int quantity; + private final List categories; + private final String description; + private final String language; +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverter.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverter.java new file mode 100644 index 0000000..4dcefe2 --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverter.java @@ -0,0 +1,42 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.converter; + +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; + +public final class BookConverter { + + private BookConverter() { + + } + + public static Book toDomain(BookInfo book) { + return Book.builder() + .isbn(book.isbn()) + .title(book.title()) + .author(book.author()) + .publisher(book.publisher()) + .publicationDate(book.publicationDate()) + .price(book.price()) + .quantity(book.quantity()) + .categories(book.categories()) + .description(book.description()) + .language(book.language()) + .build(); + } + + public static BookDTO toDTO(Book book) { + return BookDTO.builder() + .isbn(book.getIsbn()) + .title(book.getTitle()) + .author(book.getAuthor()) + .publisher(book.getPublisher()) + .publicationDate(book.getPublicationDate()) + .price(book.getPrice()) + .quantity(book.getQuantity()) + .categories(book.getCategories()) + .description(book.getDescription()) + .language(book.getLanguage()) + .build(); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsException.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsException.java new file mode 100644 index 0000000..a21df9e --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsException.java @@ -0,0 +1,13 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; + +public class BookAlreadyExistsException extends Exception { + + public static final String A_BOOK_WITH_ISBN_ALREADY_EXISTS_MESSAGE = "A book with isbn {0} already exists"; + + public BookAlreadyExistsException(long isbn) { + // ISBN passe en String pour ne pas que MessageFormat ajoute le separateur de milliers + super(MessageFormat.format(A_BOOK_WITH_ISBN_ALREADY_EXISTS_MESSAGE, String.valueOf(isbn))); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java new file mode 100644 index 0000000..679f2aa --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java @@ -0,0 +1,13 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; + +public class BookNotFoundException extends Exception { + + public static final String THE_BOOK_WITH_ISBN_DOES_NOT_EXIST_MESSAGE = "The book with isbn {0} does not exist"; + + public BookNotFoundException(long isbn) { + // ISBN passe en String pour ne pas que MessageFormat ajoute le separateur de milliers + super(MessageFormat.format(THE_BOOK_WITH_ISBN_DOES_NOT_EXIST_MESSAGE, String.valueOf(isbn))); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityException.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityException.java new file mode 100644 index 0000000..9dd11ef --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityException.java @@ -0,0 +1,12 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; + +public class IllegalBookQuantityException extends Exception { + + public static final String CANNOT_REMOVE_STOCK = "Cannot remove {0} units from {1} units in stock"; + + public IllegalBookQuantityException(int needed, int actual) { + super(MessageFormat.format(CANNOT_REMOVE_STOCK, needed, actual)); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java new file mode 100644 index 0000000..6eab151 --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java @@ -0,0 +1,8 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +public class NotValidBookException extends Exception { + + public NotValidBookException(String message) { + super(message); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java new file mode 100644 index 0000000..c88289e --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java @@ -0,0 +1,42 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.repository; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public final class BookRepository { + + private final List books = new ArrayList<>(); + + public List findAll() { + return books; + } + + public void deleteAll() { + books.clear(); + } + + public Book save(Book book) { + Optional existing = findByIsbn(book.getIsbn()); + existing.ifPresent(books::remove); + books.add(book); + return book; + } + + public Optional findByIsbn(long isbn) { + return books.stream() + .filter(b -> b.getIsbn() == isbn) + .findFirst(); + } + + public boolean existsByIsbn(long isbn) { + return books.stream().anyMatch(b -> b.getIsbn() == isbn); + } + + public void delete(Book book) { + books.remove(book); + } +} diff --git a/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java new file mode 100644 index 0000000..b278fc5 --- /dev/null +++ b/mylibrary/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java @@ -0,0 +1,93 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.validator; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; + +public final class BookValidator { + + public static final int TITLE_MAX_LENGTH = 255; + + public static final String ISBN_MUST_BE_POSITIVE = "Isbn must be a positive number"; + public static final String TITLE_CANNOT_BE_BLANK = "Title cannot be blank"; + public static final String TITLE_TOO_LONG = "Title cannot exceed " + TITLE_MAX_LENGTH + " characters"; + public static final String AUTHOR_CANNOT_BE_BLANK = "Author cannot be blank"; + public static final String PUBLISHER_CANNOT_BE_BLANK = "Publisher cannot be blank"; + public static final String PUBLICATION_DATE_REQUIRED = "Publication date is required"; + public static final String PRICE_MUST_BE_POSITIVE = "Price must be strictly positive"; + public static final String QUANTITY_MUST_BE_POSITIVE_OR_ZERO = "Quantity must be >= 0"; + public static final String CATEGORIES_REQUIRED = "At least one category is required"; + public static final String LANGUAGE_CANNOT_BE_BLANK = "Language cannot be blank"; + + private BookValidator() { + + } + + public static void validate(BookInfo book) throws NotValidBookException { + validateIsbn(book); + validateTitle(book); + validateAuthor(book); + validatePublisher(book); + validatePublicationDate(book); + validatePrice(book); + validateQuantity(book); + validateCategories(book); + validateLanguage(book); + } + + private static void validateIsbn(BookInfo book) throws NotValidBookException { + if (book.isbn() <= 0) { + throw new NotValidBookException(ISBN_MUST_BE_POSITIVE); + } + } + + private static void validateTitle(BookInfo book) throws NotValidBookException { + if (book.title() == null || book.title().isBlank()) { + throw new NotValidBookException(TITLE_CANNOT_BE_BLANK); + } + if (book.title().length() > TITLE_MAX_LENGTH) { + throw new NotValidBookException(TITLE_TOO_LONG); + } + } + + private static void validateAuthor(BookInfo book) throws NotValidBookException { + if (book.author() == null || book.author().isBlank()) { + throw new NotValidBookException(AUTHOR_CANNOT_BE_BLANK); + } + } + + private static void validatePublisher(BookInfo book) throws NotValidBookException { + if (book.publisher() == null || book.publisher().isBlank()) { + throw new NotValidBookException(PUBLISHER_CANNOT_BE_BLANK); + } + } + + private static void validatePublicationDate(BookInfo book) throws NotValidBookException { + if (book.publicationDate() == null) { + throw new NotValidBookException(PUBLICATION_DATE_REQUIRED); + } + } + + private static void validatePrice(BookInfo book) throws NotValidBookException { + if (book.price() <= 0) { + throw new NotValidBookException(PRICE_MUST_BE_POSITIVE); + } + } + + private static void validateQuantity(BookInfo book) throws NotValidBookException { + if (book.quantity() < 0) { + throw new NotValidBookException(QUANTITY_MUST_BE_POSITIVE_OR_ZERO); + } + } + + private static void validateCategories(BookInfo book) throws NotValidBookException { + if (book.categories() == null || book.categories().isEmpty()) { + throw new NotValidBookException(CATEGORIES_REQUIRED); + } + } + + private static void validateLanguage(BookInfo book) throws NotValidBookException { + if (book.language() == null || book.language().isBlank()) { + throw new NotValidBookException(LANGUAGE_CANNOT_BE_BLANK); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java new file mode 100644 index 0000000..f4ee00c --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java @@ -0,0 +1,86 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.converter; + +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 java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BookConverterTest { + + @Nested + @DisplayName("toDomain() method tests") + class ToDomainTests { + + @Test + @DisplayName("Should convert BookInfo to Book entity preserving all fields") + void testToDomain() { + BookInfo info = new BookInfo( + 9780321125217L, + "DDD", + "Evans", + "Addison-Wesley", + LocalDate.of(2003, 8, 22), + 54.99, + 10, + List.of(Category.SCIENCE), + "desc", + "EN" + ); + + Book book = BookConverter.toDomain(info); + + assertEquals(info.isbn(), book.getIsbn()); + assertEquals(info.title(), book.getTitle()); + assertEquals(info.author(), book.getAuthor()); + assertEquals(info.publisher(), book.getPublisher()); + assertEquals(info.publicationDate(), book.getPublicationDate()); + assertEquals(info.price(), book.getPrice()); + assertEquals(info.quantity(), book.getQuantity()); + assertEquals(info.categories(), book.getCategories()); + assertEquals(info.description(), book.getDescription()); + assertEquals(info.language(), book.getLanguage()); + } + } + + @Nested + @DisplayName("toDTO() method tests") + class ToDTOTests { + + @Test + @DisplayName("Should convert Book entity to BookDTO preserving all fields") + void testToDTO() { + Book book = Book.builder() + .isbn(123L) + .title("T") + .author("A") + .publisher("P") + .publicationDate(LocalDate.of(2020, 1, 1)) + .price(9.99) + .quantity(5) + .categories(List.of(Category.FICTION)) + .description("d") + .language("FR") + .build(); + + BookDTO dto = BookConverter.toDTO(book); + + assertEquals(123L, dto.getIsbn()); + assertEquals("T", dto.getTitle()); + assertEquals("A", dto.getAuthor()); + assertEquals("P", dto.getPublisher()); + assertEquals(LocalDate.of(2020, 1, 1), dto.getPublicationDate()); + assertEquals(9.99, dto.getPrice()); + assertEquals(5, dto.getQuantity()); + assertEquals(List.of(Category.FICTION), dto.getCategories()); + assertEquals("d", dto.getDescription()); + assertEquals("FR", dto.getLanguage()); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java new file mode 100644 index 0000000..91b012d --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java @@ -0,0 +1,75 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.entity; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookQuantityException; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BookTest { + + @Test + @DisplayName("Builder should create a valid Book instance") + void testBookBuilder() { + long isbn = 9780321125217L; + Book book = Book.builder() + .isbn(isbn) + .title("Domain-Driven Design") + .author("Eric Evans") + .publisher("Addison-Wesley") + .publicationDate(LocalDate.of(2003, 8, 22)) + .price(54.99) + .quantity(10) + .categories(List.of(Category.SCIENCE, Category.REFERENCE)) + .description("Tackling complexity in the heart of software") + .language("EN") + .build(); + + assertEquals(isbn, book.getIsbn()); + assertEquals("Domain-Driven Design", book.getTitle()); + assertEquals("Eric Evans", book.getAuthor()); + assertEquals("Addison-Wesley", book.getPublisher()); + assertEquals(LocalDate.of(2003, 8, 22), book.getPublicationDate()); + assertEquals(54.99, book.getPrice()); + assertEquals(10, book.getQuantity()); + assertEquals(List.of(Category.SCIENCE, Category.REFERENCE), book.getCategories()); + assertEquals("EN", book.getLanguage()); + } + + @Nested + @DisplayName("Stock management") + class StockTests { + + @Test + @DisplayName("addStock should increase the quantity") + void testAddStock() { + Book book = Book.builder().quantity(5).build(); + book.addStock(7); + assertEquals(12, book.getQuantity()); + } + + @Test + @DisplayName("removeStock should decrease the quantity") + void testRemoveStock() throws IllegalBookQuantityException { + Book book = Book.builder().quantity(5).build(); + book.removeStock(3); + assertEquals(2, book.getQuantity()); + } + + @Test + @DisplayName("removeStock should throw when removing more than available") + void testRemoveTooMuchStock() { + Book book = Book.builder().quantity(5).build(); + IllegalBookQuantityException exception = assertThrows( + IllegalBookQuantityException.class, + () -> book.removeStock(10) + ); + assertEquals(5, book.getQuantity()); + assertTrue(exception.getMessage().contains("10")); + assertTrue(exception.getMessage().contains("5")); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsExceptionTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsExceptionTest.java new file mode 100644 index 0000000..f19a9fe --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookAlreadyExistsExceptionTest.java @@ -0,0 +1,26 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BookAlreadyExistsExceptionTest { + + @Test + @DisplayName("Exception message should contain the ISBN provided") + void testExceptionMessageContainsIsbn() { + long isbn = 9780321125217L; + + BookAlreadyExistsException exception = new BookAlreadyExistsException(isbn); + + assertTrue(exception.getMessage().contains(String.valueOf(isbn))); + } + + @Test + @DisplayName("Exception should expose its message constant") + void testConstantMessage() { + assertEquals("A book with isbn {0} already exists", + BookAlreadyExistsException.A_BOOK_WITH_ISBN_ALREADY_EXISTS_MESSAGE); + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java new file mode 100644 index 0000000..1a0cfe2 --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java @@ -0,0 +1,37 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BookNotFoundExceptionTest { + + @Test + @DisplayName("Exception message should contain the ISBN provided") + void testExceptionMessageContainsIsbn() { + long isbn = 9780321125217L; + + BookNotFoundException exception = new BookNotFoundException(isbn); + + assertTrue(exception.getMessage().contains(String.valueOf(isbn))); + } + + @Test + @DisplayName("Exception should expose its message constant") + void testConstantMessage() { + assertEquals("The book with isbn {0} does not exist", + BookNotFoundException.THE_BOOK_WITH_ISBN_DOES_NOT_EXIST_MESSAGE); + } + + @Test + @DisplayName("Exception should be properly thrown and caught") + void testExceptionCanBeThrownAndCaught() { + long isbn = 1L; + try { + throw new BookNotFoundException(isbn); + } catch (BookNotFoundException e) { + assertTrue(e.getMessage().contains("1")); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityExceptionTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityExceptionTest.java new file mode 100644 index 0000000..1aa191f --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookQuantityExceptionTest.java @@ -0,0 +1,33 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; + +class IllegalBookQuantityExceptionTest { + + @Test + @DisplayName("Exception message should contain the requested and actual quantities") + void testExceptionMessageContainsQuantities() { + IllegalBookQuantityException exception = new IllegalBookQuantityException(10, 3); + String expected = "Cannot remove 10 units from 3 units in stock"; + assertEquals(expected, exception.getMessage()); + } + + @ParameterizedTest + @CsvSource({ + "10, 3", + "100, 0", + "5, 4" + }) + @DisplayName("Message should be formatted using the constant template") + void testFormattedMessage(int needed, int actual) { + IllegalBookQuantityException exception = new IllegalBookQuantityException(needed, actual); + String expected = MessageFormat.format(IllegalBookQuantityException.CANNOT_REMOVE_STOCK, needed, actual); + assertEquals(expected, exception.getMessage()); + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java new file mode 100644 index 0000000..5f826b5 --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java @@ -0,0 +1,44 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class NotValidBookExceptionTest { + + @Test + @DisplayName("Exception should be created with the provided message") + void testExceptionCreation() { + String msg = "title cannot be blank"; + NotValidBookException exception = new NotValidBookException(msg); + assertEquals(msg, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = { + "Title cannot be blank", + "Author cannot be blank", + "Price must be strictly positive", + "At least one category is required" + }) + @DisplayName("Exception should propagate any validation message") + void testExceptionWithDifferentMessages(String message) { + NotValidBookException exception = new NotValidBookException(message); + assertEquals(message, exception.getMessage()); + } + + @Test + @DisplayName("Exception should be catchable as a general Exception") + void testExceptionInheritance() { + String msg = "invalid book"; + try { + throw new NotValidBookException(msg); + } catch (Exception e) { + assertEquals(NotValidBookException.class, e.getClass()); + assertEquals(msg, e.getMessage()); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java new file mode 100644 index 0000000..e2accfb --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java @@ -0,0 +1,149 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.repository; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category; +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 static org.junit.jupiter.api.Assertions.*; + +class BookRepositoryTest { + + private BookRepository repository; + private Book book1; + private Book book2; + + @BeforeEach + void setUp() { + repository = new BookRepository(); + + book1 = Book.builder() + .isbn(1111111111111L) + .title("Book One") + .author("Author A") + .publisher("Pub") + .publicationDate(LocalDate.of(2020, 1, 1)) + .price(10.0) + .quantity(2) + .categories(List.of(Category.FICTION)) + .language("EN") + .build(); + + book2 = Book.builder() + .isbn(2222222222222L) + .title("Book Two") + .author("Author B") + .publisher("Pub") + .publicationDate(LocalDate.of(2021, 1, 1)) + .price(20.0) + .quantity(0) + .categories(List.of(Category.HISTORY)) + .language("FR") + .build(); + } + + @Test + @DisplayName("New repository should be empty") + void testNewRepositoryIsEmpty() { + assertTrue(repository.findAll().isEmpty()); + } + + @Nested + @DisplayName("Save operations") + class SaveOperations { + + @Test + @DisplayName("Save should add a new book") + void testSave() { + Book saved = repository.save(book1); + assertEquals(1, repository.findAll().size()); + assertEquals(book1.getIsbn(), saved.getIsbn()); + } + + @Test + @DisplayName("Save should replace existing book with same ISBN (idempotency)") + void testSaveReplacesExisting() { + repository.save(book1); + Book updated = Book.builder() + .isbn(book1.getIsbn()) + .title("New title") + .author("Author A") + .publisher("Pub") + .publicationDate(LocalDate.of(2020, 1, 1)) + .price(10.0) + .quantity(99) + .categories(List.of(Category.FICTION)) + .language("EN") + .build(); + + repository.save(updated); + + assertEquals(1, repository.findAll().size()); + assertEquals(99, repository.findByIsbn(book1.getIsbn()).orElseThrow().getQuantity()); + assertEquals("New title", repository.findByIsbn(book1.getIsbn()).orElseThrow().getTitle()); + } + } + + @Nested + @DisplayName("Find operations") + class FindOperations { + + @BeforeEach + void seed() { + repository.save(book1); + repository.save(book2); + } + + @Test + @DisplayName("findByIsbn returns the matching book") + void testFindByIsbn() { + Optional found = repository.findByIsbn(book1.getIsbn()); + assertTrue(found.isPresent()); + assertEquals(book1.getTitle(), found.get().getTitle()); + } + + @Test + @DisplayName("findByIsbn returns empty when not found") + void testFindByIsbnNotFound() { + assertTrue(repository.findByIsbn(99L).isEmpty()); + } + + @Test + @DisplayName("existsByIsbn returns true / false consistently") + void testExistsByIsbn() { + assertTrue(repository.existsByIsbn(book1.getIsbn())); + assertFalse(repository.existsByIsbn(99L)); + } + } + + @Nested + @DisplayName("Delete operations") + class DeleteOperations { + + @BeforeEach + void seed() { + repository.save(book1); + repository.save(book2); + } + + @Test + @DisplayName("delete should remove the specified book") + void testDelete() { + repository.delete(book1); + assertEquals(1, repository.findAll().size()); + assertFalse(repository.existsByIsbn(book1.getIsbn())); + } + + @Test + @DisplayName("deleteAll should clear the repository") + void testDeleteAll() { + repository.deleteAll(); + assertTrue(repository.findAll().isEmpty()); + } + } +} diff --git a/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java new file mode 100644 index 0000000..0ec3938 --- /dev/null +++ b/mylibrary/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java @@ -0,0 +1,167 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.validator; + +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.NotValidBookException; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class BookValidatorTest { + + private BookInfo valid() { + return new BookInfo( + 9780321125217L, + "Domain-Driven Design", + "Eric Evans", + "Addison-Wesley", + LocalDate.of(2003, 8, 22), + 54.99, + 10, + List.of(Category.SCIENCE), + "Tackling complexity in the heart of software", + "EN" + ); + } + + @Test + @DisplayName("Should accept a valid BookInfo") + void testValidBook() { + assertDoesNotThrow(() -> BookValidator.validate(valid())); + } + + @Nested + @DisplayName("ISBN validation") + class IsbnTests { + + @ParameterizedTest + @ValueSource(longs = {0L, -1L, -9999L}) + @DisplayName("Should reject ISBN that is not strictly positive") + void testInvalidIsbn(long isbn) { + BookInfo info = new BookInfo(isbn, "T", "A", "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.ISBN_MUST_BE_POSITIVE, ex.getMessage()); + } + } + + @Nested + @DisplayName("Title validation") + class TitleTests { + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t", "\n"}) + @DisplayName("Should reject blank title") + void testBlankTitle(String title) { + BookInfo info = new BookInfo(1L, title, "A", "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.TITLE_CANNOT_BE_BLANK, ex.getMessage()); + } + + @Test + @DisplayName("Should reject null title") + void testNullTitle() { + BookInfo info = new BookInfo(1L, null, "A", "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.TITLE_CANNOT_BE_BLANK, ex.getMessage()); + } + + @Test + @DisplayName("Should reject title longer than 255 characters") + void testTitleTooLong() { + String longTitle = "a".repeat(256); + BookInfo info = new BookInfo(1L, longTitle, "A", "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.TITLE_TOO_LONG, ex.getMessage()); + } + } + + @Nested + @DisplayName("Author / Publisher validation") + class AuthorPublisherTests { + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t"}) + @DisplayName("Should reject blank author") + void testBlankAuthor(String author) { + BookInfo info = new BookInfo(1L, "T", author, "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.AUTHOR_CANNOT_BE_BLANK, ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t"}) + @DisplayName("Should reject blank publisher") + void testBlankPublisher(String publisher) { + BookInfo info = new BookInfo(1L, "T", "A", publisher, LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.PUBLISHER_CANNOT_BE_BLANK, ex.getMessage()); + } + } + + @Nested + @DisplayName("Date / Price / Quantity validation") + class NumericTests { + + @Test + @DisplayName("Should reject null publication date") + void testNullDate() { + BookInfo info = new BookInfo(1L, "T", "A", "P", null, 1.0, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.PUBLICATION_DATE_REQUIRED, ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(doubles = {0.0, -0.01, -10.0}) + @DisplayName("Should reject non-positive price") + void testNonPositivePrice(double price) { + BookInfo info = new BookInfo(1L, "T", "A", "P", LocalDate.now(), price, 0, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.PRICE_MUST_BE_POSITIVE, ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(ints = {-1, -100}) + @DisplayName("Should reject negative quantity") + void testNegativeQuantity(int quantity) { + BookInfo info = new BookInfo(1L, "T", "A", "P", LocalDate.now(), 1.0, quantity, List.of(Category.SCIENCE), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.QUANTITY_MUST_BE_POSITIVE_OR_ZERO, ex.getMessage()); + } + } + + @Nested + @DisplayName("Categories / Language validation") + class MetadataTests { + + @Test + @DisplayName("Should reject empty categories") + void testEmptyCategories() { + BookInfo info = new BookInfo(1L, "T", "A", "P", LocalDate.now(), 1.0, 0, List.of(), "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.CATEGORIES_REQUIRED, ex.getMessage()); + } + + @Test + @DisplayName("Should reject null categories") + void testNullCategories() { + BookInfo info = new BookInfo(1L, "T", "A", "P", LocalDate.now(), 1.0, 0, null, "", "EN"); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.CATEGORIES_REQUIRED, ex.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t"}) + @DisplayName("Should reject blank language") + void testBlankLanguage(String language) { + BookInfo info = new BookInfo(1L, "T", "A", "P", LocalDate.now(), 1.0, 0, List.of(Category.SCIENCE), "", language); + NotValidBookException ex = assertThrows(NotValidBookException.class, () -> BookValidator.validate(info)); + assertEquals(BookValidator.LANGUAGE_CANNOT_BE_BLANK, ex.getMessage()); + } + } +}