diff --git a/src/main/java/hng_java_boilerplate/config/WebSecurityConfig.java b/src/main/java/hng_java_boilerplate/config/WebSecurityConfig.java index 9af46f59e..d89a0fc9f 100644 --- a/src/main/java/hng_java_boilerplate/config/WebSecurityConfig.java +++ b/src/main/java/hng_java_boilerplate/config/WebSecurityConfig.java @@ -113,6 +113,7 @@ public SecurityFilterChain httpSecurity(HttpSecurity httpSecurity) throws Except "/api/v1/auth/logout", "/api/v1/organisations/**", + "/api/v1/newsletter-subscription/**", "/api/v1/payment/stripe/**", "/api/v1/accounts/**", "api/v1/auth/2fa/**", diff --git a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java index dc791ccff..92b9a3a7a 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java +++ b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java @@ -2,15 +2,13 @@ import hng_java_boilerplate.newsletter.dto.SubscribeRequest; import hng_java_boilerplate.newsletter.dto.SubscribeResponse; +import hng_java_boilerplate.newsletter.dto.SubscribersResponse; import hng_java_boilerplate.newsletter.service.NewsletterService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -23,4 +21,12 @@ public ResponseEntity subscribe(@RequestBody @Valid Subscribe return ResponseEntity.status(HttpStatus.CREATED) .body(newsletterService.subscribeToNewsletter(request)); } + + @GetMapping + public ResponseEntity getSubscribers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + SubscribersResponse response = newsletterService.getSubscribersResponse(page, size); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersDto.java b/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersDto.java new file mode 100644 index 000000000..3e1f8913d --- /dev/null +++ b/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersDto.java @@ -0,0 +1,18 @@ +package hng_java_boilerplate.newsletter.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class SubscribersDto { + private String id; + private String email; + private LocalDateTime subscribedAt; +} diff --git a/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersResponse.java b/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersResponse.java new file mode 100644 index 000000000..2e613a326 --- /dev/null +++ b/src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersResponse.java @@ -0,0 +1,20 @@ +package hng_java_boilerplate.newsletter.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class SubscribersResponse { + private List subscribers; + private int page; + private int size; + private long totalElements; + private int totalPages; +} diff --git a/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java b/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java index 6fb9f98d1..ad87e4b65 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java +++ b/src/main/java/hng_java_boilerplate/newsletter/entity/Newsletter.java @@ -1,8 +1,7 @@ package hng_java_boilerplate.newsletter.entity; import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -10,6 +9,9 @@ @Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder @Entity @Table(name = "newsletters") public class Newsletter { diff --git a/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java index 889fcbce9..35f7a91a3 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java +++ b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java @@ -3,15 +3,20 @@ import hng_java_boilerplate.exception.NotFoundException; import hng_java_boilerplate.newsletter.dto.SubscribeRequest; import hng_java_boilerplate.newsletter.dto.SubscribeResponse; +import hng_java_boilerplate.newsletter.dto.SubscribersDto; +import hng_java_boilerplate.newsletter.dto.SubscribersResponse; import hng_java_boilerplate.newsletter.entity.Newsletter; import hng_java_boilerplate.newsletter.repository.NewsletterRepository; import hng_java_boilerplate.user.entity.User; import hng_java_boilerplate.user.repository.UserRepository; import hng_java_boilerplate.user.serviceImpl.EmailServiceImpl; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -34,4 +39,40 @@ public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { return new SubscribeResponse(201, "subscription successful"); } + + public SubscribersResponse getSubscribersResponse(int page, int size) { + Pageable pageable = buildPageable(page, size); + Page newsletterPage = newsletterRepository.findAll(pageable); + List subscriberDto = mapNewslettersToSubscribers(newsletterPage.getContent()); + + return buildSubscribersResponse(newsletterPage, subscriberDto); + } + + private Pageable buildPageable(int page, int size) { + return PageRequest.of(page, size, Sort.by("createdAt").descending()); + } + + private List mapNewslettersToSubscribers(List newsletters) { + return newsletters.stream() + .map(newsletter -> { + User user = userRepository.findById(newsletter.getUserId()) + .orElseThrow(() -> new NotFoundException("User not found for subscription id: " + newsletter.getId())); + return SubscribersDto.builder() + .id(newsletter.getId()) + .email(user.getEmail()) + .subscribedAt(newsletter.getCreatedAt()) + .build(); + }) + .collect(Collectors.toList()); + } + + private SubscribersResponse buildSubscribersResponse(Page newsletterPage, List subscriberDtos) { + return SubscribersResponse.builder() + .subscribers(subscriberDtos) + .page(newsletterPage.getNumber()) + .size(newsletterPage.getSize()) + .totalElements(newsletterPage.getTotalElements()) + .totalPages(newsletterPage.getTotalPages()) + .build(); + } } diff --git a/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java new file mode 100644 index 000000000..01206c228 --- /dev/null +++ b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java @@ -0,0 +1,127 @@ +package hng_java_boilerplate.newsletter.service; + +import hng_java_boilerplate.exception.NotFoundException; +import hng_java_boilerplate.newsletter.dto.SubscribersResponse; +import hng_java_boilerplate.newsletter.entity.Newsletter; +import hng_java_boilerplate.newsletter.repository.NewsletterRepository; +import hng_java_boilerplate.user.entity.User; +import hng_java_boilerplate.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +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.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +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.*; + +@ExtendWith(MockitoExtension.class) +class NewsletterServiceTest { + + @Mock + private NewsletterRepository newsletterRepository; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private NewsletterService newsletterService; + + private Newsletter newsletter1; + private Newsletter newsletter2; + private User user1; + private User user2; + + @BeforeEach + void setUp() { + user1 = new User(); + user1.setId("user1"); + user1.setEmail("user1@example.com"); + + user2 = new User(); + user2.setId("user2"); + user2.setEmail("user2@example.com"); + + newsletter1 = Newsletter.builder() + .id("newsletter1") + .userId("user1") + .createdAt(LocalDateTime.of(2021, 1, 1, 0, 0)) + .build(); + + newsletter2 = Newsletter.builder() + .id("newsletter2") + .userId("user2") + .createdAt(LocalDateTime.of(2021, 2, 15, 12, 30)) + .build(); + } + + @Test + void getSubscribersResponse_shouldReturnCorrectResponse() { + int page = 0; + int size = 10; + + List newsletters = List.of(newsletter1, newsletter2); + Pageable pageable = PageRequest.of(page, size); + Page newsletterPage = new PageImpl<>(newsletters, pageable, newsletters.size()); + + when(newsletterRepository.findAll(any(Pageable.class))).thenReturn(newsletterPage); + when(userRepository.findById("user1")).thenReturn(Optional.of(user1)); + when(userRepository.findById("user2")).thenReturn(Optional.of(user2)); + + SubscribersResponse response = newsletterService.getSubscribersResponse(page, size); + + assertNotNull(response); + assertEquals(page, response.getPage()); + assertEquals(size, response.getSize()); + assertEquals(newsletters.size(), response.getTotalElements()); + assertEquals(1, response.getTotalPages()); + assertNotNull(response.getSubscribers()); + assertEquals(2, response.getSubscribers().size()); + + boolean foundNewsletter1 = response.getSubscribers().stream() + .anyMatch(dto -> dto.getId().equals("newsletter1") + && dto.getEmail().equals("user1@example.com") + && dto.getSubscribedAt().equals(newsletter1.getCreatedAt())); + boolean foundNewsletter2 = response.getSubscribers().stream() + .anyMatch(dto -> dto.getId().equals("newsletter2") + && dto.getEmail().equals("user2@example.com") + && dto.getSubscribedAt().equals(newsletter2.getCreatedAt())); + + assertTrue(foundNewsletter1); + assertTrue(foundNewsletter2); + + verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); + verify(userRepository, times(1)).findById("user1"); + verify(userRepository, times(1)).findById("user2"); + } + + @Test + void getSubscribersResponse_shouldThrowNotFoundException_whenUserNotFound() { + int page = 0; + int size = 10; + + List newsletters = List.of(newsletter1); + Pageable pageable = PageRequest.of(page, size); + Page newsletterPage = new PageImpl<>(newsletters, pageable, newsletters.size()); + + when(newsletterRepository.findAll(any(Pageable.class))).thenReturn(newsletterPage); + when(userRepository.findById("user1")).thenReturn(Optional.empty()); + + NotFoundException exception = assertThrows(NotFoundException.class, + () -> newsletterService.getSubscribersResponse(page, size)); + + assertEquals("User not found for subscription id: " + newsletter1.getId(), exception.getMessage()); + verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); + verify(userRepository, times(1)).findById("user1"); + } +}