+18
-4
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user