diff --git a/src/test/java/com/libraryman_api/LibrarymanApiApplicationTests.java b/src/it/java/com/libraryman_api/LibrarymanApiApplicationIntegrationTest.java similarity index 79% rename from src/test/java/com/libraryman_api/LibrarymanApiApplicationTests.java rename to src/it/java/com/libraryman_api/LibrarymanApiApplicationIntegrationTest.java index e160f31..4b2197a 100644 --- a/src/test/java/com/libraryman_api/LibrarymanApiApplicationTests.java +++ b/src/it/java/com/libraryman_api/LibrarymanApiApplicationIntegrationTest.java @@ -4,7 +4,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class LibrarymanApiApplicationTests { +class LibrarymanApiApplicationIntegrationTest { @Test void contextLoads() { diff --git a/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java index d9dcc01..2f3dcec 100644 --- a/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java +++ b/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationHelper.java @@ -16,8 +16,11 @@ public class JwtAuthenticationHelper { private static final long JWT_TOKEN_VALIDITY = 60 * 60; - @Value("${jwt.secretKey}") - private String secret; + private final String secret; + + public JwtAuthenticationHelper(@Value("${jwt.secretKey}") String secret) { + this.secret = secret; + } public String getUsernameFromToken(String token) { String username = getClaimsFromToken(token).getSubject(); diff --git a/src/test/java/com/libraryman_api/TestUtil.java b/src/test/java/com/libraryman_api/TestUtil.java new file mode 100644 index 0000000..417c93f --- /dev/null +++ b/src/test/java/com/libraryman_api/TestUtil.java @@ -0,0 +1,158 @@ +package com.libraryman_api; + +import com.libraryman_api.book.Book; +import com.libraryman_api.book.BookDto; +import com.libraryman_api.borrowing.Borrowings; +import com.libraryman_api.borrowing.BorrowingsDto; +import com.libraryman_api.fine.Fine; +import com.libraryman_api.member.Members; +import com.libraryman_api.member.Role; +import com.libraryman_api.member.dto.MembersDto; +import com.libraryman_api.member.dto.UpdateMembersDto; +import com.libraryman_api.member.dto.UpdatePasswordDto; +import com.libraryman_api.newsletter.NewsletterSubscriber; +import com.libraryman_api.security.model.LoginRequest; +import org.springframework.data.domain.*; + +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Random; + +public class TestUtil { + private static final Random randomNumberUtils = new Random(); + + public static int getRandomInt() { + return randomNumberUtils.nextInt(100, 1000); + } + + public static Book getBook() { + Book book = new Book(); + book.setBookId(Constant.BOOK_ID); + book.setTitle(Constant.BOOK_TITLE); + book.setAuthor("Sarah Maas"); + book.setIsbn("978-0-7475-3269-9"); + book.setPublisher("Penguin Random House"); + book.setPublishedYear(2025); + book.setGenre("Fiction"); + book.setCopiesAvailable(5); + return book; + } + + public static BookDto getBookDto() { + BookDto inputDto = new BookDto(); + inputDto.setBookId(Constant.BOOK_ID); + inputDto.setTitle(Constant.BOOK_TITLE); + inputDto.setAuthor("Sarah Maas"); + inputDto.setIsbn("978-0-7475-3269-9"); + inputDto.setPublisher("Penguin Random House"); + inputDto.setPublishedYear(2025); + inputDto.setGenre("Fiction"); + inputDto.setCopiesAvailable(5); + return inputDto; + } + + public static BorrowingsDto getBorrowingsDto() { + BorrowingsDto borrowingsDto = new BorrowingsDto(); + borrowingsDto.setBorrowingId(1); + borrowingsDto.setBook(getBookDto()); + borrowingsDto.setFine(null); + borrowingsDto.setMember(getMembersDto()); + borrowingsDto.setBorrowDate(new Date()); + borrowingsDto.setReturnDate(adjustDays(new Date(), 7)); + return borrowingsDto; + } + + public static Fine getFine() { + Fine fine = new Fine(); + fine.setAmount(BigDecimal.valueOf(100.00)); + fine.setPaid(false); + return fine; + } + + public static MembersDto getMembersDto() { + MembersDto membersDto = new MembersDto(); + membersDto.setMemberId(Constant.MEMBER_ID); + membersDto.setName("Jane Doe"); + membersDto.setUsername("Jane01"); + membersDto.setEmail("jane.doe@gmail.com"); + membersDto.setPassword("password"); + membersDto.setRole(Role.USER); + membersDto.setMembershipDate(new Date()); + return membersDto; + } + + public static UpdateMembersDto getUpdateMembersDto() { + UpdateMembersDto updateMembersDto = new UpdateMembersDto(); + updateMembersDto.setName("David Green"); + updateMembersDto.setUsername("DGreen"); + updateMembersDto.setEmail("david.green@gmail.com"); + return updateMembersDto; + } + + public static Borrowings getBorrowings() { + Borrowings borrowings = new Borrowings(); + borrowings.setBook(getBook()); + borrowings.setMember(getMembers()); + borrowings.setBorrowDate(new Date()); + borrowings.setDueDate(adjustDays(new Date(), 14)); + borrowings.setReturnDate(null); + return borrowings; + } + + public static Date adjustDays(Date date, int days) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.DAY_OF_MONTH, days); + return calendar.getTime(); + } + + public static Pageable getPageRequest(String sortBy) { + return PageRequest.of(0, 10, Sort.by(sortBy)); + } + + public static Page getBookPage() { + return new PageImpl<>(List.of(getBook())); + } + + public static Members getMembers() { + Members members = new Members(); + members.setMemberId(Constant.MEMBER_ID); + members.setUsername("John01"); + members.setName("John Doe"); + members.setEmail("john@gmail.com"); + members.setPassword("password"); + members.setRole(Role.USER); + members.setMembershipDate(new Date()); + return members; + } + + public static UpdatePasswordDto getUpdatePasswordDto() { + UpdatePasswordDto updatePasswordDto = new UpdatePasswordDto(); + updatePasswordDto.setCurrentPassword("password"); + updatePasswordDto.setNewPassword("newPassword"); + return updatePasswordDto; + } + + public static NewsletterSubscriber getNewsletterSubscriber() { + NewsletterSubscriber newsletterSubscriber = new NewsletterSubscriber(); + newsletterSubscriber.setEmail("emma@hotmail.com"); + newsletterSubscriber.setActive(false); + return newsletterSubscriber; + } + + public static LoginRequest getLoginRequest() { + LoginRequest request = new LoginRequest(); + request.setUsername("username"); + request.setPassword("password"); + return request; + } + + public static class Constant { + public static final int BOOK_ID = 11; + public static final int BORROWING_ID = 22; + public static final int MEMBER_ID = 33; + public static final String BOOK_TITLE = "Test Book"; + } +} diff --git a/src/test/java/com/libraryman_api/analytics/AnalyticsServiceTest.java b/src/test/java/com/libraryman_api/analytics/AnalyticsServiceTest.java new file mode 100644 index 0000000..de971c3 --- /dev/null +++ b/src/test/java/com/libraryman_api/analytics/AnalyticsServiceTest.java @@ -0,0 +1,76 @@ +package com.libraryman_api.analytics; + +import com.libraryman_api.book.BookRepository; +import com.libraryman_api.borrowing.BorrowingRepository; +import com.libraryman_api.member.MemberRepository; +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.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link AnalyticsService} class. + */ +@ExtendWith(MockitoExtension.class) +class AnalyticsServiceTest { + @Mock + private BookRepository bookRepository; + @Mock + private BorrowingRepository borrowingRepository; + @Mock + private MemberRepository memberRepository; + @InjectMocks + private AnalyticsService analyticsService; + + @Test + void getLibraryOverview() { + when(bookRepository.count()).thenReturn(100L); + when(borrowingRepository.count()).thenReturn(50L); + when(memberRepository.count()).thenReturn(25L); + + Map overview = analyticsService.getLibraryOverview(); + + assertEquals(100L, overview.get("totalBooks")); + assertEquals(25L, overview.get("totalMembers")); + assertEquals(50L, overview.get("totalBorrowings")); + } + + @Test + void getPopularBooks() { + List> expectedList = List.of(Map.of("title", "Book A", "borrowCount", 10), Map.of("title", "Book B", "borrowCount", 8)); + when(borrowingRepository.findMostBorrowedBooks(2)).thenReturn(expectedList); + + List> result = analyticsService.getPopularBooks(2); + + assertEquals(expectedList, result); + } + + @Test + void getBorrowingTrends() { + Map trends = Map.of("2025-06-01", 5L, "2025-06-02", 3L); + when(borrowingRepository.getBorrowingTrendsBetweenDates(any(), any())).thenReturn(trends); + + Map result = analyticsService.getBorrowingTrends(LocalDate.now().minusDays(1), LocalDate.now()); + + assertEquals(trends, result); + } + + @Test + void getMemberActivityReport() { + List> report = List.of(Map.of("memberId", 1, "borrowCount", 10), Map.of("memberId", 2, "borrowCount", 5)); + when(memberRepository.getMemberActivityReport()).thenReturn(report); + + List> result = analyticsService.getMemberActivityReport(); + + assertEquals(report, result); + } +} diff --git a/src/test/java/com/libraryman_api/book/BookServiceTest.java b/src/test/java/com/libraryman_api/book/BookServiceTest.java new file mode 100644 index 0000000..70f02d4 --- /dev/null +++ b/src/test/java/com/libraryman_api/book/BookServiceTest.java @@ -0,0 +1,182 @@ +package com.libraryman_api.book; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.TestUtil.Constant; +import com.libraryman_api.exception.InvalidSortFieldException; +import com.libraryman_api.exception.ResourceNotFoundException; +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 org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mapping.PropertyReferenceException; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link BookService} class. + */ +@ExtendWith(MockitoExtension.class) +class BookServiceTest { + @Mock + private BookRepository bookRepository; + @InjectMocks + private BookService bookService; + + @Nested + class GetAllBooks { + @Test + void success() { + Pageable pageable = TestUtil.getPageRequest("title"); + when(bookRepository.findAll(pageable)).thenReturn(TestUtil.getBookPage()); + + Page bookDtoPage = bookService.getAllBooks(pageable); + + assertEquals(1, bookDtoPage.getTotalElements()); + assertEquals(Constant.BOOK_TITLE, bookDtoPage.getContent().get(0).getTitle()); + } + + @Test + void invalidSortField_throwsException() { + Pageable pageable = TestUtil.getPageRequest("nonexistentField"); + when(bookRepository.findAll(pageable)).thenThrow(PropertyReferenceException.class); + + Exception exception = assertThrows(InvalidSortFieldException.class, () -> bookService.getAllBooks(pageable)); + + assertEquals("The specified 'sortBy' value is invalid.", exception.getMessage()); + } + } + + @Nested + class GetBookById { + @Test + void success() { + when(bookRepository.findById(any())).thenReturn(Optional.of(TestUtil.getBook())); + + Optional bookDto = bookService.getBookById(Constant.BOOK_ID); + + assertTrue(bookDto.isPresent()); + assertEquals(Constant.BOOK_TITLE, bookDto.get().getTitle()); + } + + @Test + void noBookFound_returnsEmpty() { + int idNotInRepository = TestUtil.getRandomInt(); + when(bookRepository.findById(anyInt())).thenReturn(Optional.empty()); + + Optional bookDto = bookService.getBookById(idNotInRepository); + + assertTrue(bookDto.isEmpty()); + } + } + + @Test + void addBook() { + BookDto bookDto = TestUtil.getBookDto(); + when(bookRepository.save(any())).thenReturn(TestUtil.getBook()); + + BookDto bookDtoResult = bookService.addBook(bookDto); + + assertEquals(Constant.BOOK_TITLE, bookDtoResult.getTitle()); + } + + @Nested + class UpdateBook { + @Test + void success() { + BookDto bookDtoDetails = TestUtil.getBookDto(); + bookDtoDetails.setTitle("New Title"); + Book existingBook = TestUtil.getBook(); + existingBook.setTitle("Existing Title"); + Book savedBook = TestUtil.getBook(); + savedBook.setTitle(bookDtoDetails.getTitle()); + when(bookRepository.findById(any())).thenReturn(Optional.of(existingBook)); + when(bookRepository.save(any())).thenReturn(savedBook); + + BookDto bookDtoResult = bookService.updateBook(Constant.BOOK_ID, bookDtoDetails); + + assertEquals(bookDtoDetails.getTitle(), bookDtoResult.getTitle()); + } + + @Test + void noBookFound_throwsException() { + int idNotInRepository = TestUtil.getRandomInt(); + BookDto newBookDto = TestUtil.getBookDto(); + when(bookRepository.findById(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> bookService.updateBook(idNotInRepository, newBookDto)); + + assertEquals("Book not found", exception.getMessage()); + } + } + + @Nested + class DeleteBook { + @Test + void success() { + when(bookRepository.findById(any())).thenReturn(Optional.of(TestUtil.getBook())); + + assertDoesNotThrow(() -> bookService.deleteBook(Constant.BOOK_ID)); + } + + @Test + void noBookFound_throwsException() { + int idNotInRepository = TestUtil.getRandomInt(); + when(bookRepository.findById(anyInt())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> bookService.deleteBook(idNotInRepository)); + + assertEquals("Book not found", exception.getMessage()); + } + } + + @Test + void entityToDto() { + Book book = TestUtil.getBook(); + + BookDto bookDto = bookService.EntityToDto(book); + + assertEquals(book.getBookId(), bookDto.getBookId()); + assertEquals(book.getPublisher(), bookDto.getPublisher()); + assertEquals(book.getPublishedYear(), bookDto.getPublishedYear()); + assertEquals(book.getTitle(), bookDto.getTitle()); + assertEquals(book.getAuthor(), bookDto.getAuthor()); + assertEquals(book.getGenre(), bookDto.getGenre()); + assertEquals(book.getIsbn(), bookDto.getIsbn()); + assertEquals(book.getCopiesAvailable(), bookDto.getCopiesAvailable()); + } + + @Test + void dtoToEntity() { + BookDto bookDto = TestUtil.getBookDto(); + + Book book = bookService.DtoToEntity(bookDto); + + assertEquals(bookDto.getBookId(), book.getBookId()); + assertEquals(bookDto.getPublisher(), book.getPublisher()); + assertEquals(bookDto.getPublishedYear(), book.getPublishedYear()); + assertEquals(bookDto.getTitle(), book.getTitle()); + assertEquals(bookDto.getAuthor(), book.getAuthor()); + assertEquals(bookDto.getGenre(), book.getGenre()); + assertEquals(bookDto.getIsbn(), book.getIsbn()); + assertEquals(bookDto.getCopiesAvailable(), book.getCopiesAvailable()); + } + + @Test + void searchBook() { + Pageable pageable = TestUtil.getPageRequest("title"); + when(bookRepository.searchBook("keyword", pageable)).thenReturn(TestUtil.getBookPage()); + + Page bookPage = bookService.searchBook("keyword", pageable); + + assertEquals(Constant.BOOK_TITLE, bookPage.getContent().get(0).getTitle()); + } +} \ No newline at end of file diff --git a/src/test/java/com/libraryman_api/borrowing/BorrowingServiceTest.java b/src/test/java/com/libraryman_api/borrowing/BorrowingServiceTest.java new file mode 100644 index 0000000..98ad2f1 --- /dev/null +++ b/src/test/java/com/libraryman_api/borrowing/BorrowingServiceTest.java @@ -0,0 +1,429 @@ +package com.libraryman_api.borrowing; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.TestUtil.Constant; +import com.libraryman_api.book.BookDto; +import com.libraryman_api.book.BookService; +import com.libraryman_api.exception.InvalidSortFieldException; +import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.fine.Fine; +import com.libraryman_api.fine.FineRepository; +import com.libraryman_api.member.MemberService; +import com.libraryman_api.notification.NotificationService; +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 org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mapping.PropertyReferenceException; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link BorrowingService}. + */ +@ExtendWith(MockitoExtension.class) +class BorrowingServiceTest { + @Mock + private BorrowingRepository borrowingRepository; + @Mock + private FineRepository fineRepository; + @Mock + private NotificationService notificationService; + @Mock + private BookService bookService; + @Mock + private MemberService memberService; + @InjectMocks + private BorrowingService borrowingService; + + @Nested + class GetAllBorrowings { + @Test + void success() { + Pageable pageable = TestUtil.getPageRequest("dueDate"); + Borrowings borrowings = TestUtil.getBorrowings(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + Page borrowingsPage = new PageImpl<>(List.of(borrowings)); + when(borrowingRepository.findAll(pageable)).thenReturn(borrowingsPage); + + Page borrowingsDtoPage = borrowingService.getAllBorrowings(pageable); + + assertEquals(1, borrowingsDtoPage.getTotalElements()); + assertEquals(Constant.BOOK_TITLE, borrowingsDtoPage.getContent().get(0).getBook().getTitle()); + } + + @Test + void invalidSortField_throwsException() { + Pageable pageable = TestUtil.getPageRequest("dueDate"); + when(borrowingRepository.findAll(pageable)).thenThrow(PropertyReferenceException.class); + + Exception exception = assertThrows(InvalidSortFieldException.class, () -> borrowingService.getAllBorrowings(pageable)); + + assertEquals("The specified 'sortBy' value is invalid.", exception.getMessage()); + } + } + + @Nested + class GetBorrowingById { + @Test + void success() { + Borrowings borrowings = TestUtil.getBorrowings(); + when(borrowingRepository.findById(any())).thenReturn(Optional.of(borrowings)); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + Optional borrowingsDto = borrowingService.getBorrowingById(Constant.BORROWING_ID); + + assertTrue(borrowingsDto.isPresent()); + assertEquals(Constant.BOOK_TITLE, borrowingsDto.get().getBook().getTitle()); + } + + @Test + void noBorrowingsFound_returnsEmpty() { + when(borrowingRepository.findById(any())).thenReturn(Optional.empty()); + + Optional borrowingsDto = borrowingService.getBorrowingById(Constant.BORROWING_ID); + + assertTrue(borrowingsDto.isEmpty()); + } + } + + @Nested + class BorrowBook { + @Test + void success() { + when(bookService.getBookById(anyInt())).thenReturn(Optional.of(TestUtil.getBookDto())); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(borrowingRepository.save(any())).thenReturn(TestUtil.getBorrowings()); + + BorrowingsDto borrowingsDto = borrowingService.borrowBook(TestUtil.getBorrowingsDto()); + + assertEquals(Constant.BOOK_TITLE, borrowingsDto.getBook().getTitle()); + } + + @Test + void noCopiesAvailable_throwsException() { + BookDto bookDto = TestUtil.getBookDto(); + bookDto.setCopiesAvailable(0); + when(bookService.getBookById(anyInt())).thenReturn(Optional.of(bookDto)); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.borrowBook(TestUtil.getBorrowingsDto())); + + assertEquals("Not enough copies available", exception.getMessage()); + } + + @Test + void noBookFound_throwsException() { + when(bookService.getBookById(anyInt())).thenReturn(Optional.empty()); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.borrowBook(TestUtil.getBorrowingsDto())); + + assertEquals("Book not found", exception.getMessage()); + } + + @Test + void noMemberFound_throwsException() { + when(bookService.getBookById(anyInt())).thenReturn(Optional.of(TestUtil.getBookDto())); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.borrowBook(TestUtil.getBorrowingsDto())); + + assertEquals("Member not found", exception.getMessage()); + } + } + + @Nested + class ReturnBook { + @Test + void success() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(borrowingRepository.save(any())).thenReturn(borrowings); + + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + + when(bookService.getBookById(anyInt())).thenReturn(Optional.of(TestUtil.getBookDto())); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + BorrowingsDto borrowingsDto = borrowingService.returnBook(Constant.BORROWING_ID); + + assertEquals(Constant.BOOK_TITLE, borrowingsDto.getBook().getTitle()); + assertNotNull(borrowingsDto.getDueDate()); + } + + @Test + void noBorrowingsFound_throwsException() { + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.returnBook(Constant.BORROWING_ID)); + + assertEquals("Borrowing not found", exception.getMessage()); + } + + @Test + void noMemberFound_throwsException() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.empty()); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.returnBook(Constant.BORROWING_ID)); + + assertEquals("Member not found", exception.getMessage()); + } + + @Test + void bookAlreadyReturned_throwsException() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + borrowings.setReturnDate(new Date()); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.returnBook(Constant.BORROWING_ID)); + + assertEquals("Book has already been returned", exception.getMessage()); + } + + @Nested + class DueDateMissed { + @Test + void noFine_throwsException() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(borrowingRepository.save(any())).thenReturn(borrowings); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.returnBook(Constant.BORROWING_ID)); + + assertEquals("Due date passed. Fine imposed, pay fine first to return the book", exception.getMessage()); + } + + @Test + void hasOutstandingFine_throwsException() { + Fine fine = TestUtil.getFine(); + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + borrowings.setFine(fine); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.returnBook(Constant.BORROWING_ID)); + + assertEquals("Outstanding fine, please pay before returning the book", exception.getMessage()); + } + + @Test + void hasPaidFine_successfullyReturned() { + Fine fine = TestUtil.getFine(); + fine.setPaid(true); + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().minusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + borrowings.setFine(fine); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(borrowingRepository.save(any())).thenReturn(borrowings); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.DtoEntity(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.getBookById(anyInt())).thenReturn(Optional.of(TestUtil.getBookDto())); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + BorrowingsDto borrowingsDto = borrowingService.returnBook(Constant.BORROWING_ID); + + assertEquals(Constant.BOOK_TITLE, borrowingsDto.getBook().getTitle()); + assertNotNull(borrowingsDto.getDueDate()); + } + } + } + + @Nested + class PayFine { + @Test + void success() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setFine(TestUtil.getFine()); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(borrowingRepository.save(any())).thenReturn(borrowings); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + String result = borrowingService.payFine(Constant.BORROWING_ID); + + assertEquals("PAID", result); + } + + @Test + void borrowingNotFound_throwsException() { + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.payFine(Constant.BORROWING_ID)); + + assertEquals("Borrowing not found", exception.getMessage()); + } + + @Test + void memberNotFound_throwsException() { + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(TestUtil.getBorrowings())); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.empty()); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.payFine(Constant.BORROWING_ID)); + + assertEquals("Member not found", exception.getMessage()); + } + + @Test + void nullFine_throwsException() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.payFine(Constant.BORROWING_ID)); + + assertEquals("No outstanding fine found or fine already paid", exception.getMessage()); + } + + @Test + void fineAlreadyPaid_throwsException() { + Fine fine = TestUtil.getFine(); + fine.setPaid(true); + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setDueDate(Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant())); + when(borrowingRepository.findById(anyInt())).thenReturn(Optional.of(borrowings)); + when(memberService.getMemberById(anyInt())).thenReturn(Optional.of(TestUtil.getMembersDto())); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.payFine(Constant.BORROWING_ID)); + + assertEquals("No outstanding fine found or fine already paid", exception.getMessage()); + } + } + + @Nested + class UpdateBookCopies { + + @Nested + class Add { + @Test + void success() { + BookDto bookDto = TestUtil.getBookDto(); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + + assertDoesNotThrow(() -> borrowingService.updateBookCopies(Optional.of(bookDto), "ADD", 1)); + } + + @Test + void bookNotPresent_throwsException() { + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.updateBookCopies(Optional.empty(), "ADD", 1)); + + assertEquals("Book not found", exception.getMessage()); + } + } + + @Nested + class Remove { + @Test + void success() { + BookDto bookDto = TestUtil.getBookDto(); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + + assertDoesNotThrow(() -> borrowingService.updateBookCopies(Optional.of(bookDto), "REMOVE", 1)); + } + + @Test + void bookNotPresent_throwsException() { + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.updateBookCopies(Optional.empty(), "REMOVE", 1)); + + assertEquals("Book not found", exception.getMessage()); + } + + @Test + void notEnoughCopies_throwsException() { + BookDto bookDto = TestUtil.getBookDto(); + bookDto.setCopiesAvailable(0); + when(bookService.DtoToEntity(any())).thenCallRealMethod(); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.updateBookCopies(Optional.of(bookDto), "REMOVE", 1)); + + assertEquals("Not enough copies are available", exception.getMessage()); + } + } + } + + @Nested + class GetAllBorrowingsOfMember { + @Test + void success() { + Pageable pageable = TestUtil.getPageRequest("dueDate"); + Borrowings borrowings = TestUtil.getBorrowings(); + Page borrowingsPage = new PageImpl<>(List.of(borrowings)); + when(borrowingRepository.findByMember_memberId(anyInt(), any())).thenReturn(borrowingsPage); + when(bookService.EntityToDto(any())).thenCallRealMethod(); + when(memberService.EntityToDto(any())).thenCallRealMethod(); + + Page borrowingsDtoPage = borrowingService.getAllBorrowingsOfMember(Constant.MEMBER_ID, pageable); + + assertEquals(Constant.BOOK_TITLE, borrowingsDtoPage.getContent().get(0).getBook().getTitle()); + } + + @Test + void memberNotFound_throwsException() { + Pageable pageable = TestUtil.getPageRequest("dueDate"); + when(borrowingRepository.findByMember_memberId(anyInt(), any())).thenReturn(new PageImpl<>(List.of())); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> borrowingService.getAllBorrowingsOfMember(Constant.MEMBER_ID, pageable)); + + assertEquals("Member didn't borrow any book", exception.getMessage()); + } + + @Test + void sortByFieldInvalid_throwsException() { + Pageable pageable = TestUtil.getPageRequest("nonexistentField"); + when(borrowingRepository.findByMember_memberId(anyInt(), any())).thenThrow(PropertyReferenceException.class); + + Exception exception = assertThrows(InvalidSortFieldException.class, () -> borrowingService.getAllBorrowingsOfMember(Constant.MEMBER_ID, pageable)); + + assertEquals("The specified 'sortBy' value is invalid.", exception.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/libraryman_api/email/EmailServiceTest.java b/src/test/java/com/libraryman_api/email/EmailServiceTest.java new file mode 100644 index 0000000..4809208 --- /dev/null +++ b/src/test/java/com/libraryman_api/email/EmailServiceTest.java @@ -0,0 +1,111 @@ +package com.libraryman_api.email; + +import com.libraryman_api.notification.NotificationRepository; +import com.libraryman_api.notification.NotificationStatus; +import com.libraryman_api.notification.Notifications; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mail.javamail.JavaMailSender; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link EmailService}. + */ +@ExtendWith(MockitoExtension.class) +class EmailServiceTest { + @Mock + private NotificationRepository notificationRepository; + @Mock + private JavaMailSender mailSender; + @InjectMocks + private EmailService emailService; + + @Captor + private ArgumentCaptor notificationCaptor; + @Captor + private ArgumentCaptor mimeMessageCaptor; + + @BeforeEach + void setup() throws Exception { + injectPropertyValue(emailService); + when(mailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); + } + + private void injectPropertyValue(Object emailService) throws Exception { + Field field = emailService.getClass().getDeclaredField("domainName"); + field.setAccessible(true); + field.set(emailService, "default.io"); + } + + @Nested + class SendEmail { + @Test + void withFrom() throws MessagingException { + emailService.sendEmail("to@example.com", "

Hello

", "Subject", "sender@libraryman.com"); + + verify(mailSender).send(mimeMessageCaptor.capture()); + MimeMessage sentMessage = mimeMessageCaptor.getValue(); + assertEquals("to@example.com", sentMessage.getRecipients(Message.RecipientType.TO)[0].toString()); + assertEquals("Subject", sentMessage.getSubject()); + assertEquals("sender@libraryman.com", sentMessage.getFrom()[0].toString()); + } + + @Test + void withoutFrom_usesDefaultDomain() throws MessagingException { + emailService.sendEmail("to@example.com", "

Hello

", "Subject"); + + verify(mailSender).send(mimeMessageCaptor.capture()); + MimeMessage sentMessage = mimeMessageCaptor.getValue(); + assertEquals("to@example.com", sentMessage.getRecipients(Message.RecipientType.TO)[0].toString()); + assertEquals("Subject", sentMessage.getSubject()); + assertEquals("default.io", sentMessage.getFrom()[0].toString()); + } + + @Test + void invalidToEmail_throwsException() { + String invalidToEmail = ""; + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> emailService.sendEmail(invalidToEmail, "Body", "Subject", "sender@libraryman.com")); + + assertTrue(ex.getMessage().contains("Failed to send email")); + } + } + + @Nested + class Send { + @Test + void success() { + emailService.send("to@example.com", "

Hello

", "Subject", new Notifications()); + + verify(notificationRepository).save(notificationCaptor.capture()); + assertEquals(NotificationStatus.SENT, notificationCaptor.getValue().getNotificationStatus()); + } + + @Test + void invalidToEmail_updatesStatusToFailed() { + String invalidToEmail = ""; + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> emailService.send(invalidToEmail, "Email", "Subject", new Notifications())); + + assertTrue(ex.getMessage().contains("Failed to send notification email")); + verify(notificationRepository).save(notificationCaptor.capture()); + assertEquals(NotificationStatus.FAILED, notificationCaptor.getValue().getNotificationStatus()); + } + } +} diff --git a/src/test/java/com/libraryman_api/member/MemberServiceTest.java b/src/test/java/com/libraryman_api/member/MemberServiceTest.java new file mode 100644 index 0000000..cf4dd42 --- /dev/null +++ b/src/test/java/com/libraryman_api/member/MemberServiceTest.java @@ -0,0 +1,237 @@ +package com.libraryman_api.member; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.exception.InvalidPasswordException; +import com.libraryman_api.exception.InvalidSortFieldException; +import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.member.dto.MembersDto; +import com.libraryman_api.member.dto.UpdateMembersDto; +import com.libraryman_api.member.dto.UpdatePasswordDto; +import com.libraryman_api.notification.NotificationService; +import com.libraryman_api.security.config.PasswordEncoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mapping.PropertyReferenceException; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link MemberService}. + */ +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + private MemberService memberService; + @Mock + private MemberRepository memberRepository; + @Mock + private NotificationService notificationService; + private final PasswordEncoder passwordEncoder = new PasswordEncoder(); + + @BeforeEach + void setup() { + memberService = new MemberService(memberRepository, notificationService, passwordEncoder); + } + + @Nested + class GetAllMembers { + @Test + void success() { + Pageable pageable = TestUtil.getPageRequest("name"); + Page membersPage = new PageImpl<>(List.of(TestUtil.getMembers())); + when(memberRepository.findAll(pageable)).thenReturn(membersPage); + + Page membersDtoPage = memberService.getAllMembers(pageable); + + assertEquals(1, membersDtoPage.getTotalElements()); + assertEquals(TestUtil.Constant.MEMBER_ID, membersDtoPage.getContent().get(0).getMemberId()); + } + + @Test + void sortByFieldInvalid_throwsException() { + Pageable pageable = TestUtil.getPageRequest("nonexistentField"); + when(memberRepository.findAll(pageable)).thenThrow(PropertyReferenceException.class); + + Exception exception = assertThrows(InvalidSortFieldException.class, () -> memberService.getAllMembers(pageable)); + + assertEquals("The specified 'sortBy' value is invalid.", exception.getMessage()); + } + } + + @Nested + class GetMemberById { + @Test + void success() { + when(memberRepository.findById(any())).thenReturn(Optional.of(TestUtil.getMembers())); + + Optional membersDto = memberService.getMemberById(TestUtil.Constant.MEMBER_ID); + + assertTrue(membersDto.isPresent()); + assertEquals(TestUtil.Constant.MEMBER_ID, membersDto.get().getMemberId()); + } + + @Test + void noMemberFound_returnsEmpty() { + int idNotInRepository = TestUtil.getRandomInt(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + + Optional membersDto = memberService.getMemberById(idNotInRepository); + + assertTrue(membersDto.isEmpty()); + } + } + + @Test + void addMember() { + MembersDto membersDto = TestUtil.getMembersDto(); + when(memberRepository.save(any())).thenReturn(TestUtil.getMembers()); + + MembersDto membersDtoResult = memberService.addMember(membersDto); + + assertEquals(TestUtil.Constant.MEMBER_ID, membersDtoResult.getMemberId()); + } + + @Nested + class UpdateMember { + @Test + void success() { + UpdateMembersDto membersDtoDetails = TestUtil.getUpdateMembersDto(); + Members existingMembers = TestUtil.getMembers(); + Members savedMembers = TestUtil.getMembers(); + savedMembers.setName(membersDtoDetails.getName()); + when(memberRepository.findById(any())).thenReturn(Optional.of(existingMembers)); + when(memberRepository.save(any())).thenReturn(savedMembers); + + MembersDto membersDtoResult = memberService.updateMember(TestUtil.Constant.MEMBER_ID, membersDtoDetails); + + assertEquals(membersDtoDetails.getName(), membersDtoResult.getName()); + } + + @Test + void noMemberFound_throwsException() { + int idNotInRepository = TestUtil.getRandomInt(); + UpdateMembersDto membersDtoDetails = TestUtil.getUpdateMembersDto(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> memberService.updateMember(idNotInRepository, membersDtoDetails)); + + assertEquals("Member not found", exception.getMessage()); + } + } + + @Nested + class DeleteMember { + @Test + void success() { + when(memberRepository.findById(any())).thenReturn(Optional.of(TestUtil.getMembers())); + + assertDoesNotThrow(() -> memberService.deleteMember(TestUtil.Constant.MEMBER_ID)); + } + + @Test + void noMemberFound_throwsException() { + int idNotInRepository = TestUtil.getRandomInt(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> memberService.deleteMember(idNotInRepository)); + + assertEquals("Member not found", exception.getMessage()); + } + } + + @Nested + class UpdatePassword { + @Test + void success() { + String oldPassword = "oldPassword"; + String encodedPassword = passwordEncoder.bCryptPasswordEncoder().encode(oldPassword); + Members member = TestUtil.getMembers(); + member.setPassword(encodedPassword); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + + UpdatePasswordDto updatePasswordDto = new UpdatePasswordDto(); + updatePasswordDto.setCurrentPassword(oldPassword); + updatePasswordDto.setNewPassword("newPassword"); + + memberService.updatePassword(TestUtil.Constant.MEMBER_ID, updatePasswordDto); + + assertTrue(passwordEncoder.bCryptPasswordEncoder().matches("newPassword", member.getPassword())); + } + + @Test + void memberNotFound_throwsException() { + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, () -> memberService.updatePassword(TestUtil.Constant.MEMBER_ID, TestUtil.getUpdatePasswordDto())); + + assertEquals("Member not found", exception.getMessage()); + } + + @Test + void incorrectCurrentPassword_throwsException() { + when(memberRepository.findById(any())).thenReturn(Optional.of(TestUtil.getMembers())); + + InvalidPasswordException exception = assertThrows(InvalidPasswordException.class, () -> memberService.updatePassword(TestUtil.Constant.MEMBER_ID, TestUtil.getUpdatePasswordDto())); + + assertEquals("Current password is incorrect", exception.getMessage()); + } + + @Test + void updatePassword_newPasswordSameAsOld_throwsException() { + String oldPassword = "oldPassword"; + String encodedPassword = passwordEncoder.bCryptPasswordEncoder().encode(oldPassword); + Members member = TestUtil.getMembers(); + member.setPassword(encodedPassword); + when(memberRepository.findById(any())).thenReturn(Optional.of(member)); + + UpdatePasswordDto updatePasswordDto = new UpdatePasswordDto(); + updatePasswordDto.setCurrentPassword(oldPassword); + updatePasswordDto.setNewPassword(oldPassword); + + InvalidPasswordException exception = assertThrows(InvalidPasswordException.class, () -> memberService.updatePassword(TestUtil.Constant.MEMBER_ID, updatePasswordDto)); + + assertEquals("New password must be different from the old password", exception.getMessage()); + } + } + + @Test + void dtoEntity() { + MembersDto membersDto = TestUtil.getMembersDto(); + + Members members = memberService.DtoEntity(membersDto); + + assertEquals(membersDto.getMemberId(), members.getMemberId()); + assertEquals(membersDto.getRole(), members.getRole()); + assertEquals(membersDto.getName(), members.getName()); + assertEquals(membersDto.getUsername(), members.getUsername()); + assertEquals(membersDto.getEmail(), members.getEmail()); + assertEquals(membersDto.getPassword(), members.getPassword()); + assertEquals(membersDto.getMembershipDate(), members.getMembershipDate()); + } + + @Test + void entityToDto() { + Members members = TestUtil.getMembers(); + + MembersDto membersDto = memberService.EntityToDto(members); + + assertEquals(members.getMemberId(), membersDto.getMemberId()); + assertEquals(members.getName(), membersDto.getName()); + assertEquals(members.getUsername(), membersDto.getUsername()); + assertEquals(members.getRole(), membersDto.getRole()); + assertEquals(members.getEmail(), membersDto.getEmail()); + assertEquals(members.getPassword(), membersDto.getPassword()); + assertEquals(members.getMembershipDate(), membersDto.getMembershipDate()); + } +} diff --git a/src/test/java/com/libraryman_api/newsletter/NewsletterServiceTest.java b/src/test/java/com/libraryman_api/newsletter/NewsletterServiceTest.java new file mode 100644 index 0000000..6cdff3e --- /dev/null +++ b/src/test/java/com/libraryman_api/newsletter/NewsletterServiceTest.java @@ -0,0 +1,123 @@ +package com.libraryman_api.newsletter; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.email.EmailService; +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.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Tests the {@link NewsletterService}. + */ +@ExtendWith(MockitoExtension.class) +class NewsletterServiceTest { + @Mock + private NewsletterSubscriberRepository subscriberRepository; + @Mock + private EmailService emailService; + @InjectMocks + private NewsletterService newsletterService; + + @Nested + class Subscribe { + @Test + void success() { + NewsletterSubscriber newsletterSubscriber = TestUtil.getNewsletterSubscriber(); + String email = newsletterSubscriber.getEmail(); + when(subscriberRepository.findByEmail(any())).thenReturn(Optional.of(newsletterSubscriber)); + + String result = newsletterService.subscribe(email); + + assertEquals("You have successfully re-subscribed!", result); + verify(subscriberRepository).save(newsletterSubscriber); + verify(emailService).sendEmail(eq(email), any(), any()); + } + + @Test + void alreadySubscribed() { + NewsletterSubscriber newsletterSubscriber = TestUtil.getNewsletterSubscriber(); + newsletterSubscriber.setActive(true); + String email = newsletterSubscriber.getEmail(); + when(subscriberRepository.findByEmail(any())).thenReturn(Optional.of(newsletterSubscriber)); + + String result = newsletterService.subscribe(email); + + assertEquals("Email is already subscribed.", result); + verify(subscriberRepository, never()).save(newsletterSubscriber); + verify(emailService, never()).sendEmail(eq(email), any(), any()); + } + + @Test + void noSubscriberFound() { + String email = "wendy@outlook.com"; + when(subscriberRepository.findByEmail(any())).thenReturn(Optional.empty()); + + String result = newsletterService.subscribe(email); + + assertEquals("You have successfully subscribed!", result); + verify(subscriberRepository).save(any()); + verify(emailService).sendEmail(eq(email), any(), any()); + } + + @Test + void invalidEmail() { + String email = "wendy@outlookcom"; + + String result = newsletterService.subscribe(email); + + assertEquals("Invalid email format.", result); + } + } + + @Nested + class Unsubscribe { + @Test + void success() { + NewsletterSubscriber newsletterSubscriber = TestUtil.getNewsletterSubscriber(); + newsletterSubscriber.setActive(true); + String token = newsletterSubscriber.getUnsubscribeToken(); + when(subscriberRepository.findByUnsubscribeToken(any())).thenReturn(Optional.of(newsletterSubscriber)); + + String result = newsletterService.unsubscribe(token); + + assertEquals("You have successfully unsubscribed!", result); + verify(subscriberRepository).save(newsletterSubscriber); + verify(emailService).sendEmail(eq(newsletterSubscriber.getEmail()), any(), any()); + } + + @Test + void invalidToken() { + when(subscriberRepository.findByUnsubscribeToken(any())).thenReturn(Optional.empty()); + + String result = newsletterService.unsubscribe("token"); + + assertEquals("Invalid or expired token.", result); + verify(subscriberRepository, never()).save(any()); + verify(emailService, never()).sendEmail(any(), any(), any()); + } + + @Test + void alreadyUnsubscribed() { + NewsletterSubscriber newsletterSubscriber = TestUtil.getNewsletterSubscriber(); + newsletterSubscriber.setActive(false); + String token = newsletterSubscriber.getUnsubscribeToken(); + when(subscriberRepository.findByUnsubscribeToken(any())).thenReturn(Optional.of(newsletterSubscriber)); + + String result = newsletterService.unsubscribe(token); + + assertEquals("You are already unsubscribed.", result); + verify(subscriberRepository, never()).save(any()); + verify(emailService, never()).sendEmail(any(), any(), any()); + } + } +} diff --git a/src/test/java/com/libraryman_api/notification/NotificationServiceTest.java b/src/test/java/com/libraryman_api/notification/NotificationServiceTest.java new file mode 100644 index 0000000..9ad7132 --- /dev/null +++ b/src/test/java/com/libraryman_api/notification/NotificationServiceTest.java @@ -0,0 +1,175 @@ +package com.libraryman_api.notification; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.borrowing.Borrowings; +import com.libraryman_api.email.EmailSender; +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.member.Members; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Tests the {@link NotificationService}. + */ +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + @Mock + private EmailSender emailSender; + @Mock + private NotificationRepository notificationRepository; + @Mock + private MemberRepository memberRepository; + @InjectMocks + private NotificationService notificationService; + + @Captor + private ArgumentCaptor emailToCaptor; + @Captor + private ArgumentCaptor emailBodyCaptor; + @Captor + private ArgumentCaptor emailSubjectCaptor; + @Captor + private ArgumentCaptor notificationCaptor; + + @Test + void accountCreatedNotification() { + Members members = TestUtil.getMembers(); + + notificationService.accountCreatedNotification(members); + + verifyEmailSent(members.getEmail(), "We’re excited to welcome you to LibraryMan", "Welcome to LibraryMan!", NotificationType.ACCOUNT_CREATED); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void accountDeletionNotification() { + Members members = TestUtil.getMembers(); + + notificationService.accountDeletionNotification(members); + + verifyEmailSent(members.getEmail(), "We’re sorry to see you go!", "Your LibraryMan Account has been Deleted", NotificationType.ACCOUNT_DELETED); + } + + @Test + void borrowBookNotification() { + Borrowings borrowings = TestUtil.getBorrowings(); + + notificationService.borrowBookNotification(borrowings); + + verifyEmailSent(borrowings.getMember().getEmail(), "You have successfully borrowed", "Book Borrowed Successfully", NotificationType.BORROW); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void reminderNotification() { + Borrowings borrowings = TestUtil.getBorrowings(); + + notificationService.reminderNotification(borrowings); + + verifyEmailSent(borrowings.getMember().getEmail(), "This is a friendly reminder that the due date to return", "Reminder: Book Due Date Approaching", NotificationType.REMINDER); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void finePaidNotification() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setFine(TestUtil.getFine()); + + notificationService.finePaidNotification(borrowings); + + verifyEmailSent(borrowings.getMember().getEmail(), "Thank you for your payment.", "Payment Received", NotificationType.PAID); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void fineImposedNotification() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setFine(TestUtil.getFine()); + + notificationService.fineImposedNotification(borrowings); + + verifyEmailSent(borrowings.getMember().getEmail(), "Unfortunately, our records show that the book was returned after the due date", "Fine Imposed for Late Return", NotificationType.FINE); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void accountDetailsUpdateNotification() { + Members members = TestUtil.getMembers(); + + notificationService.accountDetailsUpdateNotification(members); + + verifyEmailSent(members.getEmail(), "Your account details have been successfully updated", "Your Account Details Have Been Updated", NotificationType.UPDATE); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void bookReturnedNotification() { + Borrowings borrowings = TestUtil.getBorrowings(); + borrowings.setReturnDate(new Date()); + + notificationService.bookReturnedNotification(borrowings); + + verifyEmailSent(borrowings.getMember().getEmail(), "Thank you for returning", "Thank You for Returning Your Book", NotificationType.RETURNED); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Nested + class SendDueDateReminders { + @Test + void success() { + Borrowings borrowings = TestUtil.getBorrowings(); + List borrowingsList = List.of(borrowings); + when(notificationRepository.findBorrowingsDueInDays(any(), any())).thenReturn(borrowingsList); + when(memberRepository.findByMemberId(anyInt())).thenReturn(Optional.of(borrowings.getMember())); + + notificationService.sendDueDateReminders(); + + verifyEmailSent(borrowings.getMember().getEmail(), "This is a friendly reminder that the due date to return", "Reminder: Book Due Date Approaching", NotificationType.REMINDER); + verify(notificationRepository).save(notificationCaptor.getValue()); + } + + @Test + void noBooksDueSoon() { + when(notificationRepository.findBorrowingsDueInDays(any(), any())).thenReturn(List.of()); + + notificationService.sendDueDateReminders(); + + verify(emailSender, never()).send(any(), any(), any(), any()); + } + + @Test + void memberNotFound() { + Borrowings borrowings = TestUtil.getBorrowings(); + List borrowingsList = List.of(borrowings); + when(notificationRepository.findBorrowingsDueInDays(any(), any())).thenReturn(borrowingsList); + when(memberRepository.findByMemberId(anyInt())).thenReturn(Optional.empty()); + + notificationService.sendDueDateReminders(); + + verify(emailSender, never()).send(any(), any(), any(), any()); + } + } + + private void verifyEmailSent(String expectedToEmail, String expectedPartOfBodyText, String expectedSubjectText, NotificationType expectedNotificationType) { + verify(emailSender).send(emailToCaptor.capture(), emailBodyCaptor.capture(), emailSubjectCaptor.capture(), notificationCaptor.capture()); + assertEquals(expectedToEmail, emailToCaptor.getValue()); + assertTrue(emailBodyCaptor.getValue().contains(expectedPartOfBodyText)); + assertEquals(expectedSubjectText, emailSubjectCaptor.getValue()); + assertEquals(expectedNotificationType, notificationCaptor.getValue().getNotificationType()); + } +} diff --git a/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationFilterTest.java b/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationFilterTest.java new file mode 100644 index 0000000..9eaee84 --- /dev/null +++ b/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationFilterTest.java @@ -0,0 +1,146 @@ +package com.libraryman_api.security.jwt; + +import com.libraryman_api.TestUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link JwtAuthenticationFilter}. + */ +@ExtendWith(MockitoExtension.class) +class JwtAuthenticationFilterTest { + @Mock + private JwtAuthenticationHelper jwtAuthenticationHelper; + @Mock + private UserDetailsService userDetailsService; + @InjectMocks + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @BeforeEach + void setup() { + SecurityContextHolder.clearContext(); + } + + @Nested + class DoFilterInternal { + @Test + void success() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer valid.token.here"); + + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenReturn("user"); + when(userDetailsService.loadUserByUsername(any())).thenReturn(TestUtil.getMembers()); + when(jwtAuthenticationHelper.isTokenExpired(any())).thenReturn(false); + + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void noAuthorizationHeader() { + MockHttpServletRequest request = new MockHttpServletRequest(); + + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void notBearer() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Basic encoded.credentials"); + + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @ParameterizedTest + @CsvSource({"io.jsonwebtoken.ExpiredJwtException", "io.jsonwebtoken.MalformedJwtException", "io.jsonwebtoken.SignatureException"}) + void getUsernameFromToken_throwsException(String exceptionClassName) throws ClassNotFoundException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer some.token.here"); + Class exception = (Class) Class.forName(exceptionClassName); + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenThrow(exception); + + assertThrows(exception, () -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void nullUsername() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer valid.token.here"); + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenReturn(null); + + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void usernameNotFoundException() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer valid.token.here"); + + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenReturn("user"); + when(userDetailsService.loadUserByUsername(any())).thenThrow(UsernameNotFoundException.class); + + assertThrows(UsernameNotFoundException.class, () -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void tokenExpired() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer valid.token.here"); + + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenReturn("user"); + when(userDetailsService.loadUserByUsername(any())).thenReturn(TestUtil.getMembers()); + when(jwtAuthenticationHelper.isTokenExpired(any())).thenReturn(true); + + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + void securityContextAlreadyExists() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer valid.token.here"); + + when(jwtAuthenticationHelper.getUsernameFromToken(any())).thenReturn("user"); + + Authentication existingAuth = mock(Authentication.class); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(existingAuth); + SecurityContextHolder.setContext(context); + assertDoesNotThrow(() -> jwtAuthenticationFilter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain())); + + assertEquals(existingAuth, SecurityContextHolder.getContext().getAuthentication()); + } + } +} diff --git a/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationHelperTest.java b/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationHelperTest.java new file mode 100644 index 0000000..5d9b886 --- /dev/null +++ b/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationHelperTest.java @@ -0,0 +1,77 @@ +package com.libraryman_api.security.jwt; + +import com.libraryman_api.TestUtil; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests the {@link JwtAuthenticationHelper}. + */ +@ExtendWith(MockitoExtension.class) +class JwtAuthenticationHelperTest { + private JwtAuthenticationHelper jwtAuthenticationHelper; + private String secret; + + @BeforeEach + void setup() { + secret = "aVeryLongSecretStringThatIsAtLeast64BytesLongAndSecureEnoughForHS512"; + jwtAuthenticationHelper = new JwtAuthenticationHelper(secret); + } + + @Nested + class GetUsernameFromToken { + @Test + void success() { + String expectedUsername = "User"; + String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256).setSubject(expectedUsername).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 100000)).compact(); + + String actualUsername = jwtAuthenticationHelper.getUsernameFromToken(token); + + assertEquals(expectedUsername, actualUsername); + } + + @Test + void expired() { + Date date = new Date(); + String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256).setSubject("User").setIssuedAt(date).setExpiration(date).compact(); + + assertThrows(ExpiredJwtException.class, () -> jwtAuthenticationHelper.getUsernameFromToken(token)); + } + + @Test + void malformed() { + assertThrows(MalformedJwtException.class, () -> jwtAuthenticationHelper.getUsernameFromToken("malformed.token")); + } + + @Test + void signatureException() { + String differentSecret = "notTheSameSecretAsTheSecretValueInTheHelper"; + String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(differentSecret.getBytes()), SignatureAlgorithm.HS256).setSubject("User").setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 100000)).compact(); + + assertThrows(SignatureException.class, () -> jwtAuthenticationHelper.getUsernameFromToken(token)); + } + } + + @Test + void generateToken() { + UserDetails userDetails = TestUtil.getMembers(); + + String token = jwtAuthenticationHelper.generateToken(userDetails); + + Claims claims = jwtAuthenticationHelper.getClaimsFromToken(token); + assertEquals(userDetails.getUsername(), claims.getSubject()); + assertNotNull(claims.getIssuedAt()); + assertNotNull(claims.getExpiration()); + } +} diff --git a/src/test/java/com/libraryman_api/security/services/CustomUserDetailsServiceTest.java b/src/test/java/com/libraryman_api/security/services/CustomUserDetailsServiceTest.java new file mode 100644 index 0000000..0b18641 --- /dev/null +++ b/src/test/java/com/libraryman_api/security/services/CustomUserDetailsServiceTest.java @@ -0,0 +1,51 @@ +package com.libraryman_api.security.services; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.member.Members; +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 org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link CustomUserDetailsService}. + */ +@ExtendWith(MockitoExtension.class) +class CustomUserDetailsServiceTest { + @Mock + private MemberRepository memberRepository; + @InjectMocks + private CustomUserDetailsService customUserDetailsService; + + @Nested + class LoadUserByUsername { + @Test + void success() { + Members members = TestUtil.getMembers(); + when(memberRepository.findByUsername(any())).thenReturn(Optional.of(members)); + + UserDetails userDetails = customUserDetailsService.loadUserByUsername(members.getUsername()); + + assertEquals(members, userDetails); + } + + @Test + void notFound() { + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + assertThrows(UsernameNotFoundException.class, () -> customUserDetailsService.loadUserByUsername("username")); + } + } +} diff --git a/src/test/java/com/libraryman_api/security/services/LoginServiceTest.java b/src/test/java/com/libraryman_api/security/services/LoginServiceTest.java new file mode 100644 index 0000000..fe21696 --- /dev/null +++ b/src/test/java/com/libraryman_api/security/services/LoginServiceTest.java @@ -0,0 +1,71 @@ +package com.libraryman_api.security.services; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.member.Members; +import com.libraryman_api.security.jwt.JwtAuthenticationHelper; +import com.libraryman_api.security.model.LoginRequest; +import com.libraryman_api.security.model.LoginResponse; +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 org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link LoginService}. + */ +@ExtendWith(MockitoExtension.class) +class LoginServiceTest { + @Mock + private AuthenticationManager authenticationManager; + @Mock + private UserDetailsService userDetailsService; + @Mock + private JwtAuthenticationHelper jwtHelper; + @Mock + private MemberRepository memberRepository; + @InjectMocks + private LoginService loginService; + + @Nested + class Login { + @Test + void success() { + LoginRequest loginRequest = TestUtil.getLoginRequest(); + String token = "jwtToken"; + Members members = TestUtil.getMembers(); + + when(userDetailsService.loadUserByUsername(any())).thenReturn(members); + when(jwtHelper.generateToken(members)).thenReturn(token); + + LoginResponse response = loginService.login(loginRequest); + + assertEquals(token, response.getToken()); + } + + @Test + void badCredentials() { + when(authenticationManager.authenticate(any())).thenThrow(BadCredentialsException.class); + + assertThrows(BadCredentialsException.class, () -> loginService.login(TestUtil.getLoginRequest())); + } + + @Test + void userNotFound() { + when(userDetailsService.loadUserByUsername(any())).thenThrow(UsernameNotFoundException.class); + + assertThrows(UsernameNotFoundException.class, () -> loginService.login(TestUtil.getLoginRequest())); + } + } +} diff --git a/src/test/java/com/libraryman_api/security/services/SignupServiceTest.java b/src/test/java/com/libraryman_api/security/services/SignupServiceTest.java new file mode 100644 index 0000000..b9f5b97 --- /dev/null +++ b/src/test/java/com/libraryman_api/security/services/SignupServiceTest.java @@ -0,0 +1,171 @@ +package com.libraryman_api.security.services; + +import com.libraryman_api.TestUtil; +import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.member.MemberRepository; +import com.libraryman_api.member.Members; +import com.libraryman_api.member.Role; +import com.libraryman_api.security.config.PasswordEncoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests the {@link SignupService}. + */ +@ExtendWith(MockitoExtension.class) +class SignupServiceTest { + @Mock + private MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder = new PasswordEncoder(); + private SignupService signupService; + + @Captor + private ArgumentCaptor membersCaptor; + + @BeforeEach + void setup() { + signupService = new SignupService(memberRepository, passwordEncoder); + } + + @Nested + class Signup { + @Test + void success() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + signupService.signup(members); + + verify(memberRepository).save(membersCaptor.capture()); + Members savedMembers = membersCaptor.getValue(); + assertEquals(members.getUsername(), savedMembers.getUsername()); + assertEquals(members.getName(), savedMembers.getName()); + assertEquals(members.getEmail(), savedMembers.getEmail()); + assertEquals(Role.USER, savedMembers.getRole()); + assertNotNull(savedMembers.getMembershipDate()); + assertNotNull(savedMembers.getPassword()); + } + + @Test + void idFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.of(members)); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signup(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + + @Test + void usernameFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.of(members)); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signup(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + } + + @Nested + class SignupAdmin { + @Test + void success() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + signupService.signupAdmin(members); + + verify(memberRepository).save(membersCaptor.capture()); + Members savedMembers = membersCaptor.getValue(); + assertEquals(members.getUsername(), savedMembers.getUsername()); + assertEquals(members.getName(), savedMembers.getName()); + assertEquals(members.getEmail(), savedMembers.getEmail()); + assertEquals(Role.ADMIN, savedMembers.getRole()); + assertNotNull(savedMembers.getMembershipDate()); + assertNotNull(savedMembers.getPassword()); + } + + @Test + void idFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.of(members)); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signupAdmin(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + + @Test + void usernameFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.of(members)); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signupAdmin(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + } + + @Nested + class SignupLibrarian { + @Test + void success() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + signupService.signupLibrarian(members); + + verify(memberRepository).save(membersCaptor.capture()); + Members savedMembers = membersCaptor.getValue(); + assertEquals(members.getUsername(), savedMembers.getUsername()); + assertEquals(members.getName(), savedMembers.getName()); + assertEquals(members.getEmail(), savedMembers.getEmail()); + assertEquals(Role.LIBRARIAN, savedMembers.getRole()); + assertNotNull(savedMembers.getMembershipDate()); + assertNotNull(savedMembers.getPassword()); + } + + @Test + void idFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.of(members)); + when(memberRepository.findByUsername(any())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signupLibrarian(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + + @Test + void usernameFound() { + Members members = TestUtil.getMembers(); + when(memberRepository.findById(any())).thenReturn(Optional.empty()); + when(memberRepository.findByUsername(any())).thenReturn(Optional.of(members)); + + Exception exception = assertThrows(ResourceNotFoundException.class, () -> signupService.signupLibrarian(members)); + + assertEquals("User already Exists", exception.getMessage()); + } + } +} \ No newline at end of file