提交一次代码

This commit is contained in:
andy
2026-07-12 09:57:54 +08:00
parent 9bd6bc1cd0
commit 6e47158d9e
46 changed files with 3524 additions and 48 deletions

View File

@@ -0,0 +1,57 @@
package cn.nianxx.thhotel.platform.access.repository;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import cn.nianxx.thhotel.platform.access.domain.PlatformRolePermissionEntity;
import cn.nianxx.thhotel.platform.access.domain.PlatformUserRoleEntity;
import cn.nianxx.thhotel.platform.access.mapper.PlatformPermissionMapper;
import cn.nianxx.thhotel.platform.access.mapper.PlatformRoleMapper;
import cn.nianxx.thhotel.platform.access.mapper.PlatformRolePermissionMapper;
import cn.nianxx.thhotel.platform.access.mapper.PlatformUserRoleMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.dao.DuplicateKeyException;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings({"rawtypes", "unchecked"})
class MybatisPlatformAccessRepositoryTest {
@Mock
private PlatformRoleMapper roleMapper;
@Mock
private PlatformPermissionMapper permissionMapper;
@Mock
private PlatformUserRoleMapper userRoleMapper;
@Mock
private PlatformRolePermissionMapper rolePermissionMapper;
@Test
void shouldIgnoreDuplicateKeyWhenEnsuringRolePermission() {
MybatisPlatformAccessRepository repository = repository();
when(rolePermissionMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
doThrow(new DuplicateKeyException("duplicate role permission"))
.when(rolePermissionMapper).insert(any(PlatformRolePermissionEntity.class));
assertThatNoException().isThrownBy(() -> repository.ensureRolePermission(1L, 2L));
}
@Test
void shouldIgnoreDuplicateKeyWhenEnsuringUserRole() {
MybatisPlatformAccessRepository repository = repository();
when(userRoleMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
doThrow(new DuplicateKeyException("duplicate user role"))
.when(userRoleMapper).insert(any(PlatformUserRoleEntity.class));
assertThatNoException().isThrownBy(() -> repository.ensureUserRole(1L, 2L));
}
private MybatisPlatformAccessRepository repository() {
return new MybatisPlatformAccessRepository(roleMapper, permissionMapper, userRoleMapper, rolePermissionMapper);
}
}

View File

@@ -21,6 +21,7 @@ import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStor
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -152,10 +153,63 @@ class DebugEmlSuperAgentControllerTest {
AND superagent_session_id = 'session-debug-001'
AND superagent_run_id = 'run-debug-001'
AND run_label = 'controller-test'
AND safe_error_summary IS NULL
""", Long.class);
org.assertj.core.api.Assertions.assertThat(debugRunCount).isEqualTo(1L);
}
@Test
void shouldRecordPhaseBeforeUploadingOriginalEmlToOss() throws Exception {
AtomicInteger uploadIndex = new AtomicInteger();
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
ObjectStoragePutRequest request = invocation.getArgument(0);
if (uploadIndex.incrementAndGet() == 1) {
Long phaseCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_debug_eml_superagent_run
WHERE run_label = 'phase-debug-upload'
AND run_status = 'UPLOADING_ORIGINAL_EML'
AND safe_error_summary LIKE '%上传原始 EML%'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(phaseCount).isEqualTo(1L);
}
return new ObjectStoragePutResult(
request.objectKey(),
"https://oss.example.test/" + request.objectKey(),
request.contentType(),
request.sizeBytes());
});
when(superAgentOpenApiClient.invokeMailDebug(any())).thenAnswer(invocation -> {
Long phaseCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_debug_eml_superagent_run
WHERE run_label = 'phase-debug-upload'
AND run_status = 'CALLING_SUPERAGENT'
AND safe_error_summary LIKE '%调用 SuperAgent Open API%'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(phaseCount).isEqualTo(1L);
return new SuperAgentOpenApiResult(
"session-debug-phase",
"run-debug-phase",
"profile-debug",
"profile-version-debug",
"debug-model",
"{\"ai_task_results\":[{\"task_type\":\"New Booking\"}]}",
11,
7,
18,
List.of("metadata", "values", "end"));
});
mockMvc.perform(multipart(ENDPOINT)
.file(emlFile())
.param("hotel_id", "HOTEL-TEST")
.param("run_label", "phase-debug-upload")
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("SUPERAGENT_SUCCEEDED"));
}
@Test
void shouldCreateIndependentSourceMessageForRepeatedDebugUpload() throws Exception {
mockStorageAndSuperAgentSuccess();

View File

@@ -23,9 +23,9 @@ class DebugEmlMultipartConfigurationTest {
private DebugEmlSuperAgentProperties debugEmlProperties;
@Test
void shouldAllowMultipartRequestBeforeDebugEmlBusinessLimit() {
void shouldLeaveHeadroomForDebugEmlBusinessLimit() {
assertThat(multipartProperties.getMaxFileSize().toBytes())
.isGreaterThanOrEqualTo(debugEmlProperties.getMaxFileBytes());
.isGreaterThan(debugEmlProperties.getMaxFileBytes());
assertThat(multipartProperties.getMaxRequestSize().toBytes())
.isGreaterThan(debugEmlProperties.getMaxFileBytes());
}

View File

@@ -0,0 +1,62 @@
package cn.nianxx.thhotel.platform.identity.repository;
import static org.assertj.core.api.Assertions.assertThat;
import cn.nianxx.thhotel.ThHotelApplication;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
import java.time.LocalDateTime;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(
classes = ThHotelApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:m003_identity_repository_review;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
"auth.bootstrap.admin.username=",
"auth.bootstrap.admin.password="
})
@ActiveProfiles("test")
class PlatformIdentityRepositoryTest {
@Autowired
private PlatformIdentityRepository identityRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
@AfterEach
void cleanReviewRows() {
jdbcTemplate.update("DELETE FROM platform_user WHERE username LIKE 'm003-review-%'");
}
@Test
void shouldOnlyCountActiveSuperAdminUsers() {
long baseline = identityRepository.countSuperAdminUsers();
identityRepository.insertUser(user("m003-review-disabled-super-admin", PlatformUserStatus.DISABLED.name(), true));
assertThat(identityRepository.countSuperAdminUsers()).isEqualTo(baseline);
identityRepository.insertUser(user("m003-review-active-super-admin", PlatformUserStatus.ACTIVE.name(), true));
assertThat(identityRepository.countSuperAdminUsers()).isEqualTo(baseline + 1);
}
private PlatformUserEntity user(String username, String status, boolean superAdmin) {
LocalDateTime now = LocalDateTime.now();
PlatformUserEntity user = new PlatformUserEntity();
user.setUsername(username);
user.setPasswordHash("{bcrypt}placeholder");
user.setDisplayName(username);
user.setUserStatus(status);
user.setSuperAdmin(superAdmin);
user.setPasswordChangedAt(now);
user.setCreatedAt(now);
user.setUpdatedAt(now);
return user;
}
}

View File

@@ -0,0 +1,104 @@
package cn.nianxx.thhotel.platform.identity.service.impl;
import static org.assertj.core.api.Assertions.assertThat;
import cn.nianxx.thhotel.ThHotelApplication;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(
classes = ThHotelApplication.class,
properties = {
"spring.datasource.url=jdbc:h2:mem:m003_bootstrap_review;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
"auth.bootstrap.admin.username=",
"auth.bootstrap.admin.password=",
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
"auth.bootstrap.default-hotel-name=测试酒店"
})
@ActiveProfiles("test")
class PlatformIdentityBootstrapRunnerTest {
@Autowired
private PlatformIdentityBootstrapRunner bootstrapRunner;
@Autowired
private AuthProperties authProperties;
@Autowired
private JdbcTemplate jdbcTemplate;
@BeforeEach
void cleanReviewRows() {
authProperties.getBootstrap().getAdmin().setUsername("");
authProperties.getBootstrap().getAdmin().setPassword("");
authProperties.getBootstrap().getAdmin().setDisplayName("系统管理员");
jdbcTemplate.update("DELETE FROM platform_user_role WHERE user_id IN (SELECT id FROM platform_user WHERE username LIKE 'm003-review-%')");
jdbcTemplate.update("DELETE FROM platform_user_hotel WHERE user_id IN (SELECT id FROM platform_user WHERE username LIKE 'm003-review-%')");
jdbcTemplate.update("DELETE FROM platform_user WHERE username LIKE 'm003-review-%'");
jdbcTemplate.update("DELETE FROM platform_role_permission WHERE role_id IN (SELECT id FROM platform_role WHERE role_code = 'RESERVATION_VIEWER') AND permission_id IN (SELECT id FROM platform_permission WHERE permission_code = 'SYSTEM_DEBUG_EML_RUN')");
}
@Test
void shouldCreateBootstrapAdminWhenOnlyDisabledSuperAdminExists() {
LocalDateTime now = LocalDateTime.now();
jdbcTemplate.update("""
INSERT INTO platform_user (
id, username, password_hash, display_name, user_status, super_admin,
password_changed_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
9003001001L,
"m003-review-disabled-admin",
"{bcrypt}disabled",
"Disabled Admin",
"DISABLED",
true,
now,
now,
now);
authProperties.getBootstrap().getAdmin().setUsername("m003-review-recovered-admin");
authProperties.getBootstrap().getAdmin().setPassword("Recovered@123456");
authProperties.getBootstrap().getAdmin().setDisplayName("恢复管理员");
bootstrapRunner.run((ApplicationArguments) null);
Integer activeSuperAdminCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_user
WHERE username = 'm003-review-recovered-admin'
AND user_status = 'ACTIVE'
AND super_admin = 1
""", Integer.class);
assertThat(activeSuperAdminCount).isEqualTo(1);
}
@Test
void shouldRemoveStaleBuiltInRolePermissionsDuringBootstrap() {
Long viewerRoleId = jdbcTemplate.queryForObject("""
SELECT id FROM platform_role WHERE role_code = 'RESERVATION_VIEWER'
""", Long.class);
Long debugPermissionId = jdbcTemplate.queryForObject("""
SELECT id FROM platform_permission WHERE permission_code = 'SYSTEM_DEBUG_EML_RUN'
""", Long.class);
jdbcTemplate.update("""
INSERT INTO platform_role_permission (id, role_id, permission_id, created_at)
VALUES (?, ?, ?, ?)
""", 9003002001L, viewerRoleId, debugPermissionId, LocalDateTime.now());
bootstrapRunner.run((ApplicationArguments) null);
Integer staleCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_role_permission
WHERE role_id = ?
AND permission_id = ?
""", Integer.class, viewerRoleId, debugPermissionId);
assertThat(staleCount).isZero();
}
}

