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

feat : use vlads BaseJpaRepository and polish #1592

Merged
merged 3 commits into from
Dec 22, 2024
Merged
Changes from 1 commit
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
Next Next commit
feat : use vlads BaseJpaRepository and polish
rajadilipkolli committed Dec 22, 2024
commit d3d5206e779fc10dbad5078dbc5016c1aa3cce47
2 changes: 1 addition & 1 deletion jpa/boot-data-customsequence/pom.xml
Original file line number Diff line number Diff line change
@@ -224,7 +224,7 @@
<configuration>
<java>
<googleJavaFormat>
<version>1.24.0</version>
<version>1.25.2</version>
<style>AOSP</style>
</googleJavaFormat>
</java>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.custom.sequence.config;

import io.hypersistence.utils.spring.repository.BaseJpaRepositoryImpl;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Configuration
@EnableJpaRepositories(
repositoryBaseClass = BaseJpaRepositoryImpl.class,
basePackages = "com.example.custom.sequence.repositories")
public class JpaConfig {}
Original file line number Diff line number Diff line change
@@ -9,4 +9,4 @@
@OpenAPIDefinition(
info = @Info(title = "boot-jpa-customsequence", version = "v1"),
servers = @Server(url = "/"))
public class SwaggerConfig {}
class SwaggerConfig {}
Original file line number Diff line number Diff line change
@@ -5,9 +5,10 @@
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final ApplicationProperties properties;

@Override
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -33,7 +32,6 @@
private String id;

@Column(nullable = false)
@NotEmpty(message = "Text cannot be empty")
private String text;

@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
@@ -43,8 +41,18 @@
this.text = text;
}

public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}

public void removeOrder(Order removedOrder) {
orders.remove(removedOrder);
removedOrder.setCustomer(null);
}

