chore: update

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
TakahashiNg
2026-04-28 16:17:29 +00:00
parent f20324568c
commit f70d37f32c
6 changed files with 257 additions and 29 deletions
+18 -4
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -8,7 +10,7 @@
<version>1.5.16</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>cart-service</artifactId>
<packaging>quarkus</packaging>
@@ -42,6 +44,17 @@
<artifactId>lib</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.wiremock</groupId>
<artifactId>quarkus-wiremock-test</artifactId>
<version>1.6.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-reactive-messaging-in-memory</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
@@ -134,7 +147,8 @@
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
@@ -157,4 +171,4 @@
</properties>
</profile>
</profiles>
</project>
</project>
@@ -22,6 +22,8 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import org.eclipse.microprofile.reactive.messaging.Incoming;
@Path("/")
@@ -34,6 +36,10 @@ public class CartController {
@Inject
ReactiveRedisDataSource redisDataSource;
@Inject
@Channel("menu-validation-out")
Emitter<MenuItemValidation> validationEmitter;
private ReactiveValueCommands<String, String> valueCommands;
private ReactiveHashCommands<String, String, String> hashCommands;
private final ReactiveKeyCommands<String> keyCommands; // Thêm cái này
@@ -135,9 +141,11 @@ public class CartController {
return Response.ok("ItemAdded").build();
}
validationEmitter.send(
new MenuItemValidation(request.menuItemId, eateryId)
);
MenuItemValidation validation = new MenuItemValidation();
validation.menuItemId = request.menuItemId;
validation.eateryId = eateryId;
validationEmitter.send(validation);
return Response.status(Response.Status.ACCEPTED)
.entity("PendingVerification")
@@ -146,21 +154,21 @@ public class CartController {
});
}
@Incoming("menu-validation-results") // Tên channel trong application.properties
public Uni<Void> processValidationResult(MenuItemStatusEvent event) {
// Giả sử event trả về có: menuItemId, eateryId, exists (boolean)
// @Incoming("menu-validation-in") // Tên channel trong application.properties
// public Uni<Void> processValidationResult(MenuItemStatusE event) {
// // Giả sử event trả về có: menuItemId, eateryId, exists (boolean)
if (!event.exists) {
// Nếu món ăn không tồn tại, ta xóa nó khỏi Redis
// Lưu ý: Vì ta không biết món này nằm ở cartId nào (do bạn yêu cầu bỏ cartId)
// Bạn sẽ cần một chiến lược tìm kiếm hoặc quản lý key hiệu quả.
// Cách 1: Nếu bạn có danh sách Cart của Eatery đó, duyệt qua và xóa:
// String cartItemsKey = "cart:" + event.cartId + ":items";
// return hashCommands.hdel(cartItemsKey, event.menuItemId.toString()).replaceWithVoid();
// Cách 2: Nếu xóa cache thông tin món ăn để các request sau cũng fail:
// return keyCommands.del("menu:item:" + event.menuItemId).replaceWithVoid();
}
// if (!event.exists) {
// // Nếu món ăn không tồn tại, ta xóa nó khỏi Redis
// // Lưu ý: Vì ta không biết món này nằm ở cartId nào (do bạn yêu cầu bỏ cartId)
// // Bạn sẽ cần một chiến lược tìm kiếm hoặc quản lý key hiệu quả.
// // Cách 1: Nếu bạn có danh sách Cart của Eatery đó, duyệt qua và xóa:
// // String cartItemsKey = "cart:" + event.cartId + ":items";
// // return hashCommands.hdel(cartItemsKey, event.menuItemId.toString()).replaceWithVoid();
// // Cách 2: Nếu xóa cache thông tin món ăn để các request sau cũng fail:
// // return keyCommands.del("menu:item:" + event.menuItemId).replaceWithVoid();
// }
return Uni.createFrom().voidItem();
}
// return Uni.createFrom().voidItem();
// }
}
@@ -1,9 +1,23 @@
mp.messaging.incoming.words-in.topic=words
mp.messaging.outgoing.words-out.topic=words
mp.messaging.incoming.words-in.auto.offset.reset=earliest
# Dependency
quarkus.index-dependency.lib.group-id=com.drinkool
quarkus.index-dependency.lib.artifact-id=lib
# Kafka
%prod.mp.messaging.connector.smallrye-kafka.bootstrap.servers=${KAFKA_SERVICE_SERVICE_HOST:host.docker.internal}:9092
quarkus.messaging.kafka.serializer-generation.enabled=true
# Redis
quarkus.redis.hosts=redis://${REDIS_SERVICE_SERVICE_HOST:localhost}:${REDIS_SERVICE_SERVICE_PORT:6379}
# GraphQl
quarkus.smallrye-graphql-client.eatery-client.url=http://eatery-service/graphql
## Create menu validation topic
### Out
mp.messaging.outgoing.menu-validation-out.connector=smallrye-kafka
mp.messaging.outgoing.menu-validation-out.topic=menu-validation
mp.messaging.outgoing.menu-validation-out.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
### In
mp.messaging.incoming.menu-validation-in.connector=smallrye-kafka
mp.messaging.incoming.menu-validation-in.topic=menu-validation
mp.messaging.incoming.menu-validation-in.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
@@ -0,0 +1,182 @@
package com.drinkool.controllers;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.drinkool.dtos.CartItemRequest;
import com.drinkool.dtos.event.MenuItemValidation;
import io.quarkiverse.wiremock.devservice.ConnectWireMock;
import io.quarkus.redis.datasource.ReactiveRedisDataSource;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.reactive.messaging.memory.InMemoryConnector;
import io.smallrye.reactive.messaging.memory.InMemorySink;
import jakarta.enterprise.inject.Any;
import jakarta.inject.Inject;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@QuarkusTest
@TestHTTPEndpoint(CartController.class)
@ConnectWireMock
public class CartControllerTest {
@Inject
ReactiveRedisDataSource redisDataSource;
@Inject
@Any
InMemoryConnector connector;
@BeforeEach
void setup() {
redisDataSource.flushall().await().indefinitely();
stubFor(
post(urlEqualTo("/graphql")).willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withBody(
"{\"data\":{\"eateryById\":{\"id\":\"" + UUID.randomUUID() + "\"}}}"
)
)
);
InMemorySink<MenuItemValidation> results = connector.sink(
"menu-validation-out"
);
results.clear();
}
@Test
void testCreateCart_Success_WithRealRedis() {
UUID eateryId = UUID.randomUUID();
// 2. Gọi API tạo giỏ hàng
String cartId = given()
.queryParam("eateryId", eateryId.toString())
.when()
.post()
.then()
.statusCode(200)
.extract()
.asString();
// 3. Kiểm tra Redis thật xem dữ liệu đã được lưu chưa
String savedEateryId = redisDataSource
.value(String.class)
.get("cart:" + cartId)
.await()
.indefinitely();
assert savedEateryId.equals(eateryId.toString());
}
@Test
void testAddItem_PendingVerification_WithKafkaMock() {
InMemorySink<MenuItemValidation> results = connector.sink(
"menu-validation-out"
);
// 1. Chuẩn bị dữ liệu: Tạo một giỏ hàng sẵn trong Redis
String cartId = UUID.randomUUID().toString();
UUID eateryId = UUID.randomUUID();
redisDataSource
.value(String.class)
.set("cart:" + cartId, eateryId.toString())
.await()
.indefinitely();
// 2. Gửi request thêm món ăn KHÔNG có trong cache menu (menuData null)
CartItemRequest request = new CartItemRequest();
request.menuItemId = UUID.randomUUID();
request.quantity = 2;
given()
.pathParam("cartId", cartId)
.contentType("application/json")
.body(request)
.when()
.post("/{cartId}")
.then()
.statusCode(202) // ACCEPTED
.body(is("PendingVerification"));
// 3. Kiểm tra Redis: hincrby có hoạt động không?
String quantityStr = redisDataSource
.hash(String.class)
.hget("cart:" + cartId + ":items", request.menuItemId.toString())
.await()
.indefinitely();
assert quantityStr.equals("2");
assertEquals(1, results.received().size());
MenuItemValidation payload = results.received().get(0).getPayload();
assertEquals(request.menuItemId, payload.menuItemId);
}
@Test
void testAddItem_Success_WhenMenuInCache() {
InMemorySink<MenuItemValidation> results = connector.sink(
"menu-validation-out"
);
// 1. Chuẩn bị: Cart và Menu đã có sẵn trong Redis cache
String cartId = UUID.randomUUID().toString();
UUID eateryId = UUID.randomUUID();
UUID menuItemId = UUID.randomUUID();
redisDataSource
.value(String.class)
.set("cart:" + cartId, eateryId.toString())
.await()
.indefinitely();
// Giả lập menu đã verify (có data trong cache)
redisDataSource
.value(String.class)
.set("menu:item:" + menuItemId, "{\"name\":\"Pizza\"}")
.await()
.indefinitely();
CartItemRequest request = new CartItemRequest();
request.menuItemId = menuItemId;
request.quantity = 1;
// 2. Thực thi
given()
.pathParam("cartId", cartId)
.contentType("application/json")
.body(request)
.when()
.post("/{cartId}")
.then()
.statusCode(200)
.body(is("ItemAdded"));
// 3. Kiểm tra: Không được gửi Kafka vì đã có trong cache
assertEquals(0, results.received().size());
}
@Test
void testAddItem_CartNotFound() {
CartItemRequest request = new CartItemRequest();
request.menuItemId = UUID.randomUUID();
request.quantity = 1;
given()
.pathParam("cartId", "invalid-cart-id")
.contentType("application/json")
.body(request)
.when()
.post("/{cartId}")
.then()
.statusCode(404)
.body(is("CartNotFound"));
}
}
@@ -0,0 +1,12 @@
# Redis
quarkus.smallrye-graphql-client.eatery-client.url=http://localhost:8089/graphql
# WireMock
quarkus.wiremock.devservices.host=0.0.0.0
quarkus.wiremock.devservices.port=8089
# Kafka
quarkus.kafka.devservices.enabled=false
mp.messaging.outgoing.menu-validation-out.connector=smallrye-in-memory
mp.messaging.incoming.menu-validation-in.connector=smallrye-in-memory
@@ -2,10 +2,8 @@ package com.drinkool.services;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import com.drinkool.InternalValue;
import com.drinkool.Role;