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

3 endpoint to create customer #10

Merged
merged 8 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("[email protected]")
.build();
return mapper.writeValueAsString(customerDto);
}

static String createCustomerWithAddress(ObjectMapper mapper) throws JsonProcessingException {
CustomerDto.CreateCustomer customerDto = CustomerDto.CreateCustomer.builder()
.firstName("Troy")
.lastName("Hahn")
.emailAddress("[email protected]")
.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
Loading