From 0877066e89a709859f19971e514e738b8f7c09cf Mon Sep 17 00:00:00 2001 From: scepter Date: Fri, 28 Feb 2025 13:51:31 +0100 Subject: [PATCH 1/4] feat: Fetch all newsletter subscribers --- .../controller/NewsletterController.java | 14 +- .../newsletter/dto/SubscribersDto.java | 18 +++ .../newsletter/dto/SubscribersResponse.java | 20 +++ .../newsletter/entity/Newsletter.java | 6 +- .../newsletter/service/NewsletterService.java | 47 ++++++ .../service/NewsletterServiceTest.java | 148 ++++++++++++++++++ 6 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersDto.java create mode 100644 src/main/java/hng_java_boilerplate/newsletter/dto/SubscribersResponse.java create mode 100644 src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java 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..b2af45b1b 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("/subscribers") + public ResponseEntity getSubscribers( + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer 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..8b039ab5a 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 @@ -20,6 +25,9 @@ public class NewsletterService { private final UserRepository userRepository; private final EmailServiceImpl emailService; + private static final int DEFAULT_PAGE = 0; + private static final int DEFAULT_SIZE = 20; + public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new NotFoundException("user not found with email")); @@ -34,4 +42,43 @@ public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { return new SubscribeResponse(201, "subscription successful"); } + + public SubscribersResponse getSubscribersResponse(Integer page, Integer size) { + int effectivePage = (page == null || page < 0) ? DEFAULT_PAGE : page; + int effectiveSize = (size == null || size <= 0) ? DEFAULT_SIZE : size; + + Pageable pageable = buildPageable(effectivePage, effectiveSize); + Page newsletterPage = newsletterRepository.findAll(pageable); + List subscriberDtos = mapNewslettersToSubscribers(newsletterPage.getContent()); + + return buildSubscribersResponse(newsletterPage, subscriberDtos); + } + + 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..94a3436a5 --- /dev/null +++ b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java @@ -0,0 +1,148 @@ +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 = 20; + + 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 = 20; + + 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"); + } + + @Test + void getSubscribersResponse_shouldUseDefaultValues_whenParametersAreInvalid() { + int defaultPage = 0; + int defaultSize = 20; + + List newsletters = List.of(newsletter1); + Pageable pageable = PageRequest.of(defaultPage, defaultSize); + Page newsletterPage = new PageImpl<>(newsletters, pageable, newsletters.size()); + + when(newsletterRepository.findAll(any(Pageable.class))).thenReturn(newsletterPage); + when(userRepository.findById("user1")).thenReturn(Optional.of(user1)); + + SubscribersResponse response = newsletterService.getSubscribersResponse(null, null); + + assertNotNull(response); + assertEquals(defaultPage, response.getPage()); + assertEquals(defaultSize, response.getSize()); + verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); + verify(userRepository, times(1)).findById("user1"); + } +} \ No newline at end of file From 3074abc9fc5cf26bc61773b7566f80ca296140fa Mon Sep 17 00:00:00 2001 From: scepter Date: Fri, 28 Feb 2025 23:13:01 +0100 Subject: [PATCH 2/4] feat: Fetch all newsletter subscribers --- .../java/hng_java_boilerplate/config/WebSecurityConfig.java | 1 + .../newsletter/controller/NewsletterController.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 b2af45b1b..4a8ae7e4a 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java +++ b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java @@ -22,7 +22,7 @@ public ResponseEntity subscribe(@RequestBody @Valid Subscribe .body(newsletterService.subscribeToNewsletter(request)); } - @GetMapping("/subscribers") + @GetMapping public ResponseEntity getSubscribers( @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer size) { From cc692b051299ec5cb5e13703c93e85d75de85d58 Mon Sep 17 00:00:00 2001 From: scepter Date: Sat, 1 Mar 2025 12:21:56 +0100 Subject: [PATCH 3/4] refactor: remove pagination defaults from NewsletterService service and set them in NewsletterController and also updated tests to reflect these changes --- .../controller/NewsletterController.java | 6 ++--- .../newsletter/service/NewsletterService.java | 14 +++-------- .../service/NewsletterServiceTest.java | 25 ++----------------- 3 files changed, 9 insertions(+), 36 deletions(-) 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 4a8ae7e4a..92b9a3a7a 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java +++ b/src/main/java/hng_java_boilerplate/newsletter/controller/NewsletterController.java @@ -23,9 +23,9 @@ public ResponseEntity subscribe(@RequestBody @Valid Subscribe } @GetMapping - public ResponseEntity getSubscribers( - @RequestParam(required = false) Integer page, - @RequestParam(required = false) Integer size) { + 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/service/NewsletterService.java b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java index 8b039ab5a..35f7a91a3 100644 --- a/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java +++ b/src/main/java/hng_java_boilerplate/newsletter/service/NewsletterService.java @@ -25,9 +25,6 @@ public class NewsletterService { private final UserRepository userRepository; private final EmailServiceImpl emailService; - private static final int DEFAULT_PAGE = 0; - private static final int DEFAULT_SIZE = 20; - public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new NotFoundException("user not found with email")); @@ -43,15 +40,12 @@ public SubscribeResponse subscribeToNewsletter(SubscribeRequest request) { return new SubscribeResponse(201, "subscription successful"); } - public SubscribersResponse getSubscribersResponse(Integer page, Integer size) { - int effectivePage = (page == null || page < 0) ? DEFAULT_PAGE : page; - int effectiveSize = (size == null || size <= 0) ? DEFAULT_SIZE : size; - - Pageable pageable = buildPageable(effectivePage, effectiveSize); + public SubscribersResponse getSubscribersResponse(int page, int size) { + Pageable pageable = buildPageable(page, size); Page newsletterPage = newsletterRepository.findAll(pageable); - List subscriberDtos = mapNewslettersToSubscribers(newsletterPage.getContent()); + List subscriberDto = mapNewslettersToSubscribers(newsletterPage.getContent()); - return buildSubscribersResponse(newsletterPage, subscriberDtos); + return buildSubscribersResponse(newsletterPage, subscriberDto); } private Pageable buildPageable(int page, int size) { diff --git a/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java index 94a3436a5..84cdabf7d 100644 --- a/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java +++ b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java @@ -68,7 +68,7 @@ void setUp() { @Test void getSubscribersResponse_shouldReturnCorrectResponse() { int page = 0; - int size = 20; + int size = 10; List newsletters = List.of(newsletter1, newsletter2); Pageable pageable = PageRequest.of(page, size); @@ -108,7 +108,7 @@ void getSubscribersResponse_shouldReturnCorrectResponse() { @Test void getSubscribersResponse_shouldThrowNotFoundException_whenUserNotFound() { int page = 0; - int size = 20; + int size = 10; List newsletters = List.of(newsletter1); Pageable pageable = PageRequest.of(page, size); @@ -124,25 +124,4 @@ void getSubscribersResponse_shouldThrowNotFoundException_whenUserNotFound() { verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); verify(userRepository, times(1)).findById("user1"); } - - @Test - void getSubscribersResponse_shouldUseDefaultValues_whenParametersAreInvalid() { - int defaultPage = 0; - int defaultSize = 20; - - List newsletters = List.of(newsletter1); - Pageable pageable = PageRequest.of(defaultPage, defaultSize); - Page newsletterPage = new PageImpl<>(newsletters, pageable, newsletters.size()); - - when(newsletterRepository.findAll(any(Pageable.class))).thenReturn(newsletterPage); - when(userRepository.findById("user1")).thenReturn(Optional.of(user1)); - - SubscribersResponse response = newsletterService.getSubscribersResponse(null, null); - - assertNotNull(response); - assertEquals(defaultPage, response.getPage()); - assertEquals(defaultSize, response.getSize()); - verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); - verify(userRepository, times(1)).findById("user1"); - } } \ No newline at end of file From 931bd28d7329a926268c823ab299da4a88efbdaa Mon Sep 17 00:00:00 2001 From: scepter Date: Sat, 1 Mar 2025 12:33:55 +0100 Subject: [PATCH 4/4] refactor: remove pagination defaults from NewsletterService service and set them in NewsletterController and also updated tests to reflect these changes --- .../newsletter/service/NewsletterServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java index 84cdabf7d..01206c228 100644 --- a/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java +++ b/src/test/java/hng_java_boilerplate/newsletter/service/NewsletterServiceTest.java @@ -124,4 +124,4 @@ void getSubscribersResponse_shouldThrowNotFoundException_whenUserNotFound() { verify(newsletterRepository, times(1)).findAll(any(Pageable.class)); verify(userRepository, times(1)).findById("user1"); } -} \ No newline at end of file +}