再次提交一下代码

This commit is contained in:
andy
2026-07-17 13:13:47 +07:00
parent f4d86248d4
commit 980a3a2535
41 changed files with 1699 additions and 187 deletions

View File

@@ -75,6 +75,7 @@ class AuthControllerTest {
"RESERVATION_TASK_CONFIRM",
"RESERVATION_OPERA_SIM_EXECUTE",
"RESERVATION_AUDIT_READ",
"RESERVATION_INVOICE_GENERATE",
"HOTEL_SWITCH",
"SYSTEM_AUTH_READ",
"SYSTEM_USER_MANAGE",

View File

@@ -28,7 +28,6 @@ import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest(
classes = ThHotelApplication.class,
properties = {
"source-message.original-read.access-key=test-original-read-key",
"spring.datasource.url=jdbc:h2:mem:source_message_controller_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
"auth.bootstrap.admin.username=source-message-admin",
"auth.bootstrap.admin.password=Admin@123456",
@@ -63,6 +62,18 @@ class SourceMessageControllerTest {
return adminToken;
}
/**
* 按用户名查询稳定用户 ID用于校验原文读取审计 actor 不受用户名变更影响。
*/
private String frontendActorId(String username) {
Long userId = jdbcTemplate.queryForObject("""
SELECT id
FROM platform_user
WHERE username = ?
""", Long.class, username);
return "frontend_user:" + userId;
}
@Test
void shouldListAndReadSummaryWithoutOriginalContentOrMediaUrls() throws Exception {
SourceMessageCaptureResult result = captureService.capture(command(
@@ -101,7 +112,7 @@ class SourceMessageControllerTest {
}
@Test
void shouldRejectOriginalReadWithoutAccessKey() throws Exception {
void shouldRejectOriginalReadWithoutLoginToken() throws Exception {
SourceMessageCaptureResult result = captureService.capture(command(
"mail-original-denied-001",
"conversation-original-denied-001",
@@ -111,13 +122,13 @@ class SourceMessageControllerTest {
));
mockMvc.perform(get("/api/source-messages/{id}/original", result.inboxId())
.header("X-TH-Hotel-Actor", "operator-001")
.header("X-TH-Hotel-Access-Scene", "reservation-detail"))
.andExpect(status().isForbidden());
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
}
@Test
void shouldReadOriginalContentWithAccessKeyAndRecordAudit() throws Exception {
void shouldReadOriginalContentWithLoginPermissionAndRecordCurrentUserAudit() throws Exception {
SourceMessageCaptureResult result = captureService.capture(command(
"mail-original-001",
"conversation-original-001",
@@ -126,8 +137,7 @@ class SourceMessageControllerTest {
"https://media.example.test/original.pdf"
));
mockMvc.perform(get("/api/source-messages/{id}/original", result.inboxId())
.header("X-TH-Hotel-Source-Original-Read-Key", "test-original-read-key")
performAuthorized(mockMvc, adminToken(), get("/api/source-messages/{id}/original", result.inboxId())
.header("X-TH-Hotel-Actor", "operator-001")
.header("X-TH-Hotel-Access-Scene", "reservation-detail"))
.andExpect(status().isOk())
@@ -143,9 +153,9 @@ class SourceMessageControllerTest {
SELECT COUNT(*)
FROM platform_source_message_original_access_audit
WHERE inbox_id = ?
AND actor_id = 'operator-001'
AND access_scene = 'reservation-detail'
""", Long.class, result.inboxId());
AND actor_id = ?
AND access_scene = 'source-message-original'
""", Long.class, result.inboxId(), frontendActorId("source-message-admin"));
assert auditCount != null;
org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(1L);
}
@@ -177,7 +187,8 @@ class SourceMessageControllerTest {
insertRelatedTransition(transitionId, first.inboxId(), "GRP-CONVERSATION-P0-001");
insertRelatedTask(taskId, orderId, first.inboxId(), transitionId);
mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", first.inboxId()))
performAuthorized(mockMvc, adminToken(), get("/api/source-messages/{sourceMessageId}/conversation",
first.inboxId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.conversation.external_conversation_id").value("conversation-p0-001"))
.andExpect(jsonPath("$.conversation.message_count").value(2))
@@ -215,9 +226,9 @@ class SourceMessageControllerTest {
SELECT COUNT(*)
FROM platform_source_message_original_access_audit
WHERE inbox_id IN (?, ?)
AND actor_id = 'system:source-message-conversation'
AND actor_id = ?
AND access_scene = 'source-message-conversation'
""", Long.class, first.inboxId(), second.inboxId());
""", Long.class, first.inboxId(), second.inboxId(), frontendActorId("source-message-admin"));
org.assertj.core.api.Assertions.assertThat(auditCount).isEqualTo(2L);
}
@@ -236,7 +247,8 @@ class SourceMessageControllerTest {
insertRelatedTransition(transitionId, sourceMessage.inboxId(), "TMP-HIDDEN-CONVERSATION-001");
insertRelatedTask(taskId, orderId, sourceMessage.inboxId(), transitionId);
mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", sourceMessage.inboxId()))
performAuthorized(mockMvc, adminToken(), get("/api/source-messages/{sourceMessageId}/conversation",
sourceMessage.inboxId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.messages[0].related_orders.length()").value(0))
.andExpect(jsonPath("$.messages[0].related_tasks[0].task_id").value(taskId.toString()))

View File

@@ -8,6 +8,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleStatus;
import cn.nianxx.thhotel.platform.access.domain.PlatformPermissionEntity;
import cn.nianxx.thhotel.platform.access.domain.PlatformRoleEntity;
import cn.nianxx.thhotel.platform.access.repository.PlatformAccessRepository;
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
@@ -49,6 +54,9 @@ class FrontendReadAuthorizationControllerTest {
private static final String HOTEL_ID = "HOTEL-TEST";
private static final String OTHER_HOTEL_ID = "HOTEL-OTHER";
private static final String ORIGINAL_ONLY_USERNAME = "cp2-original-only";
private static final String ORIGINAL_ONLY_PASSWORD = "OriginalOnly@123456";
private static final String ORIGINAL_ONLY_ROLE_CODE = "CP2_ORIGINAL_ONLY";
@Autowired
private MockMvc mockMvc;
@@ -59,6 +67,8 @@ class FrontendReadAuthorizationControllerTest {
@Autowired
private PlatformIdentityRepository identityRepository;
@Autowired
private PlatformAccessRepository accessRepository;
@Autowired
private PlatformHotelRepository hotelRepository;
@Autowired
private AuthPasswordService passwordService;
@@ -81,6 +91,7 @@ class FrontendReadAuthorizationControllerTest {
return created;
});
hotelRepository.ensureUserHotel(user.getId(), HOTEL_ID, true);
ensureOriginalOnlyUser();
}
@Test
@@ -119,6 +130,44 @@ class FrontendReadAuthorizationControllerTest {
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
}
@Test
void shouldRejectSourceMessageOriginalAndConversationWhenTokenMissing() throws Exception {
mockMvc.perform(get("/api/source-messages/{id}/original", 970000000000000901L))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
mockMvc.perform(get("/api/source-messages/{id}/conversation", 970000000000000901L))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
}
@Test
void shouldRejectSourceMessageOriginalAndConversationWhenOriginalPermissionMissing() throws Exception {
String token = loginToken(mockMvc, "cp1-no-permission", "NoPerm@123456");
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", 970000000000000902L))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", 970000000000000902L))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
}
@Test
void shouldRejectSourceMessageOriginalAndConversationWhenSummaryPermissionMissing() throws Exception {
String token = loginToken(mockMvc, ORIGINAL_ONLY_USERNAME, ORIGINAL_ONLY_PASSWORD);
SourceMessageCaptureResult source = captureHotelSourceMessage();
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", source.inboxId()))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", source.inboxId()))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
}
@Test
void shouldRejectSourceMessageListAcrossHotelsWhenSnakeHotelIdProvided() throws Exception {
String token = loginToken(mockMvc, "cp1-admin", "Admin@123456");
@@ -154,6 +203,20 @@ class FrontendReadAuthorizationControllerTest {
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
}
@Test
void shouldRejectSourceMessageOriginalAndConversationAcrossHotels() throws Exception {
String token = loginToken(mockMvc, "cp1-admin", "Admin@123456");
SourceMessageCaptureResult source = captureOtherHotelSourceMessage();
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/original", source.inboxId()))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
performAuthorized(mockMvc, token, get("/api/source-messages/{id}/conversation", source.inboxId()))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
}
@Test
void shouldKeepSuperAgentTaskResultEndpointOutsideFrontendLoginInterceptor() throws Exception {
mockMvc.perform(post("/api/integrations/superagent/task-results")
@@ -207,6 +270,62 @@ class FrontendReadAuthorizationControllerTest {
List.of()));
}
private SourceMessageCaptureResult captureHotelSourceMessage() {
return captureService.capture(new CaptureSourceMessageCommand(
HOTEL_ID,
"AGENTBUS",
"EMAIL",
"cp2-hotel-mail-001",
"cp2-hotel-thread-001",
"frame-cp2-hotel",
"session-cp2-hotel",
Instant.parse("2026-07-13T02:00:00Z"),
"guest@example.test",
"Current hotel message",
"Current hotel body",
"<html><body>Current hotel body</body></html>",
"{\"source\":{\"external_message_id\":\"cp2-hotel-mail-001\"}}",
"agentbus-outlook-v1",
List.of()));
}
private void ensureOriginalOnlyUser() {
PlatformUserEntity user = identityRepository.findUserByUsername(ORIGINAL_ONLY_USERNAME)
.orElseGet(() -> {
LocalDateTime now = LocalDateTime.now();
PlatformUserEntity created = new PlatformUserEntity();
created.setUsername(ORIGINAL_ONLY_USERNAME);
created.setPasswordHash(passwordService.hash(ORIGINAL_ONLY_PASSWORD));
created.setDisplayName("仅原文权限用户");
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
created.setSuperAdmin(false);
created.setPasswordChangedAt(now);
created.setCreatedAt(now);
created.setUpdatedAt(now);
identityRepository.insertUser(created);
return created;
});
PlatformRoleEntity role = accessRepository.findRoleByCode(ORIGINAL_ONLY_ROLE_CODE)
.orElseGet(() -> {
LocalDateTime now = LocalDateTime.now();
PlatformRoleEntity created = new PlatformRoleEntity();
created.setRoleCode(ORIGINAL_ONLY_ROLE_CODE);
created.setRoleName("CP2 仅原文权限测试角色");
created.setRoleStatus(PlatformRoleStatus.ACTIVE.name());
created.setSystemBuiltin(false);
created.setCreatedAt(now);
created.setUpdatedAt(now);
accessRepository.insertRole(created);
return created;
});
PlatformPermissionEntity originalPermission = accessRepository
.findPermissionByCode(PlatformPermissionCode.SOURCE_MESSAGE_ORIGINAL_READ.name())
.orElseThrow();
accessRepository.replaceRolePermissions(role.getId(), List.of(originalPermission.getId()));
accessRepository.ensureUserRole(user.getId(), role.getId());
hotelRepository.ensureUserHotel(user.getId(), HOTEL_ID, true);
}
private void insertOtherHotelOrderTask(Long orderId, Long taskId, Long sourceMessageId) {
Long transitionId = taskId - 1;
jdbcTemplate.update("""

View File

@@ -133,7 +133,8 @@ class ReservationDemoDataControllerTest {
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("FAILED"))
.andExpect(jsonPath("$.opera_operations[0].attempt_count").value(1));
mockMvc.perform(get("/api/source-messages/{sourceMessageId}/conversation", queueSourceMessageId))
performAuthorized(mockMvc, adminToken(), get("/api/source-messages/{sourceMessageId}/conversation",
queueSourceMessageId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.messages", hasSize(greaterThanOrEqualTo(2))))
.andExpect(jsonPath("$.messages[0].html_sanitize_required").value(true))