修复V4复核指针部署证明和运行时诊断

This commit is contained in:
andy
2026-07-21 14:07:35 +07:00
parent 4bf53768ad
commit ab766cf4e2
17 changed files with 586 additions and 27 deletions

View File

@@ -25,6 +25,10 @@ class HealthControllerTest {
mockMvc.perform(get("/api/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.service").value("th-hotel-server"));
.andExpect(jsonPath("$.service").value("th-hotel-server"))
.andExpect(jsonPath("$.runtime_marker").value("m002_v4_review_pointer_deployment_proof_v1"))
.andExpect(jsonPath("$.build_commit").isNotEmpty())
.andExpect(jsonPath("$.build_time").isNotEmpty())
.andExpect(jsonPath("$.build_version").isNotEmpty());
}
}

View File

@@ -0,0 +1,51 @@
package cn.nianxx.thhotel.platform.system.service.impl;
import static org.assertj.core.api.Assertions.assertThat;
import cn.nianxx.thhotel.platform.system.common.result.SystemBuildInfoResult;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.info.BuildProperties;
class SystemBuildInfoServiceImplTest {
@Test
void shouldReturnEmbeddedBuildCommitAsDeploymentProof() {
Properties entries = new Properties();
entries.setProperty("commit", "4bf5376");
entries.setProperty("version", "0.0.1-SNAPSHOT");
entries.setProperty("time", "2026-07-21T05:00:00Z");
SystemBuildInfoServiceImpl service = new SystemBuildInfoServiceImpl(
buildPropertiesProvider(new BuildProperties(entries)));
SystemBuildInfoResult result = service.getBuildInfo();
assertThat(result.runtimeMarker()).isEqualTo("m002_v4_review_pointer_deployment_proof_v1");
assertThat(result.buildCommit()).isEqualTo("4bf5376");
assertThat(result.buildTime()).isEqualTo("2026-07-21T05:00:00Z");
assertThat(result.buildVersion()).isEqualTo("0.0.1-SNAPSHOT");
}
@Test
void shouldReturnUnknownWhenEmbeddedBuildCommitIsUnknown() {
Properties entries = new Properties();
entries.setProperty("commit", "UNKNOWN");
entries.setProperty("version", "0.0.1-SNAPSHOT");
SystemBuildInfoServiceImpl service = new SystemBuildInfoServiceImpl(
buildPropertiesProvider(new BuildProperties(entries)));
SystemBuildInfoResult result = service.getBuildInfo();
assertThat(result.buildCommit()).isEqualTo("UNKNOWN");
}
private ObjectProvider<BuildProperties> buildPropertiesProvider(BuildProperties buildProperties) {
return new ObjectProvider<>() {
@Override
public BuildProperties getIfAvailable() {
return buildProperties;
}
};
}
}

View File

@@ -44,9 +44,12 @@ import java.time.ZoneOffset;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
@@ -67,6 +70,7 @@ import org.springframework.test.web.servlet.MvcResult;
})
@AutoConfigureMockMvc
@ActiveProfiles("test")
@ExtendWith(OutputCaptureExtension.class)
class ReservationV4CommandControllerTest {
private static final String HOTEL_ID = "HOTEL-TEST";
@@ -1248,7 +1252,7 @@ class ReservationV4CommandControllerTest {
}
@Test
void shouldRejectRoomInformationReviewWhenPointerTargetsDerivedReadonlyField() throws Exception {
void shouldRejectRoomInformationReviewWhenPointerTargetsDerivedReadonlyField(CapturedOutput output) throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-review-room-info-readonly-001",
@@ -1294,9 +1298,25 @@ class ReservationV4CommandControllerTest {
{"field_pointer": "/room_information/final_values/nights", "value": 7}
]
}
"""))
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_READONLY"));
org.assertj.core.api.Assertions.assertThat(output.getOut())
.contains("review_pointer_policy=m002_v4_review_pointer_runtime_trace_v1")
.contains("order_task_id=" + seeded.orderTask().id())
.contains("card_id=" + seeded.businessCard().id())
.contains("incoming_pointer=/room_information/final_values/nights")
.contains("query_side_editable_pointers=")
.contains("/room_information/final_values/room_items/0/room_type_code")
.contains("command_side_allowed_pointers=")
.contains("reject_reason=DERIVED_OR_SYSTEM_FIELD_READONLY")
.doesNotContain("target_order")
.doesNotContain("business_fields")
.doesNotContain("raw_evidence")
.doesNotContain("html_body")
.doesNotContain("http://")
.doesNotContain("https://");
}
@Test
@@ -1483,7 +1503,7 @@ class ReservationV4CommandControllerTest {
}
@Test
void shouldRejectReviewResolutionForIllegalPointer() throws Exception {
void shouldRejectReviewResolutionForIllegalPointer(CapturedOutput output) throws Exception {
SeededOrderTask seeded = seedReviewOrderTask(
HOTEL_ID,
"mail-v4-command-review-illegal-pointer-001",
@@ -1508,6 +1528,54 @@ class ReservationV4CommandControllerTest {
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_INVALID"));
org.assertj.core.api.Assertions.assertThat(output.getOut())
.contains("review_pointer_policy=m002_v4_review_pointer_runtime_trace_v1")
.contains("order_task_id=" + seeded.orderTask().id())
.contains("card_id=" + seeded.businessCard().id())
.contains("incoming_pointer=business_fields/room_items/0/pms_room_type_code")
.contains("reject_reason=V4_REVIEW_POINTER_INVALID")
.doesNotContain("target_order")
.doesNotContain("raw_evidence")
.doesNotContain("html_body")
.doesNotContain("http://")
.doesNotContain("https://");
}
@Test
void shouldSanitizeUnsafeReviewPointerInRejectedLogs(CapturedOutput output) throws Exception {
SeededOrderTask seeded = seedReviewOrderTask(
HOTEL_ID,
"mail-v4-command-review-unsafe-pointer-log-001",
Instant.parse("2026-07-19T01:23:30Z"),
990000000000070115L,
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.REVIEW_REQUIRED.name());
confirmBasicCard(seeded);
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
seeded.orderTask().id(),
seeded.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0,
"field_overrides": [
{"field_pointer": "bad/user@example.test/abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGH/https://example.test/raw", "value": "RM2"}
]
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_INVALID"));
org.assertj.core.api.Assertions.assertThat(output.getOut())
.contains("review_pointer_policy=m002_v4_review_pointer_runtime_trace_v1")
.contains("incoming_pointer=bad/[EMAIL]/[TOKEN]/[URL]")
.contains("reject_reason=V4_REVIEW_POINTER_INVALID")
.doesNotContain("user@example.test")
.doesNotContain("abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGH")
.doesNotContain("https://example.test");
}
@Test