Skip to content

Commit

Permalink
Merge pull request #10 from alvinmarshall/3-endpoint-to-create-customer
Browse files Browse the repository at this point in the history
3 endpoint to create customer
  • Loading branch information
alvinmarshall authored Jul 28, 2024
2 parents be1a1b6 + 12b35a3 commit a77ed23
Show file tree
Hide file tree
Showing 20 changed files with 413 additions and 42 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/PR-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Pull Request Build
on:
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'adopt'
- name: Run Tests
run: ./gradlew build

- name: Generate JaCoCo Badge
uses: cicirello/jacoco-badge-generator@v2
with:
generate-branches-badge: true
jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv

- name: Publish Test Report
uses: mikepenz/action-junit-report@v4
if: success() || failure() # always run even if the previous step fails
with:
report_paths: '**/build/test-results/test/TEST-*.xml'

- name: Log coverage percentage
run: |
echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
echo "branch coverage = ${{ steps.jacoco.outputs.branches }}"
17 changes: 17 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'jacoco'
}

group = 'com.cheise_proj'
Expand Down Expand Up @@ -39,8 +40,24 @@ dependencies {
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor "org.projectlombok:lombok"
testImplementation 'org.projectlombok:lombok'

}

tasks.named('test') {
useJUnitPlatform()
}

test {
finalizedBy jacocoTestReport // report is always generated after tests run
}

jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
xml.required = true
csv.required = true
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
}
}
7 changes: 7 additions & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This tells lombok this directory is the root,
# no need to look somewhere else for java code.
config.stopBubbling = true
# This will add the @lombok.Generated annotation
# to all the code generated by Lombok,
# so it can be excluded from coverage by jacoco.
lombok.addLombokGeneratedAnnotation = true
16 changes: 16 additions & 0 deletions src/main/java/com/cheise_proj/auditing/Address.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -22,4 +23,19 @@ class Address {
private String country;
@Column(name = "zip_code")
private String zipCode;

@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;

static Address of(CustomerDto.CustomerAddress customerAddress) {
return Address.builder()
.city(customerAddress.city())
.streetAddress(customerAddress.streetAddress())
.stateCode(customerAddress.stateCode())
.country(customerAddress.country())
.zipCode(customerAddress.zipCode())
.build();
}

}
39 changes: 34 additions & 5 deletions src/main/java/com/cheise_proj/auditing/Customer.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
package com.cheise_proj.auditing;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.*;

import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Table(name = "customers")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
@Column(name = "first_name")
@NotBlank
@Column(name = "first_name", length = 100)
private String firstName;
@NonNull
@Column(name = "last_name")
@NotBlank
@Column(name = "last_name", length = 100)
private String lastName;
@NonNull
@Email
@NotBlank
@Column(name = "email_address")
private String emailAddress;

@ToString.Exclude
@OneToMany(mappedBy = "customer", orphanRemoval = true)
private Set<Address> addresses;

static Customer of(CustomerDto.CreateCustomer customer) {
Customer customerEntity = Customer.builder()
.firstName(customer.firstName())
.lastName(customer.lastName())
.emailAddress(customer.emailAddress())
.build();
customerEntity.setAddresses(customer.customerAddress());
return customerEntity;
}

