Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

trello ticket 13 - candidate-sample-payment-addition #32

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.6.4</version>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>0.12</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

Expand Down
57 changes: 57 additions & 0 deletions src/main/java/com/bravo/user/controller/PaymentController.java
Original file line number Diff line number Diff line change
@@ -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<PaymentDto> 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<PaymentDto> 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);
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/bravo/user/dao/model/Payment.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public <T extends Collection<Payment>> List<PaymentDto> 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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Payment, String>, JpaSpecificationExecutor<Payment>{}
Original file line number Diff line number Diff line change
@@ -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<Payment>{

private final PaymentFilter filter;

public PaymentSpecification(final PaymentFilter filter) {
this.filter = filter;
}

@Override
void doFilter(
Root<Payment> root,
CriteriaQuery<?> criteriaQuery,
CriteriaBuilder criteriaBuilder
){
applyStringFilterToFields(Set.of(
root.get("userId")
), filter.getUserId());
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/bravo/user/model/dto/PaymentDto.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/bravo/user/model/filter/PaymentFilter.java
Original file line number Diff line number Diff line change
@@ -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;
}
56 changes: 56 additions & 0 deletions src/main/java/com/bravo/user/service/PaymentService.java
Original file line number Diff line number Diff line change
@@ -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<PaymentDto> retrievePaymentByUserId(
final String userId,
final PageRequest pageRequest,
final HttpServletResponse httpResponse
){
return retrieve(PaymentFilter.builder().userId(userId).build(), pageRequest, httpResponse);
}

public List<PaymentDto> 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<Payment> paymentPage = paymentRepository.findAll(specification,pageRequest);
final List<PaymentDto> payments = resourceMapper.convertPayments(paymentPage.getContent());
LOGGER.debug("Found {} payment(s)", payments.size());

PageUtil.updatePageHeaders(httpResponse, paymentPage, pageRequest);
return payments;
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/bravo/user/utility/PageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class PageUtil {

private static final int DEFAULT_SIZE = 20;

public static PageRequest createPageRequest() {
return createPageRequest(null, null);
}
Comment on lines +13 to +15

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it is only used for test code. In general, having nulls can create additional checks that need to happen in code. Since this is only in test, could the other initializer be used with hardcoded values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was more of a code cleanup effort from having null values in tests to acquire default values from the PageUtil (page 1, size 20). Just a personal choice, but the original call could still be used, and this removed.

public static PageRequest createPageRequest(final Integer page, final Integer size){
return createPageRequest(page, size, DEFAULT_SIZE);
}
Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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');
114 changes: 114 additions & 0 deletions src/test/java/com/bravo/user/controller/PaymentControllerTest.java
Original file line number Diff line number Diff line change
@@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to see mockmvc. That is often missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, it's been a little while since I have done JUnits and Mockito, so I am trying to do more learning and example coding in my free-time.

@MockBean
private PaymentService paymentService;

private List<PaymentDto> 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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the throws needed here?

Copy link
Contributor Author

@ers020 ers020 Aug 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is to cover the .perform, and result.andExpect calls as both throw exceptions. I could wrap these into a try/catch block, but this makes the code cleaner.

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);
Comment on lines +84 to +86

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which layers are being tested with this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the flow of the Controller, as the mock is designed to merely mock the results of calling the Service class.


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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

Expand Down Expand Up @@ -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)
);
Expand Down
Loading