@Override
public boolean equals(Object o) {

Check warning on line 55 in jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/Customer.java

GitHub Actions / Qodana Community for JVM

'equals()' method which does not check class of parameter

`equals()` should check the class of its parameter
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
Customer customer = (Customer) o;
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotEmpty;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -32,7 +31,6 @@
private String id;

@Column(nullable = false)
@NotEmpty(message = "Text cannot be empty")
private String text;

@ManyToOne(fetch = FetchType.LAZY)
@@ -40,11 +38,12 @@
private Customer customer;

@Override
public boolean equals(Object o) {

Check warning on line 41 in jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/Order.java

GitHub Actions / Qodana Community for JVM

'equals()' method which does not check class of parameter

`equals()` should check the class of its parameter
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
Order order = (Order) o;
return id != null && Objects.equals(id, order.id);
return Objects.equals(text, order.text)
&& Objects.equals(customer.getId(), order.customer.getId());
Comment on lines +45 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent potential NullPointerException in equals method

The current implementation could throw NullPointerException if the customer field is null. Additionally, comparing by text and customer ID instead of entity ID is unusual for JPA entities and could lead to unexpected behavior.

Consider this safer implementation:

-        return Objects.equals(text, order.text)
-                && Objects.equals(customer.getId(), order.customer.getId());
+        return Objects.equals(text, order.text)
+                && Objects.equals(id, order.id);

If you really need to compare by text and customer, add null checks:

-        return Objects.equals(text, order.text)
-                && Objects.equals(customer.getId(), order.customer.getId());
+        return Objects.equals(text, order.text)
+                && Objects.equals(customer, order.customer);

Committable suggestion skipped: line range outside the PR's diff.

}

@Override
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
package com.example.custom.sequence.mapper;

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.repositories.OrderRepository;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;

@Service
public class CustomerMapper {

private final OrderMapper orderMapper;
private final OrderRepository orderRepository;

public CustomerMapper(OrderMapper orderMapper, OrderRepository orderRepository) {
this.orderMapper = orderMapper;
this.orderRepository = orderRepository;
}

public CustomerResponse mapToResponse(Customer saved) {
return new CustomerResponse(saved.getId(), saved.getText());
return new CustomerResponse(
saved.getId(),
saved.getText(),
orderMapper.mapToResponseListWithOutCustomer(saved.getOrders()));
}

public Customer mapToEntity(CustomerRequest customerRequest) {
Customer customer = new Customer(customerRequest.text());
customerRequest
.orders()
.forEach(orderRequest -> customer.addOrder(orderMapper.mapToEntity(orderRequest)));
return customer;
}

public void updateCustomerFromRequest(CustomerRequest customerRequest, Customer foundCustomer) {
foundCustomer.setText(customerRequest.text());
List<Order> removedOrders = new ArrayList<>(foundCustomer.getOrders());
List<Order> ordersFromRequest =
customerRequest.orders().stream()
.map(
orderRequest ->
orderMapper.mapToEntityWithCustomer(
orderRequest, foundCustomer))
.collect(Collectors.toList());
removedOrders.removeAll(ordersFromRequest);

for (Order removedOrder : removedOrders) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai, please write integrationTests in CustomerControllerIT to showcase all 3 scenarions as per the code

Copy link
Contributor

Choose a reason for hiding this comment

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

@rajadilipkolli Below is an example of how you might write three integration tests in CustomerControllerIT to cover typical scenarios—creation, update, and validation error. Adjust field names and URLs as needed to match your actual setup:


Java

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

@AutoConfigureMockMvc
@transactional
class CustomerControllerIT extends BaseIT {

@Autowired
private MockMvc mockMvc;

private JacksonTester<CustomerRequest> jsonRequest;
private JacksonTester<CustomerResponse> jsonResponse;

@BeforeEach
void setUp() {
    // If needed, initialize test data here
}

@Test
void shouldCreateNewCustomerSuccessfully() throws Exception {
    // Given
    CustomerRequest request = new CustomerRequest(
        "Sample Customer", // text 
        List.of()          // empty order list for simplicity
    );

    // When & Then
    mockMvc.perform(post("/api/customers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonRequest.write(request).getJson()))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.id").exists())
        .andExpect(jsonPath("$.text").value("Sample Customer"))
        .andExpect(jsonPath("$.orderResponses").isEmpty());
}

@Test
void shouldUpdateExistingCustomer() throws Exception {
    // First, create a new customer. You can also use a DB seeding approach if you prefer.
    CustomerRequest initialRequest = new CustomerRequest("Initial Name", List.of());
    String createdCustomerResponse = mockMvc.perform(post("/api/customers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonRequest.write(initialRequest).getJson()))
        .andExpect(status().isCreated())
        .andReturn()
        .getResponse()
        .getContentAsString();
    CustomerResponse createdCustomer = jsonResponse.parse(createdCustomerResponse).getObject();

    // Given
    CustomerRequest updateRequest = new CustomerRequest("Updated Name", List.of());

    // When & Then
    mockMvc.perform(put("/api/customers/{id}", createdCustomer.id())
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonRequest.write(updateRequest).getJson()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(createdCustomer.id()))
        .andExpect(jsonPath("$.text").value("Updated Name"));
}

@Test
void shouldFailToCreateCustomerWhenTextIsEmpty() throws Exception {
    // Given
    CustomerRequest request = new CustomerRequest(
        "",    // invalid text
        List.of()
    );

    // When & Then
    mockMvc.perform(post("/api/customers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonRequest.write(request).getJson()))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.errors").isNotEmpty()); // Adjust to match your error handling
}

@Test
void shouldDeleteCustomer() throws Exception {
    // First, create a new customer
    CustomerRequest request = new CustomerRequest("Somebody to Delete", List.of());
    String createdCustomerResponse = mockMvc.perform(post("/api/customers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonRequest.write(request).getJson()))
        .andExpect(status().isCreated())
        .andReturn()
        .getResponse()
        .getContentAsString();
    CustomerResponse createdCustomer = jsonResponse.parse(createdCustomerResponse).getObject();

    // When & Then
    mockMvc.perform(delete("/api/customers/{id}", createdCustomer.id()))
        .andExpect(status().isNoContent());

    // Verify it's gone
    mockMvc.perform(get("/api/customers/{id}", createdCustomer.id()))
        .andExpect(status().isNotFound());
}

}


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1592
File: jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/mapper/CustomerMapper.java:51-51
Timestamp: 2024-12-22T12:22:30.495Z
Learning: Integration tests for CustomerController can showcase creation, update, and validation scenarios using "CustomerRequest" and verifying JSON fields in the response.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai, In above examples include various orderRequests like (newer one, updated one, delete ones )as of now all are empty

Copy link
Contributor

Choose a reason for hiding this comment

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

@rajadilipkolli Below is an illustrative example in CustomerControllerIT showing how you might handle various order requests (new, updated, and deleted) within a single update scenario. Feel free to adapt field names, request body details, and endpoints as needed to align with your code:


Java

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
;

import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.OrderResponseWithOutCustomer;
import java.util.List;
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.boot.test.json.JacksonTester;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

@AutoConfigureMockMvc
@transactional
class CustomerControllerIT extends BaseIT {

@Autowired
private MockMvc mockMvc;

private JacksonTester<CustomerRequest> jsonCustomerRequest;
private JacksonTester<OrderRequest> jsonOrderRequest;
private JacksonTester<CustomerResponse> jsonCustomerResponse;

@BeforeEach
void setUp() {
    // Initialize JacksonTesters if needed, for example:
    // JacksonTester.initFields(this, new ObjectMapper());
}

@Test
void shouldHandleMultipleOrdersInUpdateScenario() throws Exception {
    // 1) Create a new Customer with two initial orders
    CustomerRequest createRequest = new CustomerRequest(
        "Original Customer",
        List.of(
            new OrderRequest("Order_One", "dummyCustomerId"), 
            new OrderRequest("Order_Two", "dummyCustomerId")
        )
    );

    String createResponse = mockMvc.perform(post("/api/customers")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonCustomerRequest.write(createRequest).getJson()))
        .andExpect(status().isCreated())
        .andReturn()
        .getResponse()
        .getContentAsString();

    CustomerResponse createdCustomer = jsonCustomerResponse.parse(createResponse).getObject();
    String createdCustomerId = createdCustomer.id();
    assertThat(createdCustomerId).isNotNull();

    // 2) Prepare an update with:
    //    - One of the original orders changed (Order_One -> "Order_One_Updated")
    //    - A new order added ("Order_Three_New")
    //    - The other original order ("Order_Two") removed from the list
    CustomerRequest updateRequest = new CustomerRequest(
        "Updated Customer Name",
        List.of(
            // updated text for the order that previously had "Order_One"
            new OrderRequest("Order_One_Updated", "dummyCustomerId"),
            // brand-new order
            new OrderRequest("Order_Three_New", "dummyCustomerId")
            // "Order_Two" is omitted here, so it should be removed
        )
    );

    String updatedResponse = mockMvc.perform(put("/api/customers/{id}", createdCustomerId)
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonCustomerRequest.write(updateRequest).getJson()))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(createdCustomerId))
        .andExpect(jsonPath("$.text").value("Updated Customer Name"))
        .andReturn()
        .getResponse()
        .getContentAsString();

    CustomerResponse updatedCustomer = jsonCustomerResponse.parse(updatedResponse).getObject();
    assertThat(updatedCustomer.orderResponses()).hasSize(2); 
    // Should contain only the updated and the new order now

    // 3) Verify that the orders have the correct text values
    //    and that the removed order doesn't appear
    List<OrderResponseWithOutCustomer> updatedOrders = updatedCustomer.orderResponses();
    assertThat(updatedOrders).extracting("orderDescription")
        .containsExactlyInAnyOrder("Order_One_Updated", "Order_Three_New");
}

}


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1592
File: jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/mapper/CustomerMapper.java:51-51
Timestamp: 2024-12-22T12:38:14.323Z
Learning: Integration tests in CustomerControllerIT can showcase handling multiple OrderRequests (new, updated, removed) during customer creation or update scenarios.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

foundCustomer.removeOrder(removedOrder);
}

List<Order> newOrders = new ArrayList<>(ordersFromRequest);
newOrders.removeAll(foundCustomer.getOrders());

ordersFromRequest.removeAll(newOrders);

for (Order existingOrder : ordersFromRequest) {
existingOrder.setCustomer(foundCustomer);
// manually set the id of the existing order to avoid creating a new order instead of
// updating the existing one
for (Order foundOrder : foundCustomer.getOrders()) {
if (foundOrder.getText().equals(existingOrder.getText())) {
existingOrder.setId(foundOrder.getId());
break;
}
}
Order mergedOrder = orderRepository.merge(existingOrder);
foundCustomer
.getOrders()
.set(foundCustomer.getOrders().indexOf(mergedOrder), mergedOrder);
}

for (Order newOrder : newOrders) {
foundCustomer.addOrder(newOrder);
}
}
}
Original file line number Diff line number Diff line change
@@ -2,8 +2,11 @@

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.CustomerResponseWithOutOrder;
import com.example.custom.sequence.model.response.OrderResponse;
import com.example.custom.sequence.model.response.OrderResponseWithOutCustomer;
import java.util.List;
import org.springframework.stereotype.Service;

@Service
@@ -14,6 +17,28 @@ public OrderResponse getOrderResponse(Order order) {
return new OrderResponse(
order.getId(),
order.getText(),
new CustomerResponse(customer.getId(), customer.getText()));
new CustomerResponseWithOutOrder(customer.getId(), customer.getText()));
}

