From f3004ed617eddc0dc25559e0194a2f902597c772 Mon Sep 17 00:00:00 2001 From: alvinmarshall Date: Sun, 28 Jul 2024 09:51:20 -0400 Subject: [PATCH 1/2] chore: add fetch customers endpoint See also: #1, #2, #3 --- .../com/cheise_proj/auditing/Address.java | 11 +++++-- .../com/cheise_proj/auditing/Customer.java | 10 +++--- .../auditing/CustomerController.java | 22 ++++++++++--- .../com/cheise_proj/auditing/CustomerDto.java | 31 ++++++++++++++++++- .../auditing/CustomerRepository.java | 6 ++++ .../cheise_proj/auditing/CustomerService.java | 7 +++++ src/main/resources/application.yml | 3 +- .../auditing/CustomerControllerIT.java | 14 +++++++++ .../auditing/CustomerServiceTest.java | 31 ++++++++++++++----- 9 files changed, 113 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/cheise_proj/auditing/Address.java b/src/main/java/com/cheise_proj/auditing/Address.java index 20c8b1b..dc2eead 100644 --- a/src/main/java/com/cheise_proj/auditing/Address.java +++ b/src/main/java/com/cheise_proj/auditing/Address.java @@ -1,5 +1,6 @@ package com.cheise_proj.auditing; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.*; @@ -24,17 +25,23 @@ class Address { @Column(name = "zip_code") private String zipCode; - @ManyToOne + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") + @ToString.Exclude private Customer customer; - static Address of(CustomerDto.CustomerAddress customerAddress) { + @Column(name = "customer_id", insertable = false, updatable = false) + private Long customerId; + + static Address of(CustomerDto.CustomerAddress customerAddress, Customer customer) { return Address.builder() .city(customerAddress.city()) .streetAddress(customerAddress.streetAddress()) .stateCode(customerAddress.stateCode()) .country(customerAddress.country()) .zipCode(customerAddress.zipCode()) + .customer(customer) .build(); } diff --git a/src/main/java/com/cheise_proj/auditing/Customer.java b/src/main/java/com/cheise_proj/auditing/Customer.java index b9def59..df04e58 100644 --- a/src/main/java/com/cheise_proj/auditing/Customer.java +++ b/src/main/java/com/cheise_proj/auditing/Customer.java @@ -33,23 +33,21 @@ class Customer { private String emailAddress; @ToString.Exclude - @OneToMany(mappedBy = "customer", orphanRemoval = true) + @OneToMany(mappedBy = "customer", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Set
addresses; static Customer of(CustomerDto.CreateCustomer customer) { - Customer customerEntity = Customer.builder() + return Customer.builder() .firstName(customer.firstName()) .lastName(customer.lastName()) .emailAddress(customer.emailAddress()) .build(); - customerEntity.setAddresses(customer.customerAddress()); - return customerEntity; } - void setAddresses(Set customerAddresses) { + void setAddresses(Set customerAddresses, Customer customer) { if (customerAddresses == null) return; this.addresses = (this.addresses == null) ? new LinkedHashSet<>() : this.addresses; - Set
addressSet = customerAddresses.stream().map(Address::of).collect(Collectors.toSet()); + Set
addressSet = customerAddresses.stream().map(customerAddress -> Address.of(customerAddress, customer)).collect(Collectors.toSet()); this.addresses.addAll(addressSet); } } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerController.java b/src/main/java/com/cheise_proj/auditing/CustomerController.java index fe5addb..d8d25ad 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerController.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerController.java @@ -1,14 +1,15 @@ package com.cheise_proj.auditing; import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; 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.*; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; +import java.util.List; +import java.util.Map; @RestController @RequestMapping("/customers") @@ -25,4 +26,17 @@ ResponseEntity createCustomer(@RequestBody @Valid CustomerDto.CreateCustome URI location = UriComponentsBuilder.fromPath("/customers/{id}").buildAndExpand(customer.getId()).toUri(); return ResponseEntity.created(location).build(); } + + @GetMapping + ResponseEntity index( + @RequestParam(required = false, defaultValue = "0") int page, + @RequestParam(required = false, defaultValue = "10") int size + ) { + Page customerPage = customerService.getCustomers(PageRequest.of(page, size)); + List customerList = customerPage.getContent().stream().map(CustomerDto::toGetCustomer).toList(); + Map customers = Map.of("customers", customerList, "total", customerPage.getTotalElements()); + return ResponseEntity.ok(customers); + } + + } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerDto.java b/src/main/java/com/cheise_proj/auditing/CustomerDto.java index ae34c42..e02cd42 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerDto.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerDto.java @@ -6,6 +6,7 @@ import lombok.Builder; import java.util.Set; +import java.util.stream.Collectors; interface CustomerDto { @Builder @@ -13,7 +14,7 @@ record CreateCustomer( @NotBlank @JsonProperty String firstName, @NotBlank @JsonProperty String lastName, @Email @JsonProperty("email") String emailAddress, - Set customerAddress + @JsonProperty Set customerAddress ) implements CustomerDto { } @@ -26,4 +27,32 @@ record CustomerAddress( @JsonProperty String zipCode ) { } + + @Builder + record GetCustomer( + @JsonProperty String firstName, + @JsonProperty String lastName, + @JsonProperty("email") String emailAddress, + Set customerAddress + ) implements CustomerDto { + } + + static GetCustomer toGetCustomer(Customer customer) { + Set customerAddresses = null; + if (customer.getAddresses() != null) { + customerAddresses = customer.getAddresses().stream().map(address -> CustomerAddress.builder() + .zipCode(address.getZipCode()) + .city(address.getCity()) + .country(address.getCountry()) + .stateCode(address.getStateCode()) + .streetAddress(address.getStreetAddress()) + .build()).collect(Collectors.toSet()); + } + return GetCustomer.builder() + .firstName(customer.getFirstName()) + .lastName(customer.getLastName()) + .emailAddress(customer.getEmailAddress()) + .customerAddress(customerAddresses) + .build(); + } } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerRepository.java b/src/main/java/com/cheise_proj/auditing/CustomerRepository.java index ec7259f..ce98ec2 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerRepository.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerRepository.java @@ -1,6 +1,12 @@ package com.cheise_proj.auditing; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; interface CustomerRepository extends JpaRepository { + @EntityGraph(attributePaths = "addresses") + @Override + Page findAll(Pageable pageable); } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerService.java b/src/main/java/com/cheise_proj/auditing/CustomerService.java index c3ab5bc..e52c675 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerService.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerService.java @@ -1,5 +1,7 @@ package com.cheise_proj.auditing; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @Service @@ -12,6 +14,11 @@ class CustomerService { Customer createCustomer(CustomerDto.CreateCustomer customer) { Customer newCustomer = Customer.of(customer); + newCustomer.setAddresses(customer.customerAddress(), newCustomer); return customerRepository.save(newCustomer); } + + Page getCustomers(Pageable pageable) { + return customerRepository.findAll(pageable); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eeaeac6..7a16f7a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,5 @@ spring: application: name: auditing - + jpa: + open-in-view: false diff --git a/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java b/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java index 7be0b17..d419033 100644 --- a/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java +++ b/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java @@ -56,4 +56,18 @@ void createCustomer_returns_400() throws Exception { ).andExpect(MockMvcResultMatchers.status().isBadRequest()) .andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location"))); } + + @Test + void getCustomers_With_Address_returns_200() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(CustomerFixture.createCustomerWithAddress(objectMapper)) + ).andExpectAll(MockMvcResultMatchers.status().isCreated()) + .andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location"))); + + mockMvc.perform(MockMvcRequestBuilders.get("/customers") + .contentType(MediaType.APPLICATION_JSON) + ).andExpectAll(MockMvcResultMatchers.status().isOk()) + .andDo(result -> log.info("result: {}", result.getResponse().getContentAsString())); + } } \ No newline at end of file diff --git a/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java b/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java index 82a12f6..45625bf 100644 --- a/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java +++ b/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java @@ -6,7 +6,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; 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 java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -37,7 +41,7 @@ void createCustomer() { .emailAddress("claribel.zieme@gmail.com") .build(); sut.createCustomer(customerDto); - Mockito.verify(customerRepository,Mockito.atMostOnce()).save(customerArgumentCaptor.capture()); + Mockito.verify(customerRepository, Mockito.atMostOnce()).save(customerArgumentCaptor.capture()); Customer customer = customerArgumentCaptor.getValue(); assertNotNull(customer); assertEquals("Claribel", customer.getFirstName()); @@ -52,17 +56,28 @@ void createCustomerWithAddress() { .lastName("Zieme") .emailAddress("claribel.zieme@gmail.com") .customerAddress(Set.of(CustomerDto.CustomerAddress.builder() - .city("Risaberg") - .country("USA") - .streetAddress("942 Walker Street") - .stateCode("WV") - .zipCode("88742") + .city("Risaberg") + .country("USA") + .streetAddress("942 Walker Street") + .stateCode("WV") + .zipCode("88742") .build())) .build(); sut.createCustomer(customerDto); - Mockito.verify(customerRepository,Mockito.atMostOnce()).save(customerArgumentCaptor.capture()); + Mockito.verify(customerRepository, Mockito.atMostOnce()).save(customerArgumentCaptor.capture()); Customer customer = customerArgumentCaptor.getValue(); assertNotNull(customer); - assertEquals(1,customer.getAddresses().size()); + assertEquals(1, customer.getAddresses().size()); + } + + @Test + void getCustomers() { + List customerList = List.of(Customer.builder().id(1L).build()); + Mockito.when(customerRepository.findAll(ArgumentMatchers.any(Pageable.class))) + .thenReturn(new PageImpl<>(customerList)); + Page customerPage = sut.getCustomers(Pageable.ofSize(1)); + assertNotNull(customerPage); + assertEquals(1, customerPage.getTotalElements()); + assertEquals(1, customerPage.getContent().size()); } } \ No newline at end of file From dd32dcc5f04f4e9763825549c868ed35827a120d Mon Sep 17 00:00:00 2001 From: alvinmarshall Date: Sun, 28 Jul 2024 10:41:14 -0400 Subject: [PATCH 2/2] chore: add fetch customer implementation --- .../auditing/CustomerController.java | 5 ++++ .../com/cheise_proj/auditing/CustomerDto.java | 7 +++++ .../auditing/CustomerExceptionAdviser.java | 18 ++++++++++++ .../cheise_proj/auditing/CustomerService.java | 7 +++++ .../auditing/CustomerControllerIT.java | 28 +++++++++++++++++++ .../CustomerExceptionAdviserTest.java | 22 +++++++++++++++ .../auditing/CustomerServiceTest.java | 20 +++++++++++++ 7 files changed, 107 insertions(+) create mode 100644 src/main/java/com/cheise_proj/auditing/CustomerExceptionAdviser.java create mode 100644 src/test/java/com/cheise_proj/auditing/CustomerExceptionAdviserTest.java diff --git a/src/main/java/com/cheise_proj/auditing/CustomerController.java b/src/main/java/com/cheise_proj/auditing/CustomerController.java index d8d25ad..7c5d564 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerController.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerController.java @@ -38,5 +38,10 @@ ResponseEntity index( return ResponseEntity.ok(customers); } + @GetMapping("{id}") + ResponseEntity getCustomer(@PathVariable("id") Long id) { + Customer customer = customerService.getCustomer(id); + return ResponseEntity.ok(CustomerDto.toCustomer(customer)); + } } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerDto.java b/src/main/java/com/cheise_proj/auditing/CustomerDto.java index e02cd42..bd6bbe6 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerDto.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerDto.java @@ -55,4 +55,11 @@ static GetCustomer toGetCustomer(Customer customer) { .customerAddress(customerAddresses) .build(); } + static GetCustomer toCustomer(Customer customer) { + return GetCustomer.builder() + .firstName(customer.getFirstName()) + .lastName(customer.getLastName()) + .emailAddress(customer.getEmailAddress()) + .build(); + } } diff --git a/src/main/java/com/cheise_proj/auditing/CustomerExceptionAdviser.java b/src/main/java/com/cheise_proj/auditing/CustomerExceptionAdviser.java new file mode 100644 index 0000000..a750738 --- /dev/null +++ b/src/main/java/com/cheise_proj/auditing/CustomerExceptionAdviser.java @@ -0,0 +1,18 @@ +package com.cheise_proj.auditing; + +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +class CustomerExceptionAdviser { + + @ExceptionHandler(value = {EntityNotFoundException.class}) + @ResponseStatus(value = HttpStatus.NOT_FOUND) + public ProblemDetail resourceNotFoundException(EntityNotFoundException ex) { + return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + } +} diff --git a/src/main/java/com/cheise_proj/auditing/CustomerService.java b/src/main/java/com/cheise_proj/auditing/CustomerService.java index e52c675..4787509 100644 --- a/src/main/java/com/cheise_proj/auditing/CustomerService.java +++ b/src/main/java/com/cheise_proj/auditing/CustomerService.java @@ -1,5 +1,6 @@ package com.cheise_proj.auditing; +import jakarta.persistence.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -21,4 +22,10 @@ Customer createCustomer(CustomerDto.CreateCustomer customer) { Page getCustomers(Pageable pageable) { return customerRepository.findAll(pageable); } + + public Customer getCustomer(Long id) { + return customerRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Customer with id %d not found".formatted(id))); + } + } diff --git a/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java b/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java index d419033..7e24416 100644 --- a/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java +++ b/src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java @@ -70,4 +70,32 @@ void getCustomers_With_Address_returns_200() throws Exception { ).andExpectAll(MockMvcResultMatchers.status().isOk()) .andDo(result -> log.info("result: {}", result.getResponse().getContentAsString())); } + + @Test + void getCustomer_by_id_returns_200() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(CustomerFixture.createCustomerWithAddress(objectMapper)) + ).andExpectAll(MockMvcResultMatchers.status().isCreated()) + .andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location"))); + + mockMvc.perform(MockMvcRequestBuilders.get("/customers/1") + .contentType(MediaType.APPLICATION_JSON) + ).andExpectAll(MockMvcResultMatchers.status().isOk()) + .andDo(result -> log.info("result: {}", result.getResponse().getContentAsString())); + } + + @Test + void getCustomer_by_id_returns_404() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(CustomerFixture.createCustomerWithAddress(objectMapper)) + ).andExpectAll(MockMvcResultMatchers.status().isCreated()) + .andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location"))); + + mockMvc.perform(MockMvcRequestBuilders.get("/customers/10") + .contentType(MediaType.APPLICATION_JSON) + ).andExpectAll(MockMvcResultMatchers.status().isNotFound()) + .andDo(result -> log.info("result: {}", result.getResponse().getContentAsString())); + } } \ No newline at end of file diff --git a/src/test/java/com/cheise_proj/auditing/CustomerExceptionAdviserTest.java b/src/test/java/com/cheise_proj/auditing/CustomerExceptionAdviserTest.java new file mode 100644 index 0000000..49cece9 --- /dev/null +++ b/src/test/java/com/cheise_proj/auditing/CustomerExceptionAdviserTest.java @@ -0,0 +1,22 @@ +package com.cheise_proj.auditing; + +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ProblemDetail; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ExtendWith(MockitoExtension.class) +class CustomerExceptionAdviserTest { + @InjectMocks + private CustomerExceptionAdviser sut; + + @Test + void resourceNotFoundException() { + ProblemDetail notFound = sut.resourceNotFoundException(new EntityNotFoundException("not found")); + assertNotNull(notFound); + } +} \ No newline at end of file diff --git a/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java b/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java index 45625bf..7671a23 100644 --- a/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java +++ b/src/test/java/com/cheise_proj/auditing/CustomerServiceTest.java @@ -1,6 +1,8 @@ package com.cheise_proj.auditing; +import jakarta.persistence.EntityNotFoundException; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,6 +13,7 @@ import org.springframework.data.domain.Pageable; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -80,4 +83,21 @@ void getCustomers() { assertEquals(1, customerPage.getTotalElements()); assertEquals(1, customerPage.getContent().size()); } + + @Test + void getCustomer() { + Mockito.when(customerRepository.findById(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(Customer.builder().id(1L).build())); + Customer customer = sut.getCustomer(1L); + assertNotNull(customer); + } + + @Test + void getCustomer_throw_if_customer_not_found() { + EntityNotFoundException exception = Assertions.assertThrows( + EntityNotFoundException.class, + () -> sut.getCustomer(1L) + ); + assertEquals("Customer with id 1 not found", exception.getMessage()); + } } \ No newline at end of file