View File

@@ -25,6 +25,8 @@ import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepositor
import cn.nianxx.thhotel.platform.message.service.impl.SourceMessageCaptureServiceImpl;
import cn.nianxx.thhotel.platform.message.service.impl.SourceMessageSafetySanitizer;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
@@ -33,6 +35,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
@@ -221,6 +224,78 @@ class SourceMessageCaptureServiceImplTest {
assertThat(inbox.getUpdatedAt()).isEqualTo(inbox.getCreatedAt());
}
@Test
void shouldCaptureCaseVariantExternalMessageIdsAsDifferentMessages() {
CaptureSourceMessageCommand upperCaseCommand = new CaptureSourceMessageCommand(
"HOTEL-TEST",
"AGENTBUS",
"EMAIL",
"mail-case-sensitive-X",
"conversation-case-sensitive",
"frame-case-sensitive-001",
"session-case-sensitive",
Instant.parse("2026-07-06T09:20:00Z"),
"guest@example.test",
"Case sensitive delivery",
"Upper case external id",
"<html>Upper case external id</html>",
"{\"source\":{\"external_message_id\":\"mail-case-sensitive-X\"},\"version\":1}",
"agentbus-outlook-v1",
List.of()
);
CaptureSourceMessageCommand lowerCaseCommand = new CaptureSourceMessageCommand(
"HOTEL-TEST",
"AGENTBUS",
"EMAIL",
"mail-case-sensitive-x",
"conversation-case-sensitive",
"frame-case-sensitive-002",
"session-case-sensitive",
Instant.parse("2026-07-06T09:21:00Z"),
"guest@example.test",
"Case sensitive delivery",
"Lower case external id",
"<html>Lower case external id</html>",
"{\"source\":{\"external_message_id\":\"mail-case-sensitive-x\"},\"version\":1}",
"agentbus-outlook-v1",
List.of()
);
SourceMessageCaptureResult upperCaseResult = captureService.capture(upperCaseCommand);
SourceMessageCaptureResult lowerCaseResult = captureService.capture(lowerCaseCommand);
assertThat(upperCaseResult.created()).isTrue();
assertThat(lowerCaseResult.created()).isTrue();
assertThat(lowerCaseResult.inboxId()).isNotEqualTo(upperCaseResult.inboxId());
List<String> externalMessageIds = inboxMapper.selectList(Wrappers.<SourceMessageInboxEntity>lambdaQuery()
.eq(SourceMessageInboxEntity::getHotelId, "HOTEL-TEST")
.eq(SourceMessageInboxEntity::getProvider, "AGENTBUS")
.eq(SourceMessageInboxEntity::getChannel, "EMAIL")
.eq(SourceMessageInboxEntity::getExternalConversationId, "conversation-case-sensitive")
.orderByAsc(SourceMessageInboxEntity::getExternalMessageId))
.stream()
.map(SourceMessageInboxEntity::getExternalMessageId)
.toList();
assertThat(externalMessageIds)
.containsExactlyInAnyOrder("mail-case-sensitive-X", "mail-case-sensitive-x");
}
@Test
void shouldProvideMysqlMigrationForCaseSensitiveStringCollation() throws IOException {
ClassPathResource migration = new ClassPathResource(
"db/migration/V13__make_existing_string_columns_case_sensitive.sql");
assertThat(migration.exists()).isTrue();
String sql = migration.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql).contains("DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin");
assertThat(sql).contains("ALTER TABLE platform_source_message_inbox");
assertThat(sql).contains(
"MODIFY COLUMN external_message_id VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin");
assertThat(sql).contains("ALTER TABLE platform_source_message_payload_duplicate");
}
@Test
void shouldPersistFailedInboxWhenExternalMessageIdIsMissing() {
CaptureSourceMessageCommand command = new CaptureSourceMessageCommand(