public Order mapToEntity(OrderRequest orderRequest) {
Order order = new Order();
order.setText(orderRequest.text());
return order;
}

public List<OrderResponse> mapToResponseList(List<Order> orders) {
return orders.stream().map(this::getOrderResponse).toList();
}

public List<OrderResponseWithOutCustomer> mapToResponseListWithOutCustomer(List<Order> orders) {
return orders.stream()
.map(order -> new OrderResponseWithOutCustomer(order.getId(), order.getText()))
.toList();
}

public Order mapToEntityWithCustomer(OrderRequest orderRequest, Customer foundCustomer) {
Order order = mapToEntity(orderRequest);
order.setCustomer(foundCustomer);
return order;
Comment on lines +23 to +42
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for robustness

The mapping methods are well-structured and follow good practices, but could benefit from additional validation:

Consider adding these safety checks:

 public Order mapToEntity(OrderRequest orderRequest) {
+    if (orderRequest == null) {
+        throw new IllegalArgumentException("OrderRequest cannot be null");
+    }
+    if (orderRequest.text() == null || orderRequest.text().trim().isEmpty()) {
+        throw new IllegalArgumentException("Order text cannot be null or empty");
+    }
     Order order = new Order();
     order.setText(orderRequest.text());
     return order;
 }

 public List<OrderResponse> mapToResponseList(List<Order> orders) {
+    if (orders == null) {
+        throw new IllegalArgumentException("Orders list cannot be null");
+    }
     return orders.stream().map(this::getOrderResponse).toList();
 }

 public Order mapToEntityWithCustomer(OrderRequest orderRequest, Customer foundCustomer) {
+    if (foundCustomer == null) {
+        throw new IllegalArgumentException("Customer cannot be null");
+    }
     Order order = mapToEntity(orderRequest);
     order.setCustomer(foundCustomer);
     return order;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Order mapToEntity(OrderRequest orderRequest) {
Order order = new Order();
order.setText(orderRequest.text());
return order;
}
public List<OrderResponse> mapToResponseList(List<Order> orders) {
return orders.stream().map(this::getOrderResponse).toList();
}
public List<OrderResponseWithOutCustomer> mapToResponseListWithOutCustomer(List<Order> orders) {
return orders.stream()
.map(order -> new OrderResponseWithOutCustomer(order.getId(), order.getText()))
.toList();
}
public Order mapToEntityWithCustomer(OrderRequest orderRequest, Customer foundCustomer) {
Order order = mapToEntity(orderRequest);
order.setCustomer(foundCustomer);
return order;
public Order mapToEntity(OrderRequest orderRequest) {
if (orderRequest == null) {
throw new IllegalArgumentException("OrderRequest cannot be null");
}
if (orderRequest.text() == null || orderRequest.text().trim().isEmpty()) {
throw new IllegalArgumentException("Order text cannot be null or empty");
}
Order order = new Order();
order.setText(orderRequest.text());
return order;
}
public List<OrderResponse> mapToResponseList(List<Order> orders) {
if (orders == null) {
throw new IllegalArgumentException("Orders list cannot be null");
}
return orders.stream().map(this::getOrderResponse).toList();
}
public List<OrderResponseWithOutCustomer> mapToResponseListWithOutCustomer(List<Order> orders) {
return orders.stream()
.map(order -> new OrderResponseWithOutCustomer(order.getId(), order.getText()))
.toList();
}
public Order mapToEntityWithCustomer(OrderRequest orderRequest, Customer foundCustomer) {
if (foundCustomer == null) {
throw new IllegalArgumentException("Customer cannot be null");
}
Order order = mapToEntity(orderRequest);
order.setCustomer(foundCustomer);
return order;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.custom.sequence.model.request;

import jakarta.validation.constraints.NotEmpty;
import java.util.List;

public record CustomerRequest(
@NotEmpty(message = "Text cannot be empty") String text, List<OrderRequest> orders) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.custom.sequence.model.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;

public record OrderRequest(
@NotEmpty(message = "Text cannot be empty") String text,
@NotBlank(message = "CustomerId cannot be blank") String customerId) {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package com.example.custom.sequence.model.response;

public record CustomerResponse(String id, String text) {}
import java.util.List;

public record CustomerResponse(
String id, String text, List<OrderResponseWithOutCustomer> orderResponses) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example.custom.sequence.model.response;

public record CustomerResponseWithOutOrder(String id, String text) {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package com.example.custom.sequence.model.response;

public record OrderResponse(String id, String text, CustomerResponse customer) {}
public record OrderResponse(String id, String text, CustomerResponseWithOutOrder customer) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example.custom.sequence.model.response;

public record OrderResponseWithOutCustomer(String id, String text) {}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.example.custom.sequence.repositories;

import com.example.custom.sequence.entities.Customer;
import io.hypersistence.utils.spring.repository.BaseJpaRepository;
import java.util.List;
import java.util.Optional;
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;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface CustomerRepository extends JpaRepository<Customer, String> {
public interface CustomerRepository extends BaseJpaRepository<Customer, String> {

@EntityGraph(attributePaths = "orders")
Optional<Customer> findById(String id);
@@ -26,4 +26,6 @@ public interface CustomerRepository extends JpaRepository<Customer, String> {

@Query(value = "select c.id from Customer c ", countQuery = "select count(c) from Customer c")
Page<String> findAllCustomerIds(Pageable pageable);

void deleteAllInBatch();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.example.custom.sequence.repositories;

import com.example.custom.sequence.entities.Order;
import io.hypersistence.utils.spring.repository.BaseJpaRepository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.ListPagingAndSortingRepository;
import org.springframework.data.repository.query.Param;

public interface OrderRepository extends JpaRepository<Order, String> {
public interface OrderRepository
extends BaseJpaRepository<Order, String>, ListPagingAndSortingRepository<Order, String> {

@Query("select o from Order o join fetch o.customer where o.id = :id")
Optional<Order> findById(@Param("id") String id);

void deleteAllInBatch();
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Exercise caution with deleteAllInBatch operation - potential data integrity risk

The deleteAllInBatch() method in OrderRepository is unsafe to use because:

  • Orders are part of a bidirectional relationship with Customer entity
  • Customer has @OneToMany relationship with cascade = CascadeType.ALL and orphanRemoval = true
  • Using deleteAllInBatch() bypasses these JPA cascade operations and orphan removal, which could lead to:
    • Orphaned references in the Customer.orders collection
    • Inconsistent state between Customer and Order entities
    • Potential constraint violations

Instead, use the standard deleteAll() method which respects JPA cascade operations and entity lifecycle events.

🔗 Analysis chain

Exercise caution with deleteAllInBatch operation

The deleteAllInBatch() method performs a bulk delete operation which:

  1. Bypasses entity callbacks and cascades
  2. Could violate referential integrity if not used carefully

Let me gather more information about potential foreign key relationships by checking the Order entity and related entities.


Let me check one more specific detail about the Order entity and its relationships.


Let me check one final detail about the Customer entity to understand the complete relationship structure.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for foreign key relationships to Order entity
ast-grep --pattern 'class $_ {
  $$$
  @ManyToOne
  $$$
  Order $_;
  $$$
}'

# Check for usage of deleteAllInBatch to ensure it's used in appropriate contexts
rg "deleteAllInBatch" --type java

Length of output: 3869


Script:

#!/bin/bash
# Check Order entity definition
rg -A 10 "class Order" --type java

# Check for @OneToMany relationships that might reference Order
ast-grep --pattern 'class $_ {
  $$$
  @OneToMany
  $$$
  $_ Order
  $$$
}'

# Check for any JPA cascade configurations related to Order
rg "@OneToMany.*Order|@ManyToOne.*Order" --type java

Length of output: 34985


Script:

#!/bin/bash
# Check the Order entity in the custom sequence project for its relationships
rg -A 15 "class Order" jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/Order.java

# Check if there are any entities with relationships to Order
rg "@ManyToOne.*Order|@OneToMany.*Order" jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/

Length of output: 643


Script:

#!/bin/bash
# Check the Customer entity for any relationships with Order
rg -A 15 "class Customer" jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/Customer.java

# Check for any cascade configurations in the Customer-Order relationship
rg "cascade.*ALL" jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/entities/

Length of output: 877

}
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.mapper.CustomerMapper;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.repositories.CustomerRepository;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -16,14 +16,17 @@
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CustomerService {

private final CustomerRepository customerRepository;
private final CustomerMapper customerMapper;

@Transactional(readOnly = true)
public CustomerService(CustomerRepository customerRepository, CustomerMapper customerMapper) {
this.customerRepository = customerRepository;
this.customerMapper = customerMapper;
}

public PagedResult<Customer> findAllCustomers(
int pageNo, int pageSize, String sortBy, String sortDir) {
Sort sort =
@@ -52,12 +55,33 @@ public Optional<CustomerResponse> findCustomerById(String id) {
return customerRepository.findById(id).map(customerMapper::mapToResponse);
}

public CustomerResponse saveCustomer(Customer customer) {
Customer saved = customerRepository.save(customer);
@Transactional
public CustomerResponse saveCustomer(CustomerRequest customerRequest) {
Customer customer = customerMapper.mapToEntity(customerRequest);
Customer saved = customerRepository.persist(customer);
return customerMapper.mapToResponse(saved);
}

@Transactional
public Optional<CustomerResponse> updateCustomerById(
String id, CustomerRequest customerRequest) {
return customerRepository
.findById(id)
.map(
foundCustomer -> {
customerMapper.updateCustomerFromRequest(
customerRequest, foundCustomer);
return customerMapper.mapToResponse(
customerRepository.merge(foundCustomer));
});
}

@Transactional
public void deleteCustomerById(String id) {
customerRepository.deleteById(id);
}

public Optional<Customer> findById(String customerId) {
return customerRepository.findById(customerId);
}
}
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@

import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.mapper.OrderMapper;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.OrderResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.repositories.OrderRepository;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -16,13 +16,22 @@
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

private final OrderRepository orderRepository;
private final CustomerService customerService;
private final OrderMapper orderMapper;

public OrderService(
OrderRepository orderRepository,
CustomerService customerService,
OrderMapper orderMapper) {
this.orderRepository = orderRepository;
this.customerService = customerService;
this.orderMapper = orderMapper;
}

public PagedResult<OrderResponse> findAllOrders(
int pageNo, int pageSize, String sortBy, String sortDir) {
Sort sort =
@@ -52,11 +61,33 @@ public Optional<OrderResponse> findOrderById(String id) {
return orderRepository.findById(id).map(orderMapper::getOrderResponse);
}

public OrderResponse saveOrder(Order order) {
return orderMapper.getOrderResponse(orderRepository.save(order));
@Transactional
public Optional<OrderResponse> saveOrder(OrderRequest orderRequest) {
return customerService
.findById(orderRequest.customerId())
.map(
customer -> {
Order order = new Order();
order.setText(orderRequest.text());
order.setCustomer(customer);
return orderMapper.getOrderResponse(orderRepository.persist(order));
});
}

@Transactional
public void deleteOrderById(String id) {
orderRepository.deleteById(id);
}

@Transactional
public Optional<OrderResponse> updateOrderById(String id, OrderRequest orderRequest) {
return orderRepository
.findById(id)
.map(
order -> {
order.setText(orderRequest.text());
return orderMapper.getOrderResponse(
orderRepository.mergeAndFlush(order));
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.custom.sequence.web.api;

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.PagedResult;
import io.swagger.v3.oas.annotations.Operation;
@@ -104,5 +105,5 @@ ResponseEntity<CustomerResponse> getCustomerById(
schema = @Schema(implementation = ProblemDetail.class))
})
})
CustomerResponse createCustomer(@Valid @RequestBody(required = true) Customer customer);
CustomerResponse createCustomer(@Valid @RequestBody(required = true) CustomerRequest customer);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.custom.sequence.web.controllers;

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.services.CustomerService;
@@ -58,20 +59,17 @@ public ResponseEntity<CustomerResponse> getCustomerById(@PathVariable String id)
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Override
public CustomerResponse createCustomer(@RequestBody @Validated Customer customer) {
return customerService.saveCustomer(customer);
public CustomerResponse createCustomer(
@RequestBody @Validated CustomerRequest customerRequest) {
return customerService.saveCustomer(customerRequest);
}

@PutMapping("/{id}")
public ResponseEntity<CustomerResponse> updateCustomer(
@PathVariable String id, @RequestBody Customer customer) {
@PathVariable String id, @RequestBody CustomerRequest customerRequest) {
return customerService
.findCustomerById(id)
.map(
customerObj -> {
customer.setId(id);
return ResponseEntity.ok(customerService.saveCustomer(customer));
})
.updateCustomerById(id, customerRequest)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.custom.sequence.web.controllers;

import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.OrderResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.services.OrderService;
@@ -17,7 +17,6 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@@ -53,21 +52,20 @@ public ResponseEntity<OrderResponse> getOrderById(@PathVariable String id) {
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse createOrder(@RequestBody @Validated Order order) {
return orderService.saveOrder(order);
public ResponseEntity<OrderResponse> createOrder(
@RequestBody @Validated OrderRequest orderRequest) {
return orderService
.saveOrder(orderRequest)
.map(order -> ResponseEntity.status(HttpStatus.CREATED).body(order))
.orElseGet(() -> ResponseEntity.notFound().build());
Comment on lines +55 to +60
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Review error handling in createOrder method.

While the overall changes improve the API design, returning a 404 NOT FOUND status when order creation fails might be misleading. Consider the following scenarios:

  • If customer doesn't exist: 404 is appropriate
  • If validation fails: Should return 400 BAD REQUEST
  • If internal error occurs: Should return 500 INTERNAL SERVER ERROR

Consider refactoring to handle different error cases explicitly:

     public ResponseEntity<OrderResponse> createOrder(
             @RequestBody @Validated OrderRequest orderRequest) {
         return orderService
                 .saveOrder(orderRequest)
-                .map(order -> ResponseEntity.status(HttpStatus.CREATED).body(order))
-                .orElseGet(() -> ResponseEntity.notFound().build());
+                .map(order -> ResponseEntity.status(HttpStatus.CREATED).body(order))
+                .orElseThrow(() -> new ResponseStatusException(
+                    HttpStatus.BAD_REQUEST, "Failed to create order"));
     }

Committable suggestion skipped: line range outside the PR's diff.

}

@PutMapping("/{id}")
public ResponseEntity<OrderResponse> updateOrder(
@PathVariable String id, @RequestBody Order order) {
@PathVariable String id, @RequestBody OrderRequest orderRequest) {
return orderService
.findOrderById(id)
.map(
orderObj -> {
order.setId(id);
return ResponseEntity.ok(orderService.saveOrder(order));
})
.updateOrderById(id, orderRequest)
.map(ResponseEntity::ok)
Comment on lines +65 to +68
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

After analyzing the code, I can now provide a definitive response about the customer validation concern:

Customer validation is missing in the update operation

The updateOrderById method in OrderService does not validate the customer existence, unlike the saveOrder method which properly checks for customer existence. This could lead to data inconsistency if the customerId in the request is changed.

  • OrderService.updateOrderById() only checks for order existence but skips customer validation
  • OrderRequest has @NotBlank validation for customerId but this only ensures it's not blank
  • For comparison, saveOrder() method properly validates customer existence via customerService.findById()
🔗 Analysis chain

Verify customer validation in OrderService.

The error handling looks appropriate for a non-existent order. However, let's verify that the service layer properly validates the customer existence when updating an order.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for customer validation in OrderService
ast-grep --pattern 'updateOrderById($id, $request) {
  $$$
  $customer
  $$$
}'

# Search for any validation annotations on OrderRequest
rg -A 5 "class OrderRequest"

Length of output: 112


Script:

#!/bin/bash
# Let's try a broader search for OrderService implementation
ast-grep --pattern 'class OrderService'

# Search for updateOrderById method with more flexible pattern
ast-grep --pattern 'updateOrderById'

# Search for OrderRequest in all files
fd OrderRequest --exec cat {}

# Search for customer validation patterns in service layer
rg -A 5 "customer.*exist|exist.*customer" --type java

Length of output: 1955


Script:

#!/bin/bash
# Let's look at the OrderService implementation to see the validation logic
cat jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/services/OrderService.java

# Also check if there's a CustomerService that might be used for validation
fd CustomerService --exec cat {}

# Look for customer repository usage
rg -A 5 "CustomerRepository" --type java

Length of output: 96664


Script:

#!/bin/bash
# Let's look at the OrderService implementation for updateOrderById
cat jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/services/OrderService.java

# Also check if there's any validation in the OrderRequest class
cat jpa/boot-data-customsequence/src/main/java/com/example/custom/sequence/model/request/OrderRequest.java

# Look for any custom validation annotations or validators
rg -A 5 "Validator|@Valid" --type java

Length of output: 71022

.orElseGet(() -> ResponseEntity.notFound().build());
}

Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import com.example.custom.sequence.common.ContainersConfig;
import com.example.custom.sequence.config.JpaConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
@@ -12,7 +13,7 @@
import org.springframework.context.annotation.Import;

@DataJpaTest(properties = {"spring.jpa.hibernate.ddl-auto=validate"})
@Import(ContainersConfig.class)
@Import({ContainersConfig.class, JpaConfig.class})
@AutoConfigureTestDatabase
class SchemaValidationTest {

Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.mapper.CustomerMapper;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.repositories.CustomerRepository;
@@ -72,10 +73,11 @@ void findCustomerById() {
@Test
void saveCustomer() {
// given
given(customerRepository.save(getCustomer())).willReturn(getCustomer());
given(customerMapper.mapToEntity(getCustomerRequest())).willReturn(getCustomer());
given(customerRepository.persist(getCustomer())).willReturn(getCustomer());
given(customerMapper.mapToResponse(getCustomer())).willReturn(getCustomerResponse());
// when
CustomerResponse persistedCustomer = customerService.saveCustomer(getCustomer());
CustomerResponse persistedCustomer = customerService.saveCustomer(getCustomerRequest());
// then
assertThat(persistedCustomer).isNotNull();
assertThat(persistedCustomer.id()).isEqualTo("CUS_1");
@@ -99,7 +101,11 @@ private Customer getCustomer() {
return customer;
}

private CustomerRequest getCustomerRequest() {
return new CustomerRequest("junitTest", List.of());
}

private CustomerResponse getCustomerResponse() {
return new CustomerResponse("CUS_1", "junitTest");
return new CustomerResponse("CUS_1", "junitTest", List.of());
}
}
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@
import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.mapper.OrderMapper;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.CustomerResponseWithOutOrder;
import com.example.custom.sequence.model.response.OrderResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.repositories.OrderRepository;
@@ -30,6 +31,7 @@
class OrderServiceTest {

@Mock private OrderRepository orderRepository;
@Mock private CustomerService customerService;
@Mock private OrderMapper orderMapper;

@InjectMocks private OrderService orderService;
@@ -73,14 +75,16 @@ void findOrderById() {
@Test
void saveOrder() {
// given
given(orderRepository.save(getOrder())).willReturn(getOrder());
given(customerService.findById("1"))
.willReturn(Optional.of(new Customer("1", "custText", List.of())));
given(orderRepository.persist(getOrder())).willReturn(getOrder());
given(orderMapper.getOrderResponse(getOrder())).willReturn(getOrderResponse());
// when
OrderResponse persistedOrder = orderService.saveOrder(getOrder());
Optional<OrderResponse> persistedOrder = orderService.saveOrder(getOrderReq());
// then
assertThat(persistedOrder).isNotNull();
assertThat(persistedOrder.id()).isEqualTo("1");
assertThat(persistedOrder.text()).isEqualTo("junitText");
assertThat(persistedOrder.isPresent()).isNotNull();
assertThat(persistedOrder.get().id()).isEqualTo("1");
assertThat(persistedOrder.get().text()).isEqualTo("junitText");
}

@Test
@@ -105,6 +109,11 @@ private Order getOrder() {
}

private OrderResponse getOrderResponse() {
return new OrderResponse("1", "junitText", new CustomerResponse("1", "custText"));
return new OrderResponse(
"1", "junitText", new CustomerResponseWithOutOrder("1", "custText"));
}

private OrderRequest getOrderReq() {
return new OrderRequest("junitText", "1");
}
}
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ void setUp() {
customerList.add(new Customer("First Customer"));
customerList.add(new Customer("Second Customer"));
customerList.add(new Customer("Third Customer"));
customerList = customerRepository.saveAll(customerList);
customerList = customerRepository.persistAll(customerList);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Migration to persistAll is incomplete across the codebase

The search results show that saveAll is still widely used across multiple modules (36 files), while persistAll appears in only 3 files. This indicates an incomplete migration to the new BaseJpaRepository convention.

Key locations requiring attention:

  • Multiple CustomerControllerIT classes still using saveAll:
    • jpa/multitenancy/**/CustomerControllerIT.java
    • jpa/boot-data-envers/src/test/java/com/example/envers/web/controllers/CustomerControllerIT.java
    • Several others in different modules
  • Various other controller tests and services still using saveAll
🔗 Analysis chain

Method name change aligns with BaseJpaRepository adoption.

The change from saveAll to persistAll appears to be part of adopting vlads BaseJpaRepository. The functionality remains the same while following the new convention.

Let's verify the consistency of this naming convention across other repositories:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining usage of saveAll to ensure complete migration
rg -l 'saveAll' --type java

# Search for new persistAll usage to verify consistent adoption
rg -l 'persistAll' --type java

Length of output: 4399

}

@Test
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.model.request.CustomerRequest;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.services.CustomerService;
@@ -27,11 +28,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(controllers = CustomerController.class)
@@ -40,7 +41,7 @@ class CustomerControllerTest {

@Autowired private MockMvc mockMvc;

@MockBean private CustomerService customerService;
@MockitoBean private CustomerService customerService;

@Autowired private ObjectMapper objectMapper;

@@ -76,7 +77,7 @@ void shouldFetchAllCustomers() throws Exception {
@Test
void shouldFindCustomerById() throws Exception {
String customerId = "CUS_1";
CustomerResponse customer = new CustomerResponse(customerId, "text 1");
CustomerResponse customer = new CustomerResponse(customerId, "text 1", List.of());
given(customerService.findCustomerById(customerId)).willReturn(Optional.of(customer));

this.mockMvc
@@ -97,8 +98,8 @@ void shouldReturn404WhenFetchingNonExistingCustomer() throws Exception {

@Test
void shouldCreateNewCustomer() throws Exception {
given(customerService.saveCustomer(any(Customer.class)))
.willReturn(new CustomerResponse("CUS_1", "some text"));
given(customerService.saveCustomer(any(CustomerRequest.class)))
.willReturn(new CustomerResponse("CUS_1", "some text", List.of()));
rajadilipkolli marked this conversation as resolved.
Show resolved Hide resolved

Customer customer = new Customer("CUS_1", "some text", new ArrayList<>());
this.mockMvc
@@ -113,13 +114,13 @@ void shouldCreateNewCustomer() throws Exception {

@Test
void shouldReturn400WhenCreateNewCustomerWithoutText() throws Exception {
Customer customer = new Customer(null, null, new ArrayList<>());
CustomerRequest customerRequest = new CustomerRequest(null, new ArrayList<>());

this.mockMvc
.perform(
post("/api/customers")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(customer)))
.content(objectMapper.writeValueAsString(customerRequest)))
.andExpect(status().isBadRequest())
.andExpect(header().string("Content-Type", is("application/problem+json")))
.andExpect(jsonPath("$.type", is("about:blank")))
@@ -136,18 +137,19 @@ void shouldReturn400WhenCreateNewCustomerWithoutText() throws Exception {
@Test
void shouldUpdateCustomer() throws Exception {
String customerId = "CUS_1";
CustomerResponse customer = new CustomerResponse(customerId, "Updated text");
given(customerService.findCustomerById(customerId)).willReturn(Optional.of(customer));
given(customerService.saveCustomer(any(Customer.class)))
.willReturn(new CustomerResponse("CUS_1", "Updated text"));
CustomerResponse customerResponse =
new CustomerResponse(customerId, "Updated text", List.of());
CustomerRequest customerRequest = new CustomerRequest("Updated text", List.of());
given(customerService.updateCustomerById(customerId, customerRequest))
.willReturn(Optional.of(customerResponse));

this.mockMvc
.perform(
put("/api/customers/{id}", customerId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(customer)))
.content(objectMapper.writeValueAsString(customerRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.text", is(customer.text())));
.andExpect(jsonPath("$.text", is(customerResponse.text())));
}

@Test
@@ -167,7 +169,7 @@ void shouldReturn404WhenUpdatingNonExistingCustomer() throws Exception {
@Test
void shouldDeleteCustomer() throws Exception {
String customerId = "CUS_1";
CustomerResponse customer = new CustomerResponse(customerId, "Some text");
CustomerResponse customer = new CustomerResponse(customerId, "Some text", List.of());
given(customerService.findCustomerById(customerId)).willReturn(Optional.of(customer));
doNothing().when(customerService).deleteCustomerById(customerId);

Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
import com.example.custom.sequence.common.AbstractIntegrationTest;
import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.repositories.CustomerRepository;
import com.example.custom.sequence.repositories.OrderRepository;
import java.util.ArrayList;
@@ -36,15 +37,15 @@ class OrderControllerIT extends AbstractIntegrationTest {
@BeforeEach
void setUp() {
orderRepository.deleteAllInBatch();
customerRepository.deleteAll();
customerRepository.deleteAllInBatch();
Customer cust = new Customer("customer1");
customer = customerRepository.save(cust);
customer = customerRepository.persist(cust);

orderList = new ArrayList<>();
orderList.add(new Order(null, "First Order", customer));
orderList.add(new Order(null, "Second Order", customer));
orderList.add(new Order(null, "Third Order", customer));
orderList = orderRepository.saveAll(orderList);
orderList = orderRepository.persistAll(orderList);
}

@Test
@@ -76,7 +77,7 @@ void shouldFindOrderById() throws Exception {

@Test
void shouldCreateNewOrder() throws Exception {
Order order = new Order(null, "New Order", customer);
OrderRequest order = new OrderRequest("New Order", customer.getId());
this.mockMvc
.perform(
post("/api/orders")
@@ -85,12 +86,12 @@ void shouldCreateNewOrder() throws Exception {
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", notNullValue(), String.class))
.andExpect(jsonPath("$.id", hasLength(9)))
.andExpect(jsonPath("$.text", is(order.getText())));
.andExpect(jsonPath("$.text", is(order.text())));
}

@Test
void shouldReturn400WhenCreateNewOrderWithoutText() throws Exception {
Order order = new Order(null, null, null);
OrderRequest order = new OrderRequest(null, "CUS_1");

this.mockMvc
.perform(
@@ -115,17 +116,17 @@ void shouldReturn400WhenCreateNewOrderWithoutText() throws Exception {

@Test
void shouldUpdateOrder() throws Exception {
Order order = orderList.getFirst();
order.setText("Updated Order");
OrderRequest orderRequest = new OrderRequest("Updated Order", customer.getId());

Order order = orderList.getFirst();
this.mockMvc
.perform(
put("/api/orders/{id}", order.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(order)))
.content(objectMapper.writeValueAsString(orderRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(order.getId()), String.class))
.andExpect(jsonPath("$.text", is(order.getText())));
.andExpect(jsonPath("$.text", is(orderRequest.text())));
}

@Test
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@@ -17,7 +16,8 @@

import com.example.custom.sequence.entities.Customer;
import com.example.custom.sequence.entities.Order;
import com.example.custom.sequence.model.response.CustomerResponse;
import com.example.custom.sequence.model.request.OrderRequest;
import com.example.custom.sequence.model.response.CustomerResponseWithOutOrder;
import com.example.custom.sequence.model.response.OrderResponse;
import com.example.custom.sequence.model.response.PagedResult;
import com.example.custom.sequence.services.OrderService;
@@ -30,11 +30,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(controllers = OrderController.class)
@@ -43,7 +43,7 @@ class OrderControllerTest {

@Autowired private MockMvc mockMvc;

@MockBean private OrderService orderService;
@MockitoBean private OrderService orderService;

@Autowired private ObjectMapper objectMapper;

@@ -52,7 +52,7 @@ class OrderControllerTest {

@BeforeEach
void setUp() {
customer = new Customer(null, "customer1", new ArrayList<>());
customer = new Customer("CUST_01", "customer1", new ArrayList<>());
this.orderList = new ArrayList<>();
this.orderList.add(new Order("1", "text 1", customer));
this.orderList.add(new Order("2", "text 2", customer));
@@ -80,11 +80,14 @@ void shouldFetchAllOrders() throws Exception {
private static @NotNull PagedResult<OrderResponse> getOrderResponsePagedResult() {
List<OrderResponse> orderResponseList = new ArrayList<>();
orderResponseList.add(
new OrderResponse("1", "text 1", new CustomerResponse("1", "customer1")));
new OrderResponse(
"1", "text 1", new CustomerResponseWithOutOrder("1", "customer1")));
orderResponseList.add(
new OrderResponse("2", "text 2", new CustomerResponse("1", "customer1")));
new OrderResponse(
"2", "text 2", new CustomerResponseWithOutOrder("1", "customer1")));
orderResponseList.add(
new OrderResponse("3", "text 3", new CustomerResponse("1", "customer1")));
new OrderResponse(
"3", "text 3", new CustomerResponseWithOutOrder("1", "customer1")));
Page<OrderResponse> page = new PageImpl<>(orderResponseList);
return new PagedResult<>(page);
}
@@ -96,7 +99,7 @@ void shouldFindOrderById() throws Exception {
new OrderResponse(
orderId,
"text 1",
new CustomerResponse(customer.getId(), customer.getText()));
new CustomerResponseWithOutOrder(customer.getId(), customer.getText()));
given(orderService.findOrderById(orderId)).willReturn(Optional.of(order));

this.mockMvc
@@ -115,23 +118,23 @@ void shouldReturn404WhenFetchingNonExistingOrder() throws Exception {

@Test
void shouldCreateNewOrder() throws Exception {
given(orderService.saveOrder(any(Order.class)))
.willReturn(new OrderResponse("1", "some text", null));

Order order = new Order("1", "some text", customer);
OrderRequest orderRequest = new OrderRequest("some text", customer.getId());
given(orderService.saveOrder(orderRequest))
.willReturn(Optional.of(new OrderResponse("1", "some text", null)));
this.mockMvc
.perform(
post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(order)))
.content(objectMapper.writeValueAsString(orderRequest)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.text", is(order.getText())));
.andExpect(jsonPath("$.text", is(orderRequest.text())));
}

@Test
void shouldReturn400WhenCreateNewOrderWithoutText() throws Exception {
Order order = new Order(null, null, null);
OrderRequest order = new OrderRequest(null, null);

this.mockMvc
.perform(
@@ -145,9 +148,11 @@ void shouldReturn400WhenCreateNewOrderWithoutText() throws Exception {
.andExpect(jsonPath("$.status", is(400)))
.andExpect(jsonPath("$.detail", is("Invalid request content.")))
.andExpect(jsonPath("$.instance", is("/api/orders")))
.andExpect(jsonPath("$.violations", hasSize(1)))
.andExpect(jsonPath("$.violations[0].field", is("text")))
.andExpect(jsonPath("$.violations[0].message", is("Text cannot be empty")))
.andExpect(jsonPath("$.violations", hasSize(2)))
.andExpect(jsonPath("$.violations[0].field", is("customerId")))
.andExpect(jsonPath("$.violations[0].message", is("CustomerId cannot be blank")))
.andExpect(jsonPath("$.violations[1].field", is("text")))
.andExpect(jsonPath("$.violations[1].message", is("Text cannot be empty")))
.andReturn();
}

@@ -158,16 +163,16 @@ void shouldUpdateOrder() throws Exception {
new OrderResponse(
orderId,
"Updated text",
new CustomerResponse(customer.getId(), customer.getText()));
given(orderService.findOrderById(orderId)).willReturn(Optional.of(orderResponse));
given(orderService.saveOrder(any(Order.class)))
.willReturn(new OrderResponse("1", "Updated text", null));
new CustomerResponseWithOutOrder(customer.getId(), customer.getText()));
OrderRequest orderRequest = new OrderRequest("Updated text", customer.getId());
given(orderService.updateOrderById(orderId, orderRequest))
.willReturn(Optional.of(orderResponse));

this.mockMvc
.perform(
put("/api/orders/{id}", orderResponse.id())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(orderResponse)))
.content(objectMapper.writeValueAsString(orderRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.text", is(orderResponse.text())));
}
@@ -193,7 +198,7 @@ void shouldDeleteOrder() throws Exception {
new OrderResponse(
orderId,
"Some text",
new CustomerResponse(customer.getId(), customer.getText()));
new CustomerResponseWithOutOrder(customer.getId(), customer.getText()));
given(orderService.findOrderById(orderId)).willReturn(Optional.of(order));
doNothing().when(orderService).deleteOrderById(order.id());

30 changes: 16 additions & 14 deletions jpa/boot-data-customsequence/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
</configuration>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="STDOUT"/>
</root>

<logger name="org.testcontainers" level="INFO"/>
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="com.example.custom.sequence" level="DEBUG"/>

</configuration>
Loading