修复V4目录搜索与幂等审计

This commit is contained in:
andy
2026-07-19 23:44:59 +07:00
parent f2c2616ab0
commit 739fccd955
5 changed files with 184 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.workflows.reservation.control;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
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;
@@ -22,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -60,6 +62,9 @@ class ReservationV4CatalogAdminControllerTest {
@Autowired
private AuthPasswordService passwordService;
@Autowired
private JdbcTemplate jdbcTemplate;
private String adminToken;
private String noPermissionToken;
@@ -188,6 +193,30 @@ class ReservationV4CatalogAdminControllerTest {
.andReturn();
String rateCodeId = objectMapper.readTree(rateCreated.getResponse().getContentAsString()).path("id").asText();
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"code":"CP1_ROOM",
"display_name":"CP1 Room Duplicate"
}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.error_code").value("RESERVATION_CATALOG_CONFLICT"));
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/rate-codes")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"code":"CP1_RATE",
"display_name":"CP1 Rate Duplicate"
}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.error_code").value("RESERVATION_CATALOG_CONFLICT"));
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/rate-codes/{catalogId}/status", rateCodeId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
@@ -257,6 +286,100 @@ class ReservationV4CatalogAdminControllerTest {
.andExpect(jsonPath("$.items[0].code").value("CP1_DISABLE_ROOM"));
}
@Test
void shouldSearchCatalogAdminCodeIgnoringKeywordCase() throws Exception {
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"account_code":"CP1_CASE_ACCOUNT",
"account_name":"CP1 Case Account",
"market_code":"LEISURE",
"source_code":"TRAVEL_AGENT"
}
"""))
.andExpect(status().isOk());
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"code":"CP1_CASE_ROOM",
"display_name":"CP1 Case Room"
}
"""))
.andExpect(status().isOk());
performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/rate-codes")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"code":"CP1_CASE_RATE",
"display_name":"CP1 Case Rate"
}
"""))
.andExpect(status().isOk());
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/accounts")
.param("hotel_id", HOTEL_ID)
.param("keyword", "cp1_case_account"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].account_code").value("CP1_CASE_ACCOUNT"));
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/room-types")
.param("hotel_id", HOTEL_ID)
.param("keyword", "cp1_case_room"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].code").value("CP1_CASE_ROOM"));
performAuthorized(mockMvc, adminToken(), get("/api/admin/reservation/catalogs/rate-codes")
.param("hotel_id", HOTEL_ID)
.param("keyword", "cp1_case_rate"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].code").value("CP1_CASE_RATE"));
}
@Test
void shouldNotWriteAuditWhenStatusUpdateIsNoop() throws Exception {
MvcResult created = performAuthorized(mockMvc, adminToken(), post("/api/admin/reservation/catalogs/room-types")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"hotel_id":"HOTEL-TEST",
"code":"CP1_NOOP_AUDIT_ROOM",
"display_name":"CP1 Noop Audit Room"
}
"""))
.andExpect(status().isOk())
.andReturn();
String roomTypeId = objectMapper.readTree(created.getResponse().getContentAsString()).path("id").asText();
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/room-types/{catalogId}/status", roomTypeId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"status":"DISABLED"}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("DISABLED"));
Long afterFirstUpdate = countCatalogStatusAudits(roomTypeId, "UPDATE_ROOM_TYPE_CATALOG_STATUS");
assertThat(afterFirstUpdate).isEqualTo(1L);
performAuthorized(mockMvc, adminToken(), put("/api/admin/reservation/catalogs/room-types/{catalogId}/status", roomTypeId)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"status":"DISABLED"}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("DISABLED"));
Long afterNoopUpdate = countCatalogStatusAudits(roomTypeId, "UPDATE_ROOM_TYPE_CATALOG_STATUS");
assertThat(afterNoopUpdate).isEqualTo(1L);
}
@Test
void shouldRejectCatalogAdminWithoutPermissionOrHotelAccess() throws Exception {
mockMvc.perform(get("/api/admin/reservation/catalogs/accounts")
@@ -287,4 +410,17 @@ class ReservationV4CatalogAdminControllerTest {
}
return noPermissionToken;
}
private Long countCatalogStatusAudits(String targetId, String action) {
return jdbcTemplate.queryForObject("""
SELECT COUNT(1)
FROM platform_admin_audit_log
WHERE target_type = 'RESERVATION_V4_CATALOG_CODE'
AND target_id = ?
AND action = ?
""",
Long.class,
targetId,
action);
}
}

View File

@@ -118,6 +118,27 @@ class ReservationV4CatalogLookupControllerTest {
.andExpect(jsonPath("$.items[0].pricing_available").value(false));
}
@Test
void shouldSearchLookupCodeIgnoringKeywordCase() throws Exception {
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/accounts")
.param("hotel_id", HOTEL_ID)
.param("keyword", "qbd_travel"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].code").value("QBD_TRAVEL"));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/room-types")
.param("hotel_id", HOTEL_ID)
.param("keyword", "rm2"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].code").value("RM2"));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/rate-codes")
.param("hotel_id", HOTEL_ID)
.param("keyword", "group"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].code").value("GROUP"));
}
@Test
void shouldKeepCatalogMetadataWhenKeywordMatchesNothing() throws Exception {
performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/accounts")