提交一次代码
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user