diff --git a/interface-contracts.json b/interface-contracts.json index 98f304b..c5375c0 100644 --- a/interface-contracts.json +++ b/interface-contracts.json @@ -14,6 +14,7 @@ "langue": "string" }, "output": { + "_comment": "c'est ce qui affiche quand tu crée avec la usecase (regarde register de customer)", "isbn": "string(13)" } }, diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java new file mode 100644 index 0000000..d36ac70 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDTO.java @@ -0,0 +1,23 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book; + + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.ArrayList; + +@Getter +@Builder +public class BookDTO { + private String isbn; + private String title; + private String author; + private String editor; + private LocalDate date; + private double price; + private Integer stock; + private ArrayList categories = new ArrayList<>(); + private String description ; + private String language; +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDetails.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDetails.java new file mode 100644 index 0000000..15aa3fc --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookDetails.java @@ -0,0 +1,17 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book; + + +import lombok.Builder; +import lombok.Getter; + +import java.util.ArrayList; + +@Builder +@Getter +public class BookDetails { + private ArrayList categories = new ArrayList<>(); + private String description ; + private String language; + + +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookInfo.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookInfo.java new file mode 100644 index 0000000..6bdbcb2 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookInfo.java @@ -0,0 +1,6 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book; + +import java.time.LocalDate; + +public record BookInfo(String isbn, String title, String author, String editor, LocalDate date) { +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookSalesInfo.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookSalesInfo.java new file mode 100644 index 0000000..5d0b828 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/BookSalesInfo.java @@ -0,0 +1,11 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class BookSalesInfo { + private double price; + private Integer stock; +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverter.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverter.java new file mode 100644 index 0000000..42713a2 --- /dev/null +++ b/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.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; + +public final class BookConverter { + private BookConverter(){} + + public static Book ToDomain(BookInfo bookinfo, BookSalesInfo booksalesinfo, BookDetails bookdetails){ + return Book.builder() + .isbn(bookinfo.isbn()) + .title(bookinfo.title()) + .author(bookinfo.author()) + .editor(bookinfo.editor()) + .date(bookinfo.date()) + .price(booksalesinfo.getPrice()) + .stock(booksalesinfo.getStock()) + .categories(bookdetails.getCategories()) + .description(bookdetails.getDescription()) + .language(bookdetails.getLanguage()) + .build(); + } + + public static BookDTO ToDTO(Book book){ + return BookDTO.builder() + .isbn(book.getIsbn()) + .title(book.getTitle()) + .author(book.getAuthor()) + .editor(book.getEditor()) + .date(book.getDate()) + .price(book.getPrice()) + .stock(book.getStock()) + .categories(book.getCategories()) + .description(book.getDescription()) + .language(book.getLanguage()) + .build(); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/Book.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/Book.java new file mode 100644 index 0000000..500c09d --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/Book.java @@ -0,0 +1,35 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.entity; + + +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookStockException; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDate; +import java.util.ArrayList; + +@Getter +@Builder +public class Book { + private String isbn; + private String title; + private String author; + private String editor; + private LocalDate date; + private double price; + private Integer stock; + private ArrayList categories = new ArrayList<>(); + private String description ; + private String language; + + public void addStock(Integer copyToAdd){ + this.stock += copyToAdd; + } + + public void removeStock(Integer copyToRemomve) throws IllegalBookStockException { + if (copyToRemomve > this.stock){ + throw new IllegalBookStockException(copyToRemomve, this.stock); + } + this.stock -= copyToRemomve; + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java new file mode 100644 index 0000000..51136c0 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundException.java @@ -0,0 +1,11 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; + +public class BookNotFoundException extends RuntimeException { + public static final String THE_BOOK_WITH_ID_DOES_NOT_EXIST_MESSAGE = "The book with isbn {0} does not exist"; + + public BookNotFoundException(String isbn) { + super(MessageFormat.format(THE_BOOK_WITH_ID_DOES_NOT_EXIST_MESSAGE, isbn)); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockException.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockException.java new file mode 100644 index 0000000..6d7db41 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockException.java @@ -0,0 +1,13 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +import java.text.MessageFormat; + +public class IllegalBookStockException extends Exception { + public static final String CANNOT_REMOVE_COPY = "Cannot remove {0} copy from {1} copy"; + + public IllegalBookStockException(Integer toremove, Integer actual ) { + + super(MessageFormat.format(CANNOT_REMOVE_COPY, toremove, + actual)); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java new file mode 100644 index 0000000..bec0172 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookException.java @@ -0,0 +1,7 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.exception; + +public class NotValidBookException extends RuntimeException { + public NotValidBookException(String message) { + super(message); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java new file mode 100644 index 0000000..f7790b5 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepository.java @@ -0,0 +1,43 @@ +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; + +public class BookRepository { + + private final List books = new ArrayList<>(); + + public List findAll() { + return books; + } + + public void deleteAll() { + books.clear(); + } + + public Book save(Book newBook) { + Optional optionalBookWithSameIsbn = this.findByIsbn(newBook.getIsbn()); + optionalBookWithSameIsbn.ifPresent(books::remove); + this.books.add(newBook); + return newBook; + } + + public Optional findByIsbn(String isbn) { + return this.books.stream() + .filter(book -> book.getIsbn().equals(isbn)) + .findFirst(); + } + + public boolean existsByIsbn(String isbn) { + return this.books.stream() + .anyMatch(book -> book.getIsbn().equals(isbn)); + } + + public void delete(Book book) { + + this.books.remove(book); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java new file mode 100644 index 0000000..b59de47 --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCase.java @@ -0,0 +1,90 @@ +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.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +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.BookNotFoundException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookStockException; +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.Optional; + +public class BookUseCase { + + private final BookRepository bookRepository; + + public BookUseCase(BookRepository bookRepository) { + + this.bookRepository = bookRepository; + } + + public String registerBook(BookInfo newbook, BookSalesInfo newbooksalesinfo, BookDetails newbookdetails) throws NotValidBookException { + BookValidator.validate(newbook); + BookValidator.validate(newbooksalesinfo); + BookValidator.validate(newbookdetails); + Book bookToRegister = BookConverter.ToDomain(newbook, newbooksalesinfo, newbookdetails); + Book bookToRegistered = bookRepository.save(bookToRegister); + return bookToRegistered.getIsbn(); + } + + public Optional findBookByIsbn(String isbn) { + Optional optionalBook = bookRepository.findByIsbn(isbn); + return optionalBook.map(BookConverter::ToDTO); + } + + public BookDTO updateBook(String isbn, BookInfo bookinfo, BookSalesInfo booksalesinfo, BookDetails bookdetails) + throws BookNotFoundException, NotValidBookException { + BookValidator.validate(bookinfo); + BookValidator.validate(booksalesinfo); + BookValidator.validate(bookdetails); + Book bookByIsbn = getBookIfDoesNotExistThrowBookNotFoundException(isbn); + Book book = Book.builder() + .isbn(isbn) + .title(bookinfo.title()) + .author(bookinfo.author()) + .editor(bookinfo.editor()) + .date(bookByIsbn.getDate()) + .price(booksalesinfo.getPrice()) + .stock(booksalesinfo.getStock()) + .categories(bookdetails.getCategories()) + .description(bookdetails.getDescription()) + .language(bookByIsbn.getLanguage()) + .build(); + Book updatedBook = bookRepository.save(book); + return BookConverter.ToDTO(updatedBook); + } + + public void deleteBook(String isbn) throws BookNotFoundException { + Book bookToDelete = getBookIfDoesNotExistThrowBookNotFoundException(isbn); + this.bookRepository.delete(bookToDelete); + } + + public int addStockCopies(String isbn, int stockCopiesToAdd) throws BookNotFoundException { + Book bookToAddStockCopies = getBookIfDoesNotExistThrowBookNotFoundException(isbn); + bookToAddStockCopies.addStock(stockCopiesToAdd); + bookRepository.save(bookToAddStockCopies); + return bookToAddStockCopies.getStock(); + } + + public int subtractStockCopies(String isbn, int stockCopiesToRemove) + throws BookNotFoundException, IllegalBookStockException { + Book bookToSubtractStockCopies = getBookIfDoesNotExistThrowBookNotFoundException(isbn); + bookToSubtractStockCopies.removeStock(stockCopiesToRemove); + bookRepository.save(bookToSubtractStockCopies); + return bookToSubtractStockCopies.getStock(); + } + + private Book getBookIfDoesNotExistThrowBookNotFoundException(String isbn) + throws BookNotFoundException { + Optional optionalBookByIsbn = bookRepository.findByIsbn(isbn); + if (optionalBookByIsbn.isEmpty()) { + throw new BookNotFoundException(isbn); + } + return optionalBookByIsbn.get(); + } +} diff --git a/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java new file mode 100644 index 0000000..66626af --- /dev/null +++ b/src/main/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidator.java @@ -0,0 +1,110 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.validator; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; + +import java.time.LocalDate; +import java.util.ArrayList; + +public class BookValidator { + + public static final String ISBN_IS_NOT_VALID = "Isbn is not valid"; + public static final String TITLE_CANNOT_BE_BLANK = "Title cannot be blank"; + public static final String AUTHOR_CANNOT_BE_BLANK = "Author cannot be blank"; + public static final String EDITOR_CANNOT_BE_BLANK = "Editor cannot be blank"; + public static final String DATE_IS_NOT_VALID = "Date is not valid"; + public static final String PRICE_IS_NOT_VALID = "Price is not valid"; + public static final String STOCK_IS_NOT_VALID = "Stock is not valid"; + public static final String CATEGORIES_IS_NOT_VALID = "Categories is not valid"; + public static final String LANGUAGE_CANNOT_BE_BLANK = "Language cannot be blank"; + + private BookValidator() { + } + + public static void validate(BookInfo newBook) throws NotValidBookException { + validateISBN(newBook); + validateTitle(newBook); + validateAuthor(newBook); + validateEditor(newBook); + validateDate(newBook); + } + + public static void validate(BookSalesInfo newBook) throws NotValidBookException { + validatePrice(newBook); + validateStock(newBook); + } + + public static void validate(BookDetails newBook) throws NotValidBookException { + validateCategories(newBook); + validateLanguage(newBook); + } + + private static void validateISBN(BookInfo newBook) throws NotValidBookException { + if (newBook.isbn().isBlank()) { + throw new NotValidBookException(ISBN_IS_NOT_VALID); + } else if (newBook.isbn().length() != 13) { + throw new NotValidBookException(ISBN_IS_NOT_VALID); + } + } + + private static void validateTitle(BookInfo newBook) throws NotValidBookException { + if (newBook.title().isBlank()) { + throw new NotValidBookException(TITLE_CANNOT_BE_BLANK); + } + } + + private static void validateAuthor(BookInfo newBook) throws NotValidBookException { + if (newBook.author().isBlank()) { + throw new NotValidBookException(AUTHOR_CANNOT_BE_BLANK); + } + } + + private static void validateEditor(BookInfo newBook) throws NotValidBookException { + if (newBook.editor().isBlank()) { + throw new NotValidBookException(EDITOR_CANNOT_BE_BLANK); + } + } + + private static void validateDate(BookInfo newBook) throws NotValidBookException { + if (newBook.date().isAfter(LocalDate.now())) { + throw new NotValidBookException(DATE_IS_NOT_VALID); + } + } + + private static void validatePrice(BookSalesInfo newBook) throws NotValidBookException { + if (newBook.getPrice() <= 0) { + throw new NotValidBookException(PRICE_IS_NOT_VALID); + } + } + + private static void validateStock(BookSalesInfo newBook) throws NotValidBookException { + if (newBook.getStock() < 0) { + throw new NotValidBookException(STOCK_IS_NOT_VALID); + } + } + + private static void validateCategories(BookDetails newBook) throws NotValidBookException { + if (newBook.getCategories().isEmpty()) { + throw new NotValidBookException(CATEGORIES_IS_NOT_VALID); + }else if (CategoriesNotBlank(newBook.getCategories())) { + throw new NotValidBookException(CATEGORIES_IS_NOT_VALID); + } + } + + private static boolean CategoriesNotBlank(ArrayList categories){ + for (String categorie : categories) { + if (categorie.isBlank()) { + return true; + } + } + return false; + } + + private static void validateLanguage(BookDetails newBook) throws NotValidBookException { + if (newBook.getLanguage().isBlank()) { + throw new NotValidBookException(LANGUAGE_CANNOT_BE_BLANK); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java new file mode 100644 index 0000000..071d520 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/converter/BookConverterTest.java @@ -0,0 +1,118 @@ +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.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("BookConverterTest Unit Tests") +public class BookConverterTest { + + @Nested + @DisplayName("toDomain() method tests") + class ToDomainTests { + + @Test + void ShouldConvertBookToDomain(){ + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookinfo = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", "Kioon", date); + BookSalesInfo booksalesinfo = BookSalesInfo.builder() + .price(12) + .stock(10) + .build(); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + BookDetails bookdetails = BookDetails.builder() + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + Book result = BookConverter.ToDomain(bookinfo, booksalesinfo, bookdetails); + + assertEquals(bookinfo.isbn(), result.getIsbn()); + assertEquals(bookinfo.title(), result.getTitle()); + assertEquals(bookinfo.author(), result.getAuthor()); + assertEquals(bookinfo.editor(), result.getEditor()); + assertEquals(bookinfo.date(), result.getDate()); + assertEquals(booksalesinfo.getPrice(), result.getPrice()); + assertEquals(booksalesinfo.getStock(), result.getStock()); + assertEquals(bookdetails.getCategories(), result.getCategories()); + assertEquals(bookdetails.getDescription(), result.getDescription()); + assertEquals(bookdetails.getLanguage(), result.getLanguage()); + } + } + + @Nested + @DisplayName("toDTO() method tests") + class ToDTOTests { + + @Test + void ShouldConvertBookToDTO() { + LocalDate date = LocalDate.of(2026, 3, 24); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + Book book = Book.builder() + .isbn("1234567891012") + .title("La vie de Maxime") + .author("Marvin Aubert") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + BookDTO result = BookConverter.ToDTO(book); + + assertEquals(book.getIsbn(), result.getIsbn()); + assertEquals(book.getTitle(), result.getTitle()); + assertEquals(book.getAuthor(), result.getAuthor()); + assertEquals(book.getEditor(), result.getEditor()); + assertEquals(book.getDate(), result.getDate()); + assertEquals(book.getPrice(), result.getPrice()); + assertEquals(book.getStock(), result.getStock()); + assertEquals(book.getCategories(), result.getCategories()); + assertEquals(book.getDescription(), result.getDescription()); + assertEquals(book.getLanguage(), result.getLanguage()); + } + } + + @Test + @DisplayName("Should preserve empty string values during conversion") + void shouldPreserveEmptyStrings() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookinfo = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", "Kioon", date); + BookSalesInfo booksalesinfo = BookSalesInfo.builder() + .price(12) + .stock(10) + .build(); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + BookDetails bookdetails = BookDetails.builder() + .categories(categories) + .description("") + .language("Français") + .build(); + + Book book = BookConverter.ToDomain(bookinfo, booksalesinfo, bookdetails); + BookDTO result = BookConverter.ToDTO(book); + + assertEquals("", result.getDescription()); + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java new file mode 100644 index 0000000..1cf6303 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/entity/BookTest.java @@ -0,0 +1,168 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.entity; + + +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookStockException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookTest { + + @Test + @DisplayName("Builder should create a valid Book instance") + void testBookBuilder() { + String isbn = "1234567891012"; + String title = "La vie de Maxime"; + String author = "Marvin Aubert"; + String editor = "Kioon"; + LocalDate date = LocalDate.of(2026, 3, 24); + double price = 12.99; + Integer stock = 50; + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + String description = "C'était un brave partit trop tôt"; + String language = "Français"; + Book book = Book.builder() + .isbn(isbn) + .title(title) + .author(author) + .editor(editor) + .date(date) + .price(price) + .stock(stock) + .categories(categories) + .description(description) + .language(language) + .build(); + + assertEquals(isbn, book.getIsbn()); + assertEquals(title, book.getTitle()); + assertEquals(author, book.getAuthor()); + assertEquals(editor, book.getEditor()); + assertEquals(date, book.getDate()); + assertEquals(price, book.getPrice()); + assertEquals(stock, book.getStock()); + assertEquals(categories, book.getCategories()); + assertEquals(description, book.getDescription()); + assertEquals(language, book.getLanguage()); + } + + @Nested + @DisplayName("Stock Tests") + class StockTests { + + @Test + @DisplayName("addCopy should correctly increment stocks") + void testAddCopy() { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToAdd = 5; + Integer copyExpected = 10; + + book.addStock(copyToAdd); + + assertEquals(copyExpected, book.getStock()); + } + + @Test + @DisplayName("addCopy should correctly increment zero points correctly") + void testAddZeroToCopy() { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToAdd = 0; + Integer copyExpected = 5; + + book.addStock(copyToAdd); + + assertEquals(copyExpected, book.getStock()); + } + + @Test + @DisplayName("removeLoyaltyPoints should correctly decrement loyalty points") + void testRemoveCopy() throws IllegalBookStockException { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToRemove = 2; + Integer copyExpected = 3; + + book.removeStock(copyToRemove); + + assertEquals(copyExpected, book.getStock()); + } + + @Test + @DisplayName("removeLoyaltyPoints should correctly decrement loyalty points") + void testRemoveZeroToCopy() throws IllegalBookStockException { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToRemove = 0; + Integer copyExpected = 5; + + book.removeStock(copyToRemove); + + assertEquals(copyExpected, book.getStock()); + } + + @Test + @DisplayName("removeLoyaltyPoints should correctly decrement loyalty points") + void testRemoveAllToCopy() throws IllegalBookStockException { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToRemove = 5; + Integer copyExpected = 0; + + book.removeStock(copyToRemove); + + assertEquals(copyExpected, book.getStock()); + } + + @Test + @DisplayName("removeSTock should throw exception when trying to remove more copy than available") + void testRemoveTooManyCopy() { + Book book = Book.builder() + .stock(50) + .build(); + int copyToRemove = 75; + + IllegalBookStockException exception = assertThrows( + IllegalBookStockException.class, + () -> book.removeStock(copyToRemove) + ); + + assertEquals(50, book.getStock()); + + assertTrue(exception.getMessage().contains(String.valueOf(copyToRemove))); + assertTrue(exception.getMessage().contains(String.valueOf(book.getStock()))); + } + } + + @Nested + @DisplayName("Price Tests") + class PriceTests { + @Test + @DisplayName("addCopy should correctly increment stocks") + void testAddCopy() { + Book book =Book.builder() + .stock(5) + .build(); + Integer copyToAdd = 5; + Integer copyExpected = 10; + + book.addStock(copyToAdd); + + assertEquals(copyExpected, book.getStock()); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java new file mode 100644 index 0000000..bbcc134 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/BookNotFoundExceptionTest.java @@ -0,0 +1,48 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookNotFoundExceptionTest { + + @Test + @DisplayName("Exception message should contain the isbn provided") + void testExceptionMessageContainsIsbn() { + String isbn = "1234567891012"; + + BookNotFoundException exception = new BookNotFoundException(isbn); + + String expectedMessage = String.format("The book with isbn %s does not exist", isbn); + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + @DisplayName("Exception should use the correct constant message format") + void testExceptionUsesConstantMessageFormat() { + String isbn = "1234567891012"; + + BookNotFoundException exception = new BookNotFoundException(isbn); + + String expectedFormatWithPlaceholder = "The book with isbn {0} does not exist"; + assertEquals(BookNotFoundException.THE_BOOK_WITH_ID_DOES_NOT_EXIST_MESSAGE, + expectedFormatWithPlaceholder); + assertTrue(exception.getMessage().contains(isbn.toString())); + } + + @Test + @DisplayName("Exception should be properly thrown and caught") + void testExceptionCanBeThrownAndCaught() { + String isbn = "1234567891012"; + + try { + throw new BookNotFoundException(isbn); + } catch (BookNotFoundException e) { + String expectedMessage = String.format("The book with isbn %s does not exist", isbn); + assertEquals(expectedMessage, e.getMessage()); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockExceptionTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockExceptionTest.java new file mode 100644 index 0000000..9c4b18f --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/IllegalBookStockExceptionTest.java @@ -0,0 +1,70 @@ +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.CsvSource; + +import java.text.MessageFormat; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class IllegalBookStockExceptionTest { + + @Test + @DisplayName("Exception message should contain the needed and actual stock") + void testExceptionMessageContainsStock() { + int neededStock = 100; + int actualStock = 50; + + IllegalBookStockException exception = new IllegalBookStockException(neededStock, actualStock); + + String expectedMessage = "Cannot remove 100 copy from 50 copy"; + assertEquals(expectedMessage, exception.getMessage()); + } + + @ParameterizedTest + @CsvSource({ + "100, 50", + "75, 25", + "200, 150", + "1000, 750" + }) + @DisplayName("Exception message should be formatted correctly for different stock values") + void testExceptionMessageForDifferentStockValues(int neededStock, int actualStock) { + IllegalBookStockException exception = new IllegalBookStockException(neededStock, actualStock); + + String expectedMessage = MessageFormat.format(IllegalBookStockException.CANNOT_REMOVE_COPY, neededStock, actualStock); + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + @DisplayName("Exception should use the correct constant message format") + void testExceptionUsesConstantMessageFormat() { + int neededStock = 100; + int actualStock = 50; + + IllegalBookStockException exception = new IllegalBookStockException(neededStock, actualStock); + + String expectedFormatWithPlaceholder = "Cannot remove {0} copy from {1} copy"; + assertEquals(IllegalBookStockException.CANNOT_REMOVE_COPY, + expectedFormatWithPlaceholder); + assertTrue(exception.getMessage().contains(String.valueOf(neededStock))); + assertTrue(exception.getMessage().contains(String.valueOf(actualStock))); + } + + @Test + @DisplayName("Exception should be properly thrown and caught") + void testExceptionCanBeThrownAndCaught() { + int neededStock = 100; + int actualStock = 50; + + try { + throw new IllegalBookStockException(neededStock, actualStock); + } catch (IllegalBookStockException e) { + String expectedMessage = String.format("Cannot remove %d copy from %d copy", neededStock, actualStock); + assertEquals(expectedMessage, e.getMessage()); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java new file mode 100644 index 0000000..d6d6050 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/exception/NotValidBookExceptionTest.java @@ -0,0 +1,61 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class NotValidBookExceptionTest { + + @Test + @DisplayName("Exception should be created with the provided message") + void testExceptionCreation() { + String errorMessage = "Book data is not valid"; + + NotValidBookException exception = new NotValidBookException(errorMessage); + + assertEquals(errorMessage, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = { + "Isbn is required", + "Title cannot be empty", + "Date format is invalid", + "Price must be above 0" + }) + @DisplayName("Exception should handle different validation messages") + void testExceptionWithDifferentMessages(String errorMessage) { + NotValidBookException exception = new NotValidBookException(errorMessage); + + assertEquals(errorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Exception should be properly thrown and caught") + void testExceptionCanBeThrownAndCaught() { + String errorMessage = "Required field is missing"; + + Exception exception = assertThrows(NotValidBookException.class, () -> { + throw new NotValidBookException(errorMessage); + }); + + assertEquals(errorMessage, exception.getMessage()); + } + + @Test + @DisplayName("Exception should be catchable as a general Exception") + void testExceptionInheritance() { + String errorMessage = "Invalid book data"; + + try { + throw new NotValidBookException(errorMessage); + } catch (Exception e) { + assertEquals(NotValidBookException.class, e.getClass()); + assertEquals(errorMessage, e.getMessage()); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java new file mode 100644 index 0000000..a6c5669 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/repository/BookRepositoryTest.java @@ -0,0 +1,255 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.repository; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +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 java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookRepositoryTest { + + private BookRepository repository; + private Book book1; + private Book book2; + + @BeforeEach + void setUp() { + repository = new BookRepository(); + + LocalDate date = LocalDate.of(2026, 3, 24); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + + book1 = Book.builder() + .isbn("1234567891012") + .title("La vie de Maxime") + .author("Marvin Aubert") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + book2 = Book.builder() + .isbn("1234567891015") + .title("La vie de Marvin") + .author("Maxime Lebreton") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + } + + @Test + @DisplayName("New repository should be empty") + void testNewRepositoryIsEmpty() { + List books = repository.findAll(); + + assertTrue(books.isEmpty()); + assertEquals(0, books.size()); + } + + @Nested + @DisplayName("Save operations") + class SaveOperations { + + @Test + @DisplayName("Save should add a new book") + void testSaveNewBook() { + Book savedBook = repository.save(book1); + + assertEquals(1, repository.findAll().size()); + assertEquals(book1.getIsbn(), savedBook.getIsbn()); + assertEquals(book1.getTitle(), savedBook.getTitle()); + } + + @Test + @DisplayName("Save should update existing book with same isbn") + void testSaveUpdatesExistingBook() { + repository.save(book1); + + String isbn = "1234567891012"; + LocalDate date = LocalDate.of(2026, 3, 24); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + + Book updatedBook = Book.builder() + .isbn(isbn) + .title("La vie de Maxime") + .author("Updated") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt beaucoup trop tôt") + .language("Français") + .build(); + + Book savedBook = repository.save(updatedBook); + + assertEquals(1, repository.findAll().size()); + assertEquals(isbn, savedBook.getIsbn()); + assertEquals("La vie de Maxime", savedBook.getTitle()); + assertEquals("Updated", savedBook.getAuthor()); + assertEquals("Kioon", savedBook.getEditor()); + assertEquals(date, savedBook.getDate()); + assertEquals(12.99, savedBook.getPrice()); + assertEquals(50, savedBook.getStock()); + assertEquals(categories, savedBook.getCategories()); + assertEquals("C'était un brave partit trop tôt beaucoup trop tôt", savedBook.getDescription()); + assertEquals("Français", savedBook.getLanguage()); + } + + @Test + @DisplayName("Save multiple books should add all of them") + void testSaveMultipleBooks() { + repository.save(book1); + repository.save(book2); + + List books = repository.findAll(); + + assertEquals(2, books.size()); + assertTrue(books.contains(book1)); + assertTrue(books.contains(book2)); + } + } + + @Nested + @DisplayName("Find operations") + class FindOperations { + + @BeforeEach + void setUpBooks() { + repository.save(book1); + repository.save(book2); + } + + @Test + @DisplayName("FindAll should return all books") + void testFindAll() { + List books = repository.findAll(); + + assertEquals(2, books.size()); + assertTrue(books.contains(book1)); + assertTrue(books.contains(book2)); + } + + @Test + @DisplayName("FindByIsbn should return book with matching isbn") + void testFindByIsbn() { + Optional foundBook = repository.findByIsbn(book1.getIsbn()); + + assertTrue(foundBook.isPresent()); + assertEquals(book1.getTitle(), foundBook.get().getTitle()); + assertEquals(book1.getAuthor(), foundBook.get().getAuthor()); + } + + @Test + @DisplayName("FindByIsbn should return empty Optional when isbn doesn't exist") + void testFindByIsbnNotFound() { + String nonExistedisbn = "1515265522652"; + + Optional foundBook = repository.findByIsbn(nonExistedisbn); + + assertTrue(foundBook.isEmpty()); + } + + @Test + @DisplayName("ExistsByIsbn should return true when isbn exists") + void testExistsByIsbnExists() { + boolean exists = repository.existsByIsbn(book1.getIsbn()); + + assertTrue(exists); + } + + @Test + @DisplayName("ExistsByIsbn should return false when isbn doesn't exist") + void testExistsByIsbnNotExists() { + String nonExistedisbn = "1515265522652"; + + boolean exists = repository.existsByIsbn(nonExistedisbn); + + assertFalse(exists); + } + } + + @Nested + @DisplayName("Delete operations") + class DeleteOperations { + + @BeforeEach + void setUpBooks() { + repository.save(book1); + repository.save(book2); + } + + @Test + @DisplayName("Delete should remove the specified book") + void testDelete() { + repository.delete(book1); + + List books = repository.findAll(); + + assertEquals(1, books.size()); + assertFalse(books.contains(book1)); + assertTrue(books.contains(book2)); + } + + @Test + @DisplayName("DeleteAll should remove all books") + void testDeleteAll() { + repository.deleteAll(); + + List books = repository.findAll(); + + assertTrue(books.isEmpty()); + assertEquals(0, books.size()); + } + + @Test + @DisplayName("Delete should not throw exception when book doesn't exist") + void testDeleteNonExistentBook() { + LocalDate date = LocalDate.of(2026, 3, 24); + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + Book nonExistentBook = Book.builder() + .isbn("1515466461319") + .title("La vie de Patrick") + .author("Updated") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + assertDoesNotThrow(() -> repository.delete(nonExistentBook)); + + assertEquals(2, repository.findAll().size()); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java new file mode 100644 index 0000000..eda3b8c --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/usecase/BookUseCaseTest.java @@ -0,0 +1,356 @@ +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.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Book; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.BookNotFoundExceptionTest; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.IllegalBookStockException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; +import fr.iut_fbleau.but3.dev62.mylibrary.book.repository.BookRepository; +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 java.time.LocalDate; +import java.util.ArrayList; +import java.util.Optional; + +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 static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +public class BookUseCaseTest { + + @Mock + private BookRepository bookRepository; + + @InjectMocks + private BookUseCase bookUseCase; + + private String bookIsbn; + private ArrayList categories; + private LocalDate date; + private Book testBook; + private BookInfo validBookInfo; + private BookDetails validBookDetails; + private BookSalesInfo validBookSalesInfo; + + @BeforeEach + void setUp() { + bookIsbn = "1234567891012"; + date = LocalDate.of(2026, 3, 24); + categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + + testBook = Book.builder() + .isbn(bookIsbn) + .title("La vie de Maxime") + .author("Marvin AUbert") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + validBookInfo = new BookInfo(bookIsbn,"La vie de Maxime", "Marvin AUbert", "Kioon", date); + validBookSalesInfo = BookSalesInfo.builder() + .price(12.99) + .stock(50) + .build(); + validBookDetails = BookDetails.builder() + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + } + + @Nested + @DisplayName("Register Book tests") + class RegisterBookTests { + + @Test + @DisplayName("Should register book when valid data is provided") + void testRegisterBookWithValidData() throws NotValidBookException { + when(bookRepository.save(any(Book.class))).thenReturn(testBook); + + String registeredIsbn = bookUseCase.registerBook(validBookInfo, validBookSalesInfo, validBookDetails); + + assertNotNull(registeredIsbn); + assertEquals(bookIsbn, registeredIsbn); + verify(bookRepository, times(1)).save(any(Book.class)); + } + + @Test + @DisplayName("Should throw exception when book data is not valid") + void testRegisterBookWithInvalidData() { + BookInfo invalidBookInfo = new BookInfo(bookIsbn,"", "", "", date); + BookSalesInfo invalidUpdateSalesInfo = BookSalesInfo.builder() + .price(0) + .stock(-3) + .build(); + BookDetails invalidUpdateDetails = BookDetails.builder() + .categories(categories) + .description("") + .language("") + .build(); + assertThrows(NotValidBookException.class, + () -> bookUseCase.registerBook(invalidBookInfo, invalidUpdateSalesInfo, invalidUpdateDetails)); + + verify(bookRepository, never()).save(any(Book.class)); + } + } + + @Nested + @DisplayName("Find book tests") + class FindBookTests { + + @Test + @DisplayName("Should return book when isbn exists") + void testFindBookByIsbn() { + when(bookRepository.findByIsbn("1234567891012")).thenReturn(Optional.of(testBook)); + + Optional foundBook = bookUseCase.findBookByIsbn("1234567891012"); + + assertTrue(foundBook.isPresent()); + assertEquals(testBook.getIsbn(), foundBook.get().getIsbn()); + assertEquals(testBook.getTitle(), foundBook.get().getTitle()); + verify(bookRepository, times(1)).findByIsbn("1234567891012"); + } + + @Test + @DisplayName("Should return empty Optional when isbn doesn't exist") + void testFindBookByIsbnNotFound() { + when(bookRepository.findByIsbn("1656546262516")).thenReturn(Optional.empty()); + + Optional foundBook = bookUseCase.findBookByIsbn("1656546262516"); + + assertTrue(foundBook.isEmpty()); + verify(bookRepository, times(1)).findByIsbn("1656546262516"); + } + } + + @Nested + @DisplayName("Update book tests") + class UpdateBookTests { + + @Test + @DisplayName("Should update book when valid data is provided") + void testUpdateBookWithValidData() throws BookNotFoundException, NotValidBookException { + when(bookRepository.findByIsbn(bookIsbn)).thenReturn(Optional.of(testBook)); + + Book updatedBook = Book.builder() + .isbn(bookIsbn) + .title("La vie de Maxime") + .author("Updated") + .editor("Kioon") + .date(date) + .price(12.99) + .stock(50) + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + when(bookRepository.save(any(Book.class))).thenReturn(updatedBook); + + BookInfo updateInfo = new BookInfo(bookIsbn, "La vie de Maxime", "Updated", "Kioon", date); + BookSalesInfo updateSalesInfo = BookSalesInfo.builder() + .price(12.99) + .stock(50) + .build(); + BookDetails updateDetails = BookDetails.builder() + .categories(categories) + .description("C'était un brave partit trop tôt beaucoup trop tôt") + .language("Français") + .build(); + BookDTO result = bookUseCase.updateBook(bookIsbn, updateInfo, updateSalesInfo, updateDetails); + + assertNotNull(result); + assertEquals(bookIsbn, result.getIsbn()); + assertEquals("La vie de Maxime", result.getTitle()); + assertEquals("Updated", result.getAuthor()); + assertEquals("Kioon", result.getEditor()); + assertEquals(date, result.getDate()); + assertEquals(12.99, result.getPrice()); + assertEquals(50, result.getStock()); + assertEquals(categories, result.getCategories()); + assertEquals("C'était un brave partit trop tôt", result.getDescription()); + assertEquals("Français", result.getLanguage()); + verify(bookRepository, times(1)).findByIsbn(bookIsbn); + verify(bookRepository, times(1)).save(any(Book.class)); + } + + @Test + @DisplayName("Should throw exception when book isbn doesn't exist") + void testUpdateBookNotFound() { + String nonExistentIsbn = "1656546262516"; + when(bookRepository.findByIsbn(nonExistentIsbn)).thenReturn(Optional.empty()); + + BookInfo updateInfo = new BookInfo(nonExistentIsbn, "La vie de Maxime", "Updated", "Kioon", date); + BookSalesInfo updateSalesInfo = BookSalesInfo.builder() + .price(12.99) + .stock(50) + .build(); + BookDetails updateDetails = BookDetails.builder() + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + assertThrows(BookNotFoundException.class, + () -> bookUseCase.updateBook(nonExistentIsbn, updateInfo, updateSalesInfo, updateDetails)); + + verify(bookRepository, times(1)).findByIsbn(nonExistentIsbn); + verify(bookRepository, never()).save(any(Book.class)); + } + + @Test + @DisplayName("Should throw exception when update data is not valid") + void testUpdateBookWithInvalidData() { + BookInfo invalidUpdateInfo = new BookInfo(bookIsbn,"", "", "", date); + BookSalesInfo invalidUpdateSalesInfo = BookSalesInfo.builder() + .price(0) + .stock(-3) + .build(); + BookDetails invalidUpdateDetails = BookDetails.builder() + .categories(categories) + .description("") + .language("") + .build(); + + assertThrows(NotValidBookException.class, + () -> bookUseCase.updateBook(bookIsbn, invalidUpdateInfo, invalidUpdateSalesInfo, invalidUpdateDetails)); + + verify(bookRepository, never()).findByIsbn(any(String.class)); + verify(bookRepository, never()).save(any(Book.class)); + } + } + + @Nested + @DisplayName("Delete book tests") + class DeleteBookTests { + + @Test + @DisplayName("Should delete book when isbn exists") + void testDeleteBook() throws BookNotFoundException { + when(bookRepository.findByIsbn(bookIsbn)).thenReturn(Optional.of(testBook)); + doNothing().when(bookRepository).delete(testBook); + + bookUseCase.deleteBook(bookIsbn); + + verify(bookRepository, times(1)).findByIsbn(bookIsbn); + verify(bookRepository, times(1)).delete(testBook); + } + + @Test + @DisplayName("Should throw exception when book isbn doesn't exist") + void testDeleteBookNotFound() { + String nonExistentIsbn = "1656546262516"; + when(bookRepository.findByIsbn(nonExistentIsbn)).thenReturn(Optional.empty()); + + assertThrows(BookNotFoundException.class, + () -> bookUseCase.deleteBook(nonExistentIsbn)); + + verify(bookRepository, times(1)).findByIsbn(nonExistentIsbn); + verify(bookRepository, never()).delete(any(Book.class)); + } + } + + @Nested + @DisplayName("Stock copies tests") + class StockCopiesTests { + + @Test + @DisplayName("Should add stock copies to book") + void testAddStockCopies() throws BookNotFoundException { + when(bookRepository.findByIsbn(bookIsbn)).thenReturn(Optional.of(testBook)); + when(bookRepository.save(testBook)).thenReturn(testBook); + + int initialCopies = testBook.getStock(); + int copiesToAdd = 50; + int expectedCopies = initialCopies + copiesToAdd; + + int newCopies = bookUseCase.addStockCopies(bookIsbn, copiesToAdd); + + assertEquals(expectedCopies, newCopies); + assertEquals(expectedCopies, testBook.getStock()); + verify(bookRepository, times(1)).findByIsbn(bookIsbn); + verify(bookRepository, times(1)).save(testBook); + } + + @Test + @DisplayName("Should throw exception when adding copies to non-existent book") + void testAddStockCopiesToNonExistentBook() { + String nonExistentIsbn = "1656546262516"; + when(bookRepository.findByIsbn(nonExistentIsbn)).thenReturn(Optional.empty()); + + assertThrows(BookNotFoundException.class, + () -> bookUseCase.addStockCopies(nonExistentIsbn, 50)); + + verify(bookRepository, times(1)).findByIsbn(nonExistentIsbn); + verify(bookRepository, never()).save(any(Book.class)); + } + + @Test + @DisplayName("Should subtract stock copies from book") + void testSubtractStockCopies() throws BookNotFoundException, IllegalBookStockException { + when(bookRepository.findByIsbn(bookIsbn)).thenReturn(Optional.of(testBook)); + when(bookRepository.save(testBook)).thenReturn(testBook); + + int initialCopies = testBook.getStock(); + int copiesToRemove = 30; + int expectedCopies = initialCopies - copiesToRemove; + + int newCopies = bookUseCase.subtractStockCopies(bookIsbn, copiesToRemove); + + assertEquals(expectedCopies, newCopies); + assertEquals(expectedCopies, testBook.getStock()); + verify(bookRepository, times(1)).findByIsbn(bookIsbn); + verify(bookRepository, times(1)).save(testBook); + } + + @Test + @DisplayName("Should throw exception when trying to remove more copies than available") + void testSubtractTooManyStockCopies() { + when(bookRepository.findByIsbn(bookIsbn)).thenReturn(Optional.of(testBook)); + + int copiesToRemove = 200; + + assertThrows(IllegalBookStockException.class, + () -> bookUseCase.subtractStockCopies(bookIsbn, copiesToRemove)); + + verify(bookRepository, times(1)).findByIsbn(bookIsbn); + verify(bookRepository, never()).save(any(Book.class)); + } + + @Test + @DisplayName("Should throw exception when subtracting copies from non-existent book") + void testSubtractStockCopiesFromNonExistentBook() { + String nonExistentIsbn = "1656546262516"; + when(bookRepository.findByIsbn(nonExistentIsbn)).thenReturn(Optional.empty()); + + assertThrows(BookNotFoundException.class, + () -> bookUseCase.subtractStockCopies(nonExistentIsbn, 50)); + + verify(bookRepository, times(1)).findByIsbn(nonExistentIsbn); + verify(bookRepository, never()).save(any(Book.class)); + } + } +} diff --git a/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java new file mode 100644 index 0000000..73dc509 --- /dev/null +++ b/src/test/java/fr/iut_fbleau/but3/dev62/mylibrary/book/validator/BookValidatorTest.java @@ -0,0 +1,379 @@ +package fr.iut_fbleau.but3.dev62.mylibrary.book.validator; + +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookDetails; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.BookSalesInfo; +import fr.iut_fbleau.but3.dev62.mylibrary.book.exception.NotValidBookException; +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.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class BookValidatorTest { + + @Test + @DisplayName("Should validate book with valid data") + void testValidateValidBook() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo validBook = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", "Kioon", date); + + assertDoesNotThrow(() -> BookValidator.validate(validBook)); + } + + @Test + @DisplayName("Should validate book details with valid data") + void testValidateValidBookDetails() { + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + BookDetails validBookDetails = BookDetails.builder() + .categories(categories) + .description("C'était un brave partit trop tôt") + .language("Français") + .build(); + + assertDoesNotThrow(() -> BookValidator.validate(validBookDetails)); + } + + @Test + @DisplayName("Should validate book sales informations with valid data") + void testValidateValidBookSalesInfo() { + BookSalesInfo validBookSalesInfo = BookSalesInfo.builder() + .price(15) + .stock(10) + .build(); + + assertDoesNotThrow(() -> BookValidator.validate(validBookSalesInfo)); + } + + @Nested + @DisplayName("ISBN validation tests") + class ISBNValidationTests { + + @Test + @DisplayName("Should throw exception when isbn is blank") + void testValidateBlankISBN() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithBlankISBN = new BookInfo("", "La vie de Maxime", "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithBlankISBN) + ); + + assertEquals(BookValidator.ISBN_IS_NOT_VALID, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\t", "\n"}) + @DisplayName("Should throw exception when isbn contains only whitespace") + void testValidateWhitespaceISBN(String whitespace) { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithWhitespaceISBN = new BookInfo(whitespace, "La vie de Maxime", "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceISBN) + ); + + assertEquals(BookValidator.ISBN_IS_NOT_VALID, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {"00000", "0000000000", "0000000000000000"}) + @DisplayName("Should throw exception when isbn contains only thriteen character") + void testValidateThirteenCharacterISBN(String number) { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithNotExactCharacterISBN = new BookInfo(number, "La vie de Maxime", "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithNotExactCharacterISBN) + ); + + assertEquals(BookValidator.ISBN_IS_NOT_VALID, exception.getMessage()); + } + } + + @Nested + @DisplayName("Title validation tests") + class TitleValidationTests { + + @Test + @DisplayName("Should throw exception when title is blank") + void testValidateBlankTitle() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithBlankTitle = new BookInfo("0000000000000","", "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithBlankTitle) + ); + + assertEquals(BookValidator.TITLE_CANNOT_BE_BLANK, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\t", "\n"}) + @DisplayName("Should throw exception when title contains only whitespace") + void testValidateWhitespaceFirstName(String whitespace) { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithWhitespaceTitle = new BookInfo("0000000000000",whitespace, "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceTitle) + ); + + assertEquals(BookValidator.TITLE_CANNOT_BE_BLANK, exception.getMessage()); + } + } + + @Nested + @DisplayName("author validation tests") + class AuthorValidationTests { + + @Test + @DisplayName("Should throw exception when author is blank") + void testValidateBlankAuthor() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithBlankAuthor = new BookInfo("0000000000000","La vie de Maxime", "", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithBlankAuthor) + ); + + assertEquals(BookValidator.AUTHOR_CANNOT_BE_BLANK, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\t", "\n"}) + @DisplayName("Should throw exception when author contains only whitespace") + void testValidateWhitespaceAuthor(String whitespace) { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithWhitespaceAuthor = new BookInfo("0000000000000","La vie de Maxime", whitespace, "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceAuthor) + ); + + assertEquals(BookValidator.AUTHOR_CANNOT_BE_BLANK, exception.getMessage()); + } + } + + @Nested + @DisplayName("editor validation tests") + class EditorValidationTests { + + @Test + @DisplayName("Should throw exception when editor is blank") + void testValidateBlankEditor() { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithBlankEditor = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", "", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithBlankEditor) + ); + + assertEquals(BookValidator.EDITOR_CANNOT_BE_BLANK, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\t", "\n"}) + @DisplayName("Should throw exception when editor contains only whitespace") + void testValidateWhitespaceEditor(String whitespace) { + LocalDate date = LocalDate.of(2026, 3, 24); + BookInfo bookWithWhitespaceEditor = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", whitespace, date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceEditor) + ); + + assertEquals(BookValidator.EDITOR_CANNOT_BE_BLANK, exception.getMessage()); + } + } + + @Nested + @DisplayName("date validation tests") + class DateValidationTests { + + @Test + @DisplayName("Should throw exception when date is after the actual date") + void testValidateFuturDate() { + LocalDate date = LocalDate.of(2026, 5, 24); + BookInfo bookWithFuturDate = new BookInfo("0000000000000","La vie de Maxime", "Marvin Aubert", "Kioon", date); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithFuturDate) + ); + + assertEquals(BookValidator.DATE_IS_NOT_VALID, exception.getMessage()); + } + } + + @Nested + @DisplayName("price validation tests") + class PriceValidationTests { + + @ParameterizedTest + @ValueSource(doubles = {-3, -15, 0}) + @DisplayName("Should throw exception when price is negative or equal to zero") + void testValidateNegativePrice(double invalidprice) { + BookSalesInfo bookWithNegativePrice = BookSalesInfo.builder() + .price(invalidprice) + .stock(10) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithNegativePrice) + ); + + assertEquals(BookValidator.PRICE_IS_NOT_VALID, exception.getMessage()); + } + } + + @Nested + @DisplayName("stock validation tests") + class StockValidationTests { + + @Test + @DisplayName("Should throw exception when stock is negative") + void testValidateNegativeSTock() { + BookSalesInfo bookWithNegativeStock = BookSalesInfo.builder() + .price(3) + .stock(-3) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithNegativeStock) + ); + + assertEquals(BookValidator.STOCK_IS_NOT_VALID, exception.getMessage()); + } + } + + @Nested + @DisplayName("categories validation tests") + class CategoriesValidationTests { + + @Test + @DisplayName("Should throw exception when categories is empty") + void testValidateBlankCategories() { + ArrayList categories = new ArrayList<>(); + String description = "C'était un brave partit trop tôt"; + String language = "Français"; + BookDetails bookWithEmptyCategories = BookDetails.builder() + .categories(categories) + .description(description) + .language(language) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithEmptyCategories) + ); + + assertEquals(BookValidator.CATEGORIES_IS_NOT_VALID, exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("provideLists") + @DisplayName("Should throw exception when categories contains an whitespace") + void testValidateWhitespaceCategories(ArrayList invalidlist) { + String description = "C'était un brave partit trop tôt"; + String language = "Français"; + BookDetails bookWithWhitespaceCategories = BookDetails.builder() + .categories(invalidlist) + .description(description) + .language(language) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceCategories) + ); + + assertEquals(BookValidator.CATEGORIES_IS_NOT_VALID, exception.getMessage()); + } + + static Stream> provideLists() { + return Stream.of( + new ArrayList(java.util.List.of("")), + new ArrayList(java.util.List.of(" ")), + new ArrayList(java.util.List.of(" ")), + new ArrayList(java.util.List.of("\t")), + new ArrayList(java.util.List.of("\n")), + new ArrayList(java.util.List.of("A", "", "C")) + ); + } + } + + @Nested + @DisplayName("language validation tests") + class LanguageValidationTests { + + @Test + @DisplayName("Should throw exception when language is blank") + void testValidateNegativeLanguage() { + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + String description = "C'était un brave partit trop tôt"; + String language = ""; + + BookDetails bookWithBlankLanguage = BookDetails.builder() + .categories(categories) + .description(description) + .language(language) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithBlankLanguage) + ); + + assertEquals(BookValidator.LANGUAGE_CANNOT_BE_BLANK, exception.getMessage()); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", "\t", "\n"}) + @DisplayName("Should throw exception when language contains only whitespace") + void testValidateWhitespaceLanguage(String whitespace) { + ArrayList categories = new ArrayList<>(); + categories.add("Thriller"); + categories.add("Biographie"); + String description = "C'était un brave partit trop tôt"; + + BookDetails bookWithWhitespaceLanguage = BookDetails.builder() + .categories(categories) + .description(description) + .language(whitespace) + .build(); + + NotValidBookException exception = assertThrows( + NotValidBookException.class, + () -> BookValidator.validate(bookWithWhitespaceLanguage) + ); + + assertEquals(BookValidator.LANGUAGE_CANNOT_BE_BLANK, exception.getMessage()); + } + } +}