修复 V4 查询接口安全与筛选问题

This commit is contained in:
andy
2026-07-19 10:13:57 +07:00
parent dc6c8f48ab
commit 9404a0e5c1
7 changed files with 348 additions and 26 deletions

View File

@@ -19,16 +19,22 @@ import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
import cn.nianxx.thhotel.workflows.reservation.common.enums.AiResultType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationSystemTaskType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4TargetResolutionStatus;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
import java.time.Instant;
@@ -73,6 +79,9 @@ class ReservationV4QueryControllerTest {
@Autowired
private ReservationV4WorkflowRepository workflowRepository;
@Autowired
private ReservationAiWorkflowRepository aiWorkflowRepository;
@Autowired
private ReservationV4SourceNotificationRepository sourceNotificationRepository;
@Autowired
@@ -136,6 +145,7 @@ class ReservationV4QueryControllerTest {
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
.param("hotel_id", HOTEL_ID)
.param("keyword", "mail-v4-query-workbench")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
@@ -159,6 +169,64 @@ class ReservationV4QueryControllerTest {
.andExpect(content().string(not(containsString("Sensitive raw notification body"))));
}
@Test
void shouldReturnAdapterContractErrorsOnV4OrderTaskDetail() throws Exception {
Instant receivedAt = Instant.parse("2026-07-18T03:10:00Z");
SourceMessageCaptureResult source = captureSourceMessage(
"mail-v4-query-adapter-error-001",
"V4 Query Adapter Error",
receivedAt,
HOTEL_ID);
Long batchId = insertAiBatch(source.inboxId(), LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC));
insertAdapterContractErrorTransition(
source.inboxId(),
batchId,
LocalDateTime.ofInstant(receivedAt.plusSeconds(1), ZoneOffset.UTC));
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
source,
batchId,
receivedAt,
"order-adapter-error",
"GRP-V4-ADAPTER-001");
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.adapter_contract_errors.length()").value(1))
.andExpect(jsonPath("$.adapter_contract_errors[0].system_process_category")
.value("ADAPTER_CONTRACT_ERROR"))
.andExpect(jsonPath("$.adapter_contract_errors[0].adapter_error_code")
.value("V4_PAYMENT_ATTACHMENT_NOT_FOUND"))
.andExpect(jsonPath("$.adapter_contract_errors[0].payload_fragment.contract_errors[0]")
.value("attachment_ids 未匹配来源附件"))
.andExpect(content().string(not(containsString("raw_sensitive_payload"))));
}
@Test
void shouldNotExposeCrossHotelSourceMessageSummaryWhenV4ReferenceIsPolluted() throws Exception {
Instant receivedAt = Instant.parse("2026-07-18T03:20:00Z");
SourceMessageCaptureResult otherSource = captureSourceMessage(
"mail-v4-query-cross-source-001",
"Other Hotel Secret Subject",
receivedAt,
OTHER_HOTEL_ID);
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
otherSource,
990000000000000901L,
receivedAt,
"order-cross-source",
"GRP-V4-CROSS-SOURCE-001");
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.source_message_summary.source_message_id")
.value(otherSource.inboxId().toString()))
.andExpect(jsonPath("$.source_message_summary.subject").doesNotExist())
.andExpect(jsonPath("$.source_message_summary.external_message_id").doesNotExist())
.andExpect(content().string(not(containsString("Other Hotel Secret Subject"))));
}
@Test
void shouldReturnV4OrderTaskDetailWithSafeCards() throws Exception {
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask("mail-v4-query-detail-001",
@@ -203,6 +271,45 @@ class ReservationV4QueryControllerTest {
.andExpect(jsonPath("$.page.total").value(1));
}
@Test
void shouldNotMatchReadonlySourceMessageDisplayCardWhenFilteringBusinessCards() throws Exception {
seedOrderTask("mail-v4-query-readonly-filter-001",
Instant.parse("2026-07-18T03:40:00Z"));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
.param("hotel_id", HOTEL_ID)
.param("card_status", "READONLY")
.param("keyword", "mail-v4-query-readonly-filter-001")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items.length()").value(0))
.andExpect(jsonPath("$.page.total").value(0));
}
@Test
void shouldSortSameReceivedAtWorkbenchItemsByUpdatedAtAndId() throws Exception {
Instant sameReceivedAt = Instant.parse("2026-07-18T03:50:00Z");
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
"mail-v4-query-same-time-notification-001",
"S10",
sameReceivedAt);
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
"mail-v4-query-same-time-order-001",
sameReceivedAt);
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
.param("hotel_id", HOTEL_ID)
.param("keyword", "mail-v4-query-same-time")
.param("page_num", "1")
.param("page_size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].item_type").value("SOURCE_NOTIFICATION"))
.andExpect(jsonPath("$.items[0].target_id").value(notification.id().toString()))
.andExpect(jsonPath("$.items[1].item_type").value("ORDER_TASK"))
.andExpect(jsonPath("$.items[1].target_id").value(orderTask.id().toString()));
}
@Test
void shouldReturnV4SourceNotificationDetailWithoutRawPayload() throws Exception {
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
@@ -296,6 +403,21 @@ class ReservationV4QueryControllerTest {
.andExpect(jsonPath("$.error_code").value("V4_WORKBENCH_ITEM_TYPE_INVALID"));
}
@Test
void shouldRejectUnsupportedOrderTaskStatusAndCardStatus() throws Exception {
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
.param("hotel_id", HOTEL_ID)
.param("order_task_status", "UNKNOWN"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_ORDER_TASK_STATUS_INVALID"));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
.param("hotel_id", HOTEL_ID)
.param("card_status", "UNKNOWN"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_CARD_STATUS_INVALID"));
}
@Test
void shouldCapHugeWorkbenchPageSafely() throws Exception {
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
@@ -308,18 +430,32 @@ class ReservationV4QueryControllerTest {
}
private ReservationV4OrderTaskSnapshot seedOrderTask(String externalMessageId, Instant receivedAt) {
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt);
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt, HOTEL_ID);
return seedOrderTask(
source,
990000000000000001L,
receivedAt,
"order-1",
"GRP-V4-QUERY-001");
}
private ReservationV4OrderTaskSnapshot seedOrderTask(
SourceMessageCaptureResult source,
Long aiBatchId,
Instant receivedAt,
String orderRef,
String targetLocatorValue) {
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
HOTEL_ID,
source.inboxId(),
990000000000000001L,
"order-1",
aiBatchId,
orderRef,
1,
null,
"GROUP",
"GROUP_CODE",
"GRP-V4-QUERY-001",
targetLocatorValue,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4OrderTaskStatus.OPEN.name(),
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
@@ -346,7 +482,8 @@ class ReservationV4QueryControllerTest {
SourceMessageCaptureResult source = captureSourceMessage(
externalMessageId,
"V4 Query Notification",
receivedAt);
receivedAt,
HOTEL_ID);
return sourceNotificationRepository.findOrCreateSourceNotification(new ReservationV4SourceNotificationDraft(
HOTEL_ID,
source.inboxId(),
@@ -392,9 +529,10 @@ class ReservationV4QueryControllerTest {
private SourceMessageCaptureResult captureSourceMessage(
String externalMessageId,
String subject,
Instant receivedAt) {
Instant receivedAt,
String hotelId) {
return captureService.capture(new CaptureSourceMessageCommand(
HOTEL_ID,
hotelId,
"AGENTBUS",
"EMAIL",
externalMessageId,
@@ -412,4 +550,57 @@ class ReservationV4QueryControllerTest {
List.of()
));
}
private Long insertAiBatch(Long sourceMessageId, LocalDateTime receivedAt) {
return aiWorkflowRepository.insertBatch(new ReservationAiBatchDraft(
HOTEL_ID,
sourceMessageId,
"sha256-v4-query-adapter-error",
"batch-v4-query-adapter-error",
"superagent-test",
"request-v4-query-adapter-error",
receivedAt,
2,
null));
}
private void insertAdapterContractErrorTransition(Long sourceMessageId, Long batchId, LocalDateTime now) {
aiWorkflowRepository.insertTransition(new ReservationAiTransitionDraft(
HOTEL_ID,
batchId,
sourceMessageId,
2,
2,
2,
"P0",
"reservation-v4",
AiResultType.ADAPTER_CONTRACT_ERROR.code(),
"adapter_contract_error",
null,
ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name(),
ReservationSystemTaskType.ADAPTER_CONTRACT_ERROR.name(),
"ADAPTER_CONTRACT_ERROR",
null,
"current",
null,
null,
"sha256-v4-query-adapter-error-item",
"item-v4-query-adapter-error",
null,
null,
null,
false,
"""
{"event_type":"PAYMENT","source_event_index":2,"contract_errors":["attachment_ids 未匹配来源附件"],"raw_sensitive_payload":"must not leak"}
""",
null,
null,
null,
null,
null,
null,
"V4_PAYMENT_ATTACHMENT_NOT_FOUND",
"PAYMENT.attachment_ids 未匹配来源附件。",
now));
}
}