void setAddresses(Set<CustomerDto.CustomerAddress> customerAddresses) {
if (customerAddresses == null) return;
this.addresses = (this.addresses == null) ? new LinkedHashSet<>() : this.addresses;
Set<Address> addressSet = customerAddresses.stream().map(Address::of).collect(Collectors.toSet());
this.addresses.addAll(addressSet);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/cheise_proj/auditing/CustomerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cheise_proj.auditing;

import jakarta.validation.Valid;
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.util.UriComponentsBuilder;

import java.net.URI;

@RestController
@RequestMapping("/customers")
class CustomerController {
private final CustomerService customerService;

CustomerController(CustomerService customerService) {
this.customerService = customerService;
}

@PostMapping
ResponseEntity<URI> createCustomer(@RequestBody @Valid CustomerDto.CreateCustomer input) {
Customer customer = customerService.createCustomer(input);
URI location = UriComponentsBuilder.fromPath("/customers/{id}").buildAndExpand(customer.getId()).toUri();
return ResponseEntity.created(location).build();
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/cheise_proj/auditing/CustomerDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.cheise_proj.auditing;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Builder;

import java.util.Set;

interface CustomerDto {
@Builder
record CreateCustomer(
@NotBlank @JsonProperty String firstName,
@NotBlank @JsonProperty String lastName,
@Email @JsonProperty("email") String emailAddress,
Set<CustomerAddress> customerAddress
) implements CustomerDto {
}

@Builder
record CustomerAddress(
@JsonProperty String streetAddress,
@JsonProperty String city,
@JsonProperty String stateCode,
@JsonProperty String country,
@JsonProperty String zipCode
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.cheise_proj.auditing;

import org.springframework.data.jpa.repository.JpaRepository;

interface CustomerRepository extends JpaRepository<Customer, Long> {
}
17 changes: 17 additions & 0 deletions src/main/java/com/cheise_proj/auditing/CustomerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.cheise_proj.auditing;

import org.springframework.stereotype.Service;

@Service
class CustomerService {
private final CustomerRepository customerRepository;

CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}

Customer createCustomer(CustomerDto.CreateCustomer customer) {
Customer newCustomer = Customer.of(customer);
return customerRepository.save(newCustomer);
}
}
4 changes: 2 additions & 2 deletions src/main/resources/db/migration/V1__1_customer_schema.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CREATE TABLE customers
(
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
first_name VARCHAR(255),
last_name VARCHAR(255),
first_name VARCHAR(100),
last_name VARCHAR(100),
email_address VARCHAR(255),
CONSTRAINT pk_customers PRIMARY KEY (id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ CREATE TABLE customer_address
state_code VARCHAR(255),
country VARCHAR(255),
zip_code VARCHAR(255),
customer_id BIGINT,
CONSTRAINT pk_customer_address PRIMARY KEY (id)
);
);

ALTER TABLE customer_address
ADD CONSTRAINT FK_CUSTOMER_ADDRESS_ON_CUSTOMER FOREIGN KEY (customer_id) REFERENCES customers (id);
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package com.cheise_proj.auditing;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Import(TestcontainersConfiguration.class)
@SpringBootTest
class AuditingApplicationTests {
class AuditingApplicationTests extends IntegrationTest {

@Test
void contextLoads() {
Expand Down
59 changes: 59 additions & 0 deletions src/test/java/com/cheise_proj/auditing/CustomerControllerIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.cheise_proj.auditing;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

@AutoConfigureMockMvc
@Slf4j
class CustomerControllerIT extends IntegrationTest {
@Autowired
private MockMvc mockMvc;
private final ObjectMapper objectMapper = new ObjectMapper();

@BeforeEach
void setUp() {
}

@AfterEach
void tearDown() {
}

@Test
void createCustomer_returns_201() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/customers")
.contentType(MediaType.APPLICATION_JSON)
.content(CustomerFixture.createCustomer(objectMapper))

).andExpectAll(MockMvcResultMatchers.status().isCreated())
.andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location")));
}

@Test
void createCustomer_With_Address_returns_201() 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")));
}

@Test
void createCustomer_returns_400() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/customers")
.contentType(MediaType.APPLICATION_JSON)
.content("{}")

).andExpect(MockMvcResultMatchers.status().isBadRequest())
.andDo(result -> log.info("result: {}", result.getResponse().getHeaderValue("location")));
}
}
36 changes: 36 additions & 0 deletions src/test/java/com/cheise_proj/auditing/CustomerFixture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.cheise_proj.auditing;

import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Set;

class CustomerFixture {
private CustomerFixture() {
}

static String createCustomer(ObjectMapper mapper) throws JsonProcessingException {
CustomerDto.CreateCustomer customerDto = CustomerDto.CreateCustomer.builder()
.firstName("Debra")
.lastName("Herman")
.emailAddress("debra@gmail.com")
.build();
return mapper.writeValueAsString(customerDto);
}

static String createCustomerWithAddress(ObjectMapper mapper) throws JsonProcessingException {
CustomerDto.CreateCustomer customerDto = CustomerDto.CreateCustomer.builder()
.firstName("Troy")
.lastName("Hahn")
.emailAddress("troy.hahn@gmail.com")
.customerAddress(Set.of(CustomerDto.CustomerAddress.builder()
.city("Risaberg")
.country("USA")
.streetAddress("942 Walker Street")
.stateCode("WV")
.zipCode("88742")
.build()))
.build();
return mapper.writeValueAsString(customerDto);
}
}
Loading

0 comments on commit a77ed23

Please sign in to comment.