diff --git a/pom.xml b/pom.xml index 5b31f78..4b03bdc 100644 --- a/pom.xml +++ b/pom.xml @@ -130,6 +130,18 @@ org.springframework.boot spring-boot-maven-plugin + + org.pitest + pitest-maven + 1.6.4 + + + org.pitest + pitest-junit5-plugin + 0.12 + + + diff --git a/src/main/java/com/bravo/user/controller/PaymentController.java b/src/main/java/com/bravo/user/controller/PaymentController.java new file mode 100644 index 0000000..65257a9 --- /dev/null +++ b/src/main/java/com/bravo/user/controller/PaymentController.java @@ -0,0 +1,57 @@ +package com.bravo.user.controller; + +import com.bravo.user.annotation.SwaggerController; +import com.bravo.user.exception.BadRequestException; +import com.bravo.user.model.dto.PaymentDto; +import com.bravo.user.model.filter.PaymentFilter; +import com.bravo.user.service.PaymentService; +import com.bravo.user.utility.PageUtil; +import com.bravo.user.validator.UserValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +@RequestMapping(value = "/payment") +@SwaggerController +public class PaymentController { + + private final UserValidator userValidator; + private final PaymentService paymentService; + + public PaymentController( + UserValidator userValidator, + PaymentService paymentService + ){ + this.userValidator = userValidator; + this.paymentService = paymentService; + } + + @GetMapping(value = "/retrieve") + @ResponseBody + public List retrieve( + final @RequestParam String userId, + final @RequestParam(required = false) Integer page, + final @RequestParam(required = false) Integer size, + final HttpServletResponse httpResponse + ){ + userValidator.validateId(userId); + final PageRequest pageRequest = PageUtil.createPageRequest(page, size); + return paymentService.retrievePaymentByUserId(userId, pageRequest, httpResponse); + } + + @PostMapping(value = "/retrieve") + @ResponseBody + public List retrieve( + final @RequestBody PaymentFilter filter, + final @RequestParam(required = false) Integer page, + final @RequestParam(required = false) Integer size, + final HttpServletResponse httpResponse + ){ + final PageRequest pageRequest = PageUtil.createPageRequest(page, size); + return paymentService.retrieve(filter, pageRequest, httpResponse); + } +} diff --git a/src/main/java/com/bravo/user/dao/model/Payment.java b/src/main/java/com/bravo/user/dao/model/Payment.java index 12eb7a5..15a31d4 100644 --- a/src/main/java/com/bravo/user/dao/model/Payment.java +++ b/src/main/java/com/bravo/user/dao/model/Payment.java @@ -6,10 +6,15 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; + +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; @Entity @Data +@Builder +@AllArgsConstructor @Table(name = "payment") public class Payment { diff --git a/src/main/java/com/bravo/user/dao/model/mapper/ResourceMapper.java b/src/main/java/com/bravo/user/dao/model/mapper/ResourceMapper.java index 4a78188..81efe58 100644 --- a/src/main/java/com/bravo/user/dao/model/mapper/ResourceMapper.java +++ b/src/main/java/com/bravo/user/dao/model/mapper/ResourceMapper.java @@ -48,7 +48,7 @@ public > List convertPayments(final T public PaymentDto convertPayment(final Payment payment){ final String cardNumber = payment.getCardNumber(); final PaymentDto dto = mapperFacade.map(payment, PaymentDto.class); - dto.setCardNumberLast4(cardNumber.substring(cardNumber.length() - 5)); + dto.setCardNumberLast4(cardNumber.substring(cardNumber.length() - 4)); return dto; } diff --git a/src/main/java/com/bravo/user/dao/repository/PaymentRepository.java b/src/main/java/com/bravo/user/dao/repository/PaymentRepository.java new file mode 100644 index 0000000..89e123f --- /dev/null +++ b/src/main/java/com/bravo/user/dao/repository/PaymentRepository.java @@ -0,0 +1,9 @@ +package com.bravo.user.dao.repository; + +import com.bravo.user.dao.model.Payment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +@Repository +public interface PaymentRepository extends JpaRepository, JpaSpecificationExecutor{} diff --git a/src/main/java/com/bravo/user/dao/specification/PaymentSpecification.java b/src/main/java/com/bravo/user/dao/specification/PaymentSpecification.java new file mode 100644 index 0000000..4df6f87 --- /dev/null +++ b/src/main/java/com/bravo/user/dao/specification/PaymentSpecification.java @@ -0,0 +1,31 @@ +package com.bravo.user.dao.specification; + +import com.bravo.user.dao.model.Payment; +import com.bravo.user.model.filter.PaymentFilter; +import lombok.EqualsAndHashCode; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; +import java.util.Set; + +@EqualsAndHashCode +public class PaymentSpecification extends AbstractSpecification{ + + private final PaymentFilter filter; + + public PaymentSpecification(final PaymentFilter filter) { + this.filter = filter; + } + + @Override + void doFilter( + Root root, + CriteriaQuery criteriaQuery, + CriteriaBuilder criteriaBuilder + ){ + applyStringFilterToFields(Set.of( + root.get("userId") + ), filter.getUserId()); + } +} diff --git a/src/main/java/com/bravo/user/model/dto/PaymentDto.java b/src/main/java/com/bravo/user/model/dto/PaymentDto.java index db32487..e693a48 100644 --- a/src/main/java/com/bravo/user/model/dto/PaymentDto.java +++ b/src/main/java/com/bravo/user/model/dto/PaymentDto.java @@ -1,12 +1,20 @@ package com.bravo.user.model.dto; import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class PaymentDto { private String id; + private String userId; private String cardNumberLast4; private Integer expiryMonth; private Integer expiryYear; diff --git a/src/main/java/com/bravo/user/model/filter/PaymentFilter.java b/src/main/java/com/bravo/user/model/filter/PaymentFilter.java new file mode 100644 index 0000000..e47c70a --- /dev/null +++ b/src/main/java/com/bravo/user/model/filter/PaymentFilter.java @@ -0,0 +1,14 @@ +package com.bravo.user.model.filter; + +import lombok.Builder; +import lombok.Data; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +public class PaymentFilter { + + private String userId; +} diff --git a/src/main/java/com/bravo/user/service/PaymentService.java b/src/main/java/com/bravo/user/service/PaymentService.java new file mode 100644 index 0000000..d109675 --- /dev/null +++ b/src/main/java/com/bravo/user/service/PaymentService.java @@ -0,0 +1,56 @@ +package com.bravo.user.service; + +import com.bravo.user.dao.model.Payment; +import com.bravo.user.dao.model.mapper.ResourceMapper; +import com.bravo.user.dao.repository.PaymentRepository; +import com.bravo.user.dao.specification.PaymentSpecification; +import com.bravo.user.model.dto.PaymentDto; +import com.bravo.user.model.filter.PaymentFilter; +import com.bravo.user.utility.PageUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +@Service +public class PaymentService { + + private static final Logger LOGGER = LoggerFactory.getLogger(PaymentService.class); + private final PaymentRepository paymentRepository; + private final ResourceMapper resourceMapper; + + public PaymentService( + PaymentRepository paymentRepository, + ResourceMapper resourceMapper + ){ + this.paymentRepository = paymentRepository; + this.resourceMapper = resourceMapper; + } + + public List retrievePaymentByUserId( + final String userId, + final PageRequest pageRequest, + final HttpServletResponse httpResponse + ){ + return retrieve(PaymentFilter.builder().userId(userId).build(), pageRequest, httpResponse); + } + + public List retrieve( + final PaymentFilter filter, + final PageRequest pageRequest, + final HttpServletResponse httpResponse + ){ + LOGGER.debug("Request to retrieve payment information being conducted... paymentFilter: {}", filter); + final PaymentSpecification specification = new PaymentSpecification(filter); + final Page paymentPage = paymentRepository.findAll(specification,pageRequest); + final List payments = resourceMapper.convertPayments(paymentPage.getContent()); + LOGGER.debug("Found {} payment(s)", payments.size()); + + PageUtil.updatePageHeaders(httpResponse, paymentPage, pageRequest); + return payments; + } +} diff --git a/src/main/java/com/bravo/user/utility/PageUtil.java b/src/main/java/com/bravo/user/utility/PageUtil.java index 15d7f85..5ac725d 100644 --- a/src/main/java/com/bravo/user/utility/PageUtil.java +++ b/src/main/java/com/bravo/user/utility/PageUtil.java @@ -10,6 +10,9 @@ public class PageUtil { private static final int DEFAULT_SIZE = 20; + public static PageRequest createPageRequest() { + return createPageRequest(null, null); + } public static PageRequest createPageRequest(final Integer page, final Integer size){ return createPageRequest(page, size, DEFAULT_SIZE); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index da45720..f17a26a 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -648,3 +648,12 @@ insert into address (id, user_id, line1, line2, city, state, zip) values ('42f33d30-f3f8-4743-a94e-4db11fdb747d', '008a4215-0b1d-445e-b655-a964039cbb5a', '412 Maple St', null, 'Dowagiac', 'Michigan', '49047'), ('579872ec-46f8-46b5-b809-d0724d965f0e', '00963d9b-f884-485e-9455-fcf30c6ac379', '237 Mountain Ter', 'Apt 10', 'Odenville', 'Alabama', '35120'), ('95a983d0-ba0e-4f30-afb6-667d4724b253', '00963d9b-f884-485e-9455-fcf30c6ac379', '107 Annettes Ct', null, 'Aydlett', 'North Carolina', '27916'); + + +insert into payment(id, user_id, card_number, expiry_month, expiry_year) values +('9k9v8x6v-9hir-8c6a-00s2-dp8922xhik','008a4215-0b1d-445e-b655-a964039cbb5a', '1234567890123456', '10', '2024'), +('y414leo3-3imm-nxp4-coev-r7rjytdmur','008a4215-0b1d-445e-b655-a964039cbb5a', '4567890123456789', '11', '2026'), +('7qzfn2fz-kugj-maq7-0rly-r9q8hi0mf7','00963d9b-f884-485e-9455-fcf30c6ac379', '2345678901234567', '07', '2025'), +('ad7yhtjn-qqlm-x1p4-ijdw-6dluiyysd2','00bed3ac-5f3c-4a2d-a67b-80376ea9f941', '3456789012345678', '03', '2025'), +('tzkif37a-vkx8-341h-hzka-pm4hj5svgi','0111d3ca-514b-4ae8-8f57-e85cca43fb1e', '5678901234567890', '07', '2025'), +('934g1gle-e26f-tnta-elos-k7misr3v36','01316816-0cb7-41c4-8424-8367294aea27', '6789012345678901', '03', '2025'); \ No newline at end of file diff --git a/src/test/java/com/bravo/user/controller/PaymentControllerTest.java b/src/test/java/com/bravo/user/controller/PaymentControllerTest.java new file mode 100644 index 0000000..042682f --- /dev/null +++ b/src/test/java/com/bravo/user/controller/PaymentControllerTest.java @@ -0,0 +1,114 @@ +package com.bravo.user.controller; + +import com.bravo.user.App; +import com.bravo.user.model.dto.PaymentDto; +import com.bravo.user.model.filter.PaymentFilter; +import com.bravo.user.service.PaymentService; +import com.bravo.user.utility.PageUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ContextConfiguration(classes = {App.class}) +@ExtendWith(SpringExtension.class) +@SpringBootTest() +@AutoConfigureMockMvc +public class PaymentControllerTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Autowired + private MockMvc mockMvc; + @MockBean + private PaymentService paymentService; + + private List payments; + private PaymentFilter paymentFilter; + + @BeforeEach + public void beforeEach(){ + this.payments = IntStream + .range(1,6) + .mapToObj(id -> PaymentDto.builder().id(Integer.toString(id)).build()) + .collect(Collectors.toList()); + + this.paymentFilter = PaymentFilter.builder().userId("1").build(); + } + + @Test + public void getRetrieveWithUserId() throws Exception { + when(paymentService + .retrievePaymentByUserId(anyString(), any(PageRequest.class), any(HttpServletResponse.class))) + .thenReturn(payments); + + final ResultActions result = this.mockMvc + .perform(get( "/payment/retrieve?userId=1")) + .andExpect(status().isOk()); + + for(int i = 0; i < payments.size(); i++) { + result.andExpect((jsonPath(String.format("$[%d].id", i)).value(payments.get(i).getId()))); + } + + final PageRequest pageRequest = PageUtil.createPageRequest(); + verify(paymentService).retrievePaymentByUserId( + eq("1"), eq(pageRequest), any(HttpServletResponse.class) + ); + } + + @Test + public void postRetrieveWithFilter() throws Exception { + when(paymentService.retrieve(any(PaymentFilter.class), any(PageRequest.class), any(HttpServletResponse.class))) + .thenReturn(payments); + + final String jsonResult = MAPPER.writeValueAsString(paymentFilter); + + final ResultActions result = this.mockMvc + .perform(post("/payment/retrieve") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonResult) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + for(int i = 0; i < payments.size(); i++) { + result.andExpect((jsonPath(String.format("$[%d].id", i)).value(payments.get(i).getId()))); + } + + final PageRequest pageRequest = PageUtil.createPageRequest(); + verify(paymentService).retrieve(eq(paymentFilter), eq(pageRequest), any(HttpServletResponse.class)); + } + + @Test + public void retrieveWithUserIdMissing() throws Exception { + this.mockMvc.perform(get("/payment/retrieve")).andExpect(status().isBadRequest()); + } + + @Test + public void retrieveWithEmptyUserId() throws Exception { + this.mockMvc.perform(get("/payment/retrieve?userId=")).andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/com/bravo/user/controller/UserControllerTest.java b/src/test/java/com/bravo/user/controller/UserControllerTest.java index 6f98aac..5b72c9f 100644 --- a/src/test/java/com/bravo/user/controller/UserControllerTest.java +++ b/src/test/java/com/bravo/user/controller/UserControllerTest.java @@ -41,7 +41,6 @@ class UserControllerTest { @Autowired private MockMvc mockMvc; - @MockBean private UserService userService; @@ -73,7 +72,7 @@ void getRetrieveWithName() throws Exception { result.andExpect(jsonPath(String.format("$[%d].id", i)).value(users.get(i).getId())); } - final PageRequest pageRequest = PageUtil.createPageRequest(null, null); + final PageRequest pageRequest = PageUtil.createPageRequest(); verify(userService).retrieveByName( eq("lucy"), eq(pageRequest), any(HttpServletResponse.class) ); diff --git a/src/test/java/com/bravo/user/service/PaymentServiceTest.java b/src/test/java/com/bravo/user/service/PaymentServiceTest.java new file mode 100644 index 0000000..5d06749 --- /dev/null +++ b/src/test/java/com/bravo/user/service/PaymentServiceTest.java @@ -0,0 +1,128 @@ +package com.bravo.user.service; + +import com.bravo.user.App; +import com.bravo.user.dao.model.Payment; +import com.bravo.user.dao.model.mapper.ResourceMapper; +import com.bravo.user.dao.repository.PaymentRepository; +import com.bravo.user.dao.specification.PaymentSpecification; +import com.bravo.user.model.dto.PaymentDto; +import com.bravo.user.model.filter.PaymentFilter; +import com.bravo.user.utility.PageUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ContextConfiguration(classes = {App.class}) +@ExtendWith(SpringExtension.class) +@SpringBootTest +public class PaymentServiceTest { + + private static final String PAGE_COUNT = "page-count"; + private static final String PAGE_NUMBER = "page-number"; + private static final String PAGE_SIZE = "page-size"; + @Autowired + private HttpServletResponse httpResponse; + @Autowired + private PaymentService paymentService; + @MockBean + private ResourceMapper resourceMapper; + @MockBean + private PaymentRepository paymentRepository; + + private List paymentDtos; + private PaymentFilter paymentFilter; + + @BeforeEach + public void beforeEach() { + final List ids = List.of(1, 2, 3, 4, 5, 6); + + this.paymentFilter = PaymentFilter.builder().userId("1").build(); + + this.paymentDtos = ids.stream() + .map(id -> PaymentDto.builder().id(Integer.toString(id)).build()) + .collect(Collectors.toList()); + when(resourceMapper.convertPayments(anyList())).thenReturn(paymentDtos); + + final List payments = ids.stream() + .map(id -> Payment.builder().id(Integer.toString(id)).build()) + .collect(Collectors.toList()); + + final Page mockPage = mock(Page.class); + when(mockPage.getContent()).thenReturn(payments); + when(mockPage.getTotalPages()).thenReturn(6); + + when(paymentRepository.findAll(any(PaymentSpecification.class), any(PageRequest.class))) + .thenReturn(mockPage); + } + + @Test + public void retrievePaymentByUserId() { + final String userId = "1"; + final PageRequest pageRequest = PageUtil.createPageRequest(); + final List results = paymentService.retrievePaymentByUserId(userId, pageRequest, httpResponse); + assertEquals(paymentDtos, results); + assertEquals("6", httpResponse.getHeader(PAGE_COUNT)); + assertEquals("1", httpResponse.getHeader(PAGE_NUMBER)); + assertEquals("20", httpResponse.getHeader(PAGE_SIZE)); + + final PaymentFilter filter = PaymentFilter.builder().userId("1").build(); + final PaymentSpecification specification = new PaymentSpecification(filter); + verify(paymentRepository).findAll(specification, pageRequest); + } + + @Test + public void retrievePaymentByUserIdWithPagination() { + final String userId = "1"; + final PageRequest pageRequest = PageUtil.createPageRequest(1, 6); + final List results = paymentService.retrievePaymentByUserId(userId, pageRequest, httpResponse); + assertEquals(paymentDtos, results); + assertEquals("6", httpResponse.getHeader(PAGE_COUNT)); + assertEquals("1", httpResponse.getHeader(PAGE_NUMBER)); + assertEquals("6", httpResponse.getHeader(PAGE_SIZE)); + + final PaymentFilter filter = PaymentFilter.builder().userId(userId).build(); + final PaymentSpecification specification = new PaymentSpecification(filter); + verify(paymentRepository).findAll(specification, pageRequest); + } + + @Test + public void retrieveWithFilter() { + final PageRequest pageRequest = PageUtil.createPageRequest(); + final List results = paymentService.retrieve(paymentFilter, pageRequest, httpResponse); + assertEquals(paymentDtos, results); + assertEquals("6", httpResponse.getHeader(PAGE_COUNT)); + assertEquals("1", httpResponse.getHeader(PAGE_NUMBER)); + assertEquals("20", httpResponse.getHeader(PAGE_SIZE)); + + final PaymentSpecification specification = new PaymentSpecification(paymentFilter); + verify(paymentRepository).findAll(specification, pageRequest); + } + + @Test + public void retrieveWithFilterWithPagination() { + final PageRequest pageRequest = PageUtil.createPageRequest(1, 6); + final List results = paymentService.retrieve(paymentFilter, pageRequest, httpResponse); + assertEquals(paymentDtos, results); + assertEquals("6", httpResponse.getHeader(PAGE_COUNT)); + assertEquals("1", httpResponse.getHeader(PAGE_NUMBER)); + assertEquals("6", httpResponse.getHeader(PAGE_SIZE)); + + final PaymentSpecification specification = new PaymentSpecification(paymentFilter); + verify(paymentRepository).findAll(specification, pageRequest); + } +} diff --git a/src/test/java/com/bravo/user/service/UserServiceTest.java b/src/test/java/com/bravo/user/service/UserServiceTest.java index 78043ec..47be7df 100644 --- a/src/test/java/com/bravo/user/service/UserServiceTest.java +++ b/src/test/java/com/bravo/user/service/UserServiceTest.java @@ -77,7 +77,7 @@ public void beforeEach(){ @Test public void retrieveByName() { final String input = "input"; - final PageRequest pageRequest = PageUtil.createPageRequest(null, null); + final PageRequest pageRequest = PageUtil.createPageRequest(); final List results = userService.retrieveByName(input, pageRequest, httpResponse); assertEquals(dtoUsers, results); assertEquals("9", httpResponse.getHeader("page-count"));