You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
12 KiB
252 lines
12 KiB
package com.crm.dict.config;
|
|
|
|
import com.crm.dict.AbstractDictH2Test;
|
|
import com.crm.dict.constant.DictConstants;
|
|
import com.crm.dict.domain.dto.DictItemDTO;
|
|
import com.crm.dict.domain.entity.DictGroup;
|
|
import com.crm.dict.domain.entity.DictGroupDefault;
|
|
import com.crm.dict.domain.entity.DictItem;
|
|
import com.crm.dict.domain.entity.DictRefCount;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
/**
|
|
* T08 内置字典幂等初始化:首装建库 / 幂等重跑 / 只改内置不碰用户数据 / 默认项不覆盖 / 只加不删 / 种子校验 fail-fast
|
|
* <p>种子注入走 {@link DictDataInitializer#run(List)} 包私有入口(Excel 替换口的同构接入点)。</p>
|
|
*/
|
|
@DisplayName("T08 内置字典幂等初始化")
|
|
class DictDataInitializerTest extends AbstractDictH2Test {
|
|
|
|
private DictDataInitializer initializer;
|
|
|
|
@BeforeEach
|
|
void setUpInitializer() {
|
|
initializer = new DictDataInitializer(dictGroupMapper, dictItemMapper,
|
|
dictGroupDefaultMapper, dictRefCountMapper);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首装:3 个内置分组 + 17 个内置项全部建库,ref_count 预建,种子默认项生效")
|
|
void firstRun_createsBuiltinDictsWithDefaults() {
|
|
initializer.run();
|
|
|
|
List<DictGroup> groups = dictGroupMapper.selectList(null);
|
|
assertThat(groups).hasSize(3);
|
|
assertThat(groups).extracting(DictGroup::getCode)
|
|
.containsExactlyInAnyOrder("lead_source", "customer_status", "industry");
|
|
assertThat(groups).allMatch(g -> Boolean.TRUE.equals(g.getBuiltin()));
|
|
assertThat(groups).allMatch(g -> g.getStatus() == DictConstants.STATUS_ENABLED);
|
|
|
|
List<DictItem> items = dictItemMapper.selectList(null);
|
|
assertThat(items).hasSize(17); // 6 + 3 + 8
|
|
assertThat(items).allMatch(i -> Boolean.TRUE.equals(i.getBuiltin()));
|
|
|
|
// 引用计数行预建(与新建接口同口径)
|
|
assertThat(dictRefCountMapper.selectCount(null)).isEqualTo(17);
|
|
DictGroup statusGroup = groups.stream()
|
|
.filter(g -> g.getCode().equals("customer_status")).findFirst().orElseThrow();
|
|
DictItem potential = dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getGroupId().equals(statusGroup.getId()) && i.getCode().equals("potential"))
|
|
.findFirst().orElseThrow();
|
|
// 种子默认项生效
|
|
DictGroupDefault def = dictGroupDefaultMapper.selectById(statusGroup.getId());
|
|
assertThat(def).isNotNull();
|
|
assertThat(def.getItemId()).isEqualTo(potential.getId());
|
|
// 默认项自身在启用列表中
|
|
assertThat(dictQueryService.listEnabledItems("customer_status"))
|
|
.extracting(DictItemDTO::getCode).contains("potential");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("幂等:重复执行不产生重复分组/项/计数行/默认行")
|
|
void rerun_idempotent() {
|
|
initializer.run();
|
|
initializer.run();
|
|
|
|
assertThat(dictGroupMapper.selectCount(null)).isEqualTo(3);
|
|
assertThat(dictItemMapper.selectCount(null)).isEqualTo(17);
|
|
assertThat(dictRefCountMapper.selectCount(null)).isEqualTo(17);
|
|
assertThat(dictGroupDefaultMapper.selectCount(null)).isEqualTo(1);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("更新:内置项按种子刷新名称;builtin=false 用户数据绝不触碰")
|
|
void update_refreshesBuiltinButNeverTouchesUserData() {
|
|
initializer.run();
|
|
|
|
// 管理员本地化:改内置项名 + 手建用户项(builtin=false)
|
|
DictGroup leadSource = dictGroupMapper.selectList(null).stream()
|
|
.filter(g -> g.getCode().equals("lead_source")).findFirst().orElseThrow();
|
|
DictItem adItem = dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getGroupId().equals(leadSource.getId()) && i.getCode().equals("ad"))
|
|
.findFirst().orElseThrow();
|
|
adItem.setName("广告投放-本地改名");
|
|
adItem.setValue("LOCAL_VALUE"); // 管理员本地化改值
|
|
dictItemMapper.updateById(adItem);
|
|
|
|
DictItem userItem = new DictItem();
|
|
userItem.setGroupId(leadSource.getId());
|
|
userItem.setCode("user_created");
|
|
userItem.setName("用户自建项");
|
|
userItem.setValue("USER_VALUE");
|
|
userItem.setStatus(DictConstants.STATUS_ENABLED);
|
|
userItem.setBuiltin(false);
|
|
userItem.setDeleteKey(0L);
|
|
dictItemMapper.insert(userItem);
|
|
|
|
initializer.run();
|
|
|
|
// 内置项名被种子刷新(种子的"改"语义),但字典值属业务契约不被覆盖
|
|
DictItem refreshed = dictItemMapper.selectById(adItem.getId());
|
|
assertThat(refreshed.getName()).isEqualTo("广告投放");
|
|
assertThat(refreshed.getValue()).isEqualTo("LOCAL_VALUE");
|
|
// 用户项原样保留:名称/值/内置标记均未动
|
|
DictItem untouched = dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getCode().equals("user_created")).findFirst().orElseThrow();
|
|
assertThat(untouched.getName()).isEqualTo("用户自建项");
|
|
assertThat(untouched.getValue()).isEqualTo("USER_VALUE");
|
|
assertThat(untouched.getBuiltin()).isFalse();
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("默认项:管理员已设默认不覆盖;种子默认项停用后不复活")
|
|
void defaultItem_notOverwritten() {
|
|
initializer.run();
|
|
DictGroup statusGroup = dictGroupMapper.selectList(null).stream()
|
|
.filter(g -> g.getCode().equals("customer_status")).findFirst().orElseThrow();
|
|
|
|
// 管理员改设 active 为默认 → 重跑种子不得覆盖
|
|
DictItem active = dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getGroupId().equals(statusGroup.getId()) && i.getCode().equals("active"))
|
|
.findFirst().orElseThrow();
|
|
dictGroupDefaultMapper.deleteById(statusGroup.getId());
|
|
DictGroupDefault adminDefault = new DictGroupDefault();
|
|
adminDefault.setGroupId(statusGroup.getId());
|
|
adminDefault.setItemId(active.getId());
|
|
dictGroupDefaultMapper.insert(adminDefault);
|
|
|
|
initializer.run();
|
|
assertThat(dictGroupDefaultMapper.selectById(statusGroup.getId()).getItemId())
|
|
.isEqualTo(active.getId()); // 管理员默认保留
|
|
|
|
// 停用种子默认项(停用自动取消默认)→ 重跑种子不得复活
|
|
DictItem potential = dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getGroupId().equals(statusGroup.getId()) && i.getCode().equals("potential"))
|
|
.findFirst().orElseThrow();
|
|
// 先把默认改回种子默认项(模拟 potential 是当前默认),再停用它触发自动取消默认
|
|
dictGroupDefaultMapper.deleteById(statusGroup.getId());
|
|
DictGroupDefault seedDefault = new DictGroupDefault();
|
|
seedDefault.setGroupId(statusGroup.getId());
|
|
seedDefault.setItemId(potential.getId());
|
|
dictGroupDefaultMapper.insert(seedDefault);
|
|
dictItemService.updateItemStatus(potential.getId(), DictConstants.STATUS_DISABLED);
|
|
assertThat(dictGroupDefaultMapper.selectById(statusGroup.getId())).isNull(); // 停用已取消默认
|
|
|
|
initializer.run();
|
|
assertThat(dictGroupDefaultMapper.selectById(statusGroup.getId())).isNull(); // 种子不复活停用默认
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("只加不删:种子外既有内置项在重跑后仍保留")
|
|
void removedFromSeed_notDeleted() {
|
|
initializer.run();
|
|
DictGroup industry = dictGroupMapper.selectList(null).stream()
|
|
.filter(g -> g.getCode().equals("industry")).findFirst().orElseThrow();
|
|
|
|
// 模拟历史版本种子里有、当前种子里已移除的内置项
|
|
DictItem legacy = new DictItem();
|
|
legacy.setGroupId(industry.getId());
|
|
legacy.setCode("legacy_removed");
|
|
legacy.setName("旧版本内置项");
|
|
legacy.setValue("LEGACY");
|
|
legacy.setStatus(DictConstants.STATUS_ENABLED);
|
|
legacy.setBuiltin(true);
|
|
legacy.setDeleteKey(0L);
|
|
dictItemMapper.insert(legacy);
|
|
|
|
initializer.run();
|
|
assertThat(dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getCode().equals("legacy_removed"))).hasSize(1);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("US-41:同编码用户分组被种子分组占用时,整组跳过——不插入种子项/不设默认/不改名")
|
|
void userGroupWithSeedCode_neverTouched() {
|
|
// 模拟部署前管理员已手建同编码分组(builtin=false)
|
|
DictGroup userGroup = new DictGroup();
|
|
userGroup.setCode("lead_source");
|
|
userGroup.setName("用户自有来源");
|
|
userGroup.setStatus(DictConstants.STATUS_ENABLED);
|
|
userGroup.setBuiltin(false);
|
|
userGroup.setDeleteKey(0L);
|
|
dictGroupMapper.insert(userGroup);
|
|
|
|
initializer.run();
|
|
|
|
// 用户分组未被改名、未被重建,且其下未插入任何种子项
|
|
assertThat(dictGroupMapper.selectById(userGroup.getId()).getName()).isEqualTo("用户自有来源");
|
|
assertThat(dictGroupMapper.selectById(userGroup.getId()).getBuiltin()).isFalse();
|
|
assertThat(dictItemMapper.selectList(null).stream()
|
|
.filter(i -> i.getGroupId().equals(userGroup.getId()))).isEmpty();
|
|
// 种子其他分组不受影响(继续正常导入):customer_status + industry
|
|
assertThat(dictGroupMapper.selectCount(null)).isEqualTo(3); // lead_source(用户) + customer_status + industry
|
|
assertThat(dictGroupMapper.selectList(null).stream()
|
|
.filter(g -> g.getCode().equals("industry"))).hasSize(1);
|
|
assertThat(dictItemMapper.selectCount(null)).isEqualTo(11); // customer_status 3 + industry 8
|
|
assertThat(dictRefCountMapper.selectCount(null)).isEqualTo(11);
|
|
assertThat(dictGroupDefaultMapper.selectCount(null)).isEqualTo(1); // customer_status 默认项
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("种子校验 fail-fast:坏种子拒绝导入且不写库")
|
|
void invalidSeed_failFast() {
|
|
// 1. 分组编码重复
|
|
assertSeedRejected(List.of(
|
|
groupSeed("g1", "一", itemSeed("a", "项A", "A", false)),
|
|
groupSeed("g1", "二", itemSeed("b", "项B", "B", false))));
|
|
// 2. 组内字典编码重复
|
|
assertSeedRejected(List.of(
|
|
groupSeed("g1", "一", itemSeed("a", "项A", "A", false),
|
|
itemSeed("a", "项A2", "B", false))));
|
|
// 3. 同组字典值重复(非空)
|
|
assertSeedRejected(List.of(
|
|
groupSeed("g1", "一", itemSeed("a", "项A", "A", false),
|
|
itemSeed("b", "项B", "A", false))));
|
|
// 4. 每组超过一个默认项
|
|
assertSeedRejected(List.of(
|
|
groupSeed("g1", "一", itemSeed("a", "项A", "A", true),
|
|
itemSeed("b", "项B", "B", true))));
|
|
// 5. 编码格式非法(含非法字符)
|
|
assertSeedRejected(List.of(
|
|
groupSeed("bad-code!", "非法编码", itemSeed("a", "项A", "A", false))));
|
|
// 6. 分组名称缺失
|
|
assertSeedRejected(List.of(
|
|
groupSeed("g1", " ", itemSeed("a", "项A", "A", false))));
|
|
|
|
// 全部被拒 → 库中未写入任何数据(fail-fast 语义)
|
|
assertThat(dictGroupMapper.selectCount(null)).isZero();
|
|
assertThat(dictItemMapper.selectCount(null)).isZero();
|
|
}
|
|
|
|
/*-------- 测试数据工厂 --------*/
|
|
|
|
private void assertSeedRejected(List<DictDataInitializer.GroupSeed> seeds) {
|
|
org.assertj.core.api.Assertions.assertThatThrownBy(() -> initializer.run(seeds))
|
|
.isInstanceOf(com.crm.base.domain.exception.BusinessErrorException.class);
|
|
}
|
|
|
|
private DictDataInitializer.GroupSeed groupSeed(String code, String name,
|
|
DictDataInitializer.ItemSeed... items) {
|
|
return new DictDataInitializer.GroupSeed(code, name, 1, null, List.of(items));
|
|
}
|
|
|
|
private DictDataInitializer.ItemSeed itemSeed(String code, String name,
|
|
String value, boolean defaultItem) {
|
|
return new DictDataInitializer.ItemSeed(code, name, value, 1, null, defaultItem);
|
|
}
|
|
}
|
|
|