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.

253 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);
}
}