package jnpf.elnService.template;
|
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
|
import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity;
|
import jnpf.elnEntity.template.ElnTemplateVos;
|
import jnpf.elnMapper.ElnFieldVersionMapper;
|
import jnpf.elnMapper.ElnTemplateBindingMapper;
|
import jnpf.elnService.template.docx.DocxControlSnapshot;
|
import jnpf.util.JsonUtil;
|
import org.junit.Test;
|
|
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Proxy;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertTrue;
|
|
public class ElnPendingBindingCleanupServiceTest {
|
|
private static final Date NOW = new Date(2_000_000L);
|
|
@Test
|
public void cancelsOnlyExpiredPendingWithoutAControlAndAuditsTheCasWinner() {
|
Fixture fixture = new Fixture();
|
fixture.add("b1", "tenant-a", "template-1", new Date(1_000_000L));
|
|
List<ElnTemplateVos.Diagnostic> result = fixture.service.reconcileExpired(
|
"tenant-a", "template-1", NOW, Collections.emptyList());
|
|
assertEquals("CANCELLED", fixture.binding("b1").getBangdingZhuangtai());
|
assertEquals("PENDING_TIMEOUT_NO_CONTROL", fixture.binding("b1").getZhuangtaiYuanyin());
|
assertCode(result, "PENDING_TIMEOUT_NO_CONTROL");
|
assertEquals(1, fixture.audits.size());
|
assertEquals("ELN_TEMPLATE_BINDING_CANCEL", fixture.audits.get(0).getActionCode());
|
}
|
|
@Test
|
public void keepsSingleMatchingControlPendingForPluginConfirmation() {
|
Fixture fixture = new Fixture();
|
fixture.add("b1", "tenant-a", "template-1", new Date(1_000_000L));
|
|
List<ElnTemplateVos.Diagnostic> result = fixture.service.reconcileExpired(
|
"tenant-a", "template-1", NOW,
|
Collections.singletonList(control("eln.field.sample_code", "TEXT",
|
"PRIMARY", Collections.emptyMap())));
|
|
assertEquals("PENDING", fixture.binding("b1").getBangdingZhuangtai());
|
assertCode(result, "PENDING_CONFIRMATION_REQUIRED");
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void ignoresUnobservableTextBusinessConfig() {
|
Fixture fixture = new Fixture();
|
fixture.add("text", "tenant-a", "template-1", new Date(1_000_000L));
|
Map<String, Object> fieldConfig = new LinkedHashMap<>();
|
fieldConfig.put("placeholder", "请输入");
|
fieldConfig.put("defaultValue", "默认文本");
|
fixture.configs.put("text", fieldConfig);
|
|
List<ElnTemplateVos.Diagnostic> result = fixture.service.reconcileExpired(
|
"tenant-a", "template-1", NOW,
|
Collections.singletonList(control("eln.field.sample_code", "TEXT",
|
"PRIMARY", Collections.emptyMap())));
|
|
assertBindingCode(result, "text", "PENDING_CONFIRMATION_REQUIRED");
|
assertNoBindingCode(result, "text", "CONFIG_MISMATCH");
|
}
|
|
@Test
|
public void canonicalizesDropdownOptionsAndReportsRealDifference() {
|
Fixture fixture = new Fixture();
|
fixture.add("same", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.binding("same").setKongjianBiaoqian("eln.field.same");
|
fixture.binding("same").setKongjianLeixing("DROPDOWN");
|
fixture.configs.put("same", dropdownConfig());
|
fixture.add("different", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.binding("different").setKongjianBiaoqian("eln.field.different");
|
fixture.binding("different").setKongjianLeixing("DROPDOWN");
|
fixture.configs.put("different", dropdownConfig());
|
Map<String, Object> reordered = new LinkedHashMap<>();
|
reordered.put("FAIL", "不合格");
|
reordered.put("PASS", "合格");
|
Map<String, Object> different = new LinkedHashMap<>();
|
different.put("PASS", "合格");
|
different.put("FAIL", "异常");
|
|
List<ElnTemplateVos.Diagnostic> result = fixture.service.reconcileExpired(
|
"tenant-a", "template-1", NOW, Arrays.asList(
|
control("eln.field.same", "DROPDOWN", "PRIMARY", reordered),
|
control("eln.field.different", "DROPDOWN", "PRIMARY", different)));
|
|
assertBindingCode(result, "same", "PENDING_CONFIRMATION_REQUIRED");
|
assertNoBindingCode(result, "same", "CONFIG_MISMATCH");
|
assertBindingCode(result, "different", "CONFIG_MISMATCH");
|
}
|
|
@Test
|
public void reportsDuplicateRoleTypeAndConfigurationMismatchWithoutCancelling() {
|
Fixture fixture = new Fixture();
|
fixture.add("duplicate", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.add("role", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.binding("role").setKongjianBiaoqian("eln.field.role");
|
fixture.add("type", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.binding("type").setKongjianBiaoqian("eln.field.type");
|
fixture.add("config", "tenant-a", "template-1", new Date(1_000_000L));
|
fixture.binding("config").setKongjianBiaoqian("eln.field.config");
|
fixture.binding("config").setKongjianLeixing("DROPDOWN");
|
fixture.configs.put("config", dropdownConfig());
|
List<DocxControlSnapshot> controls = new ArrayList<>();
|
controls.add(control("eln.field.sample_code", "TEXT", "PRIMARY", Collections.emptyMap()));
|
controls.add(control("eln.field.sample_code", "TEXT", "PRIMARY", Collections.emptyMap()));
|
controls.add(control("eln.field.role", "TEXT", "REFERENCE", Collections.emptyMap()));
|
controls.add(control("eln.field.type", "DATE", "PRIMARY", Collections.emptyMap()));
|
Map<String, Object> differentOptions = new LinkedHashMap<>();
|
differentOptions.put("PASS", "合格");
|
differentOptions.put("FAIL", "异常");
|
controls.add(control("eln.field.config", "DROPDOWN", "PRIMARY", differentOptions));
|
|
List<ElnTemplateVos.Diagnostic> result = fixture.service.reconcileExpired(
|
"tenant-a", "template-1", NOW, controls);
|
|
assertCode(result, "DUPLICATE_TAG");
|
assertCode(result, "ROLE_MISMATCH");
|
assertCode(result, "TYPE_MISMATCH");
|
assertCode(result, "CONFIG_MISMATCH");
|
assertTrue(fixture.audits.isEmpty());
|
assertTrue(fixture.bindings.values().stream()
|
.allMatch(item -> "PENDING".equals(item.getBangdingZhuangtai())));
|
}
|
|
@Test
|
public void tenantCannotReconcileAnotherTenantAndCopiedPendingNeverExpires() {
|
Fixture fixture = new Fixture();
|
fixture.add("tenant-b", "tenant-b", "template-1", new Date(1_000_000L));
|
fixture.add("copied", "tenant-a", "template-1", null);
|
|
fixture.service.reconcileExpired("tenant-a", "template-1", NOW,
|
Collections.emptyList());
|
|
assertEquals("PENDING", fixture.binding("tenant-b").getBangdingZhuangtai());
|
assertEquals("PENDING", fixture.binding("copied").getBangdingZhuangtai());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void twoInstancesProduceOnlyOneCancellationAudit() {
|
Fixture fixture = new Fixture();
|
fixture.add("b1", "tenant-a", "template-1", new Date(1_000_000L));
|
ElnPendingBindingCleanupService second =
|
new ElnPendingBindingCleanupService(fixture.store, fixture.audits::add);
|
|
fixture.service.reconcileExpired("tenant-a", "template-1", NOW,
|
Collections.emptyList());
|
second.reconcileExpired("tenant-a", "template-1", NOW, Collections.emptyList());
|
|
assertEquals(1, fixture.audits.size());
|
}
|
|
@Test
|
public void cancellationCasExcludesLogicallyDeletedBindings() throws Exception {
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(
|
new MybatisConfiguration(), "eln-pending-cleanup-test"),
|
ElnTemplateBindingEntity.class);
|
final Wrapper<?>[] captured = new Wrapper<?>[1];
|
ElnTemplateBindingMapper mapper = (ElnTemplateBindingMapper) Proxy.newProxyInstance(
|
ElnTemplateBindingMapper.class.getClassLoader(),
|
new Class<?>[]{ElnTemplateBindingMapper.class}, (proxy, method, args) -> {
|
if ("update".equals(method.getName())) {
|
captured[0] = (Wrapper<?>) args[1];
|
return 1;
|
}
|
return null;
|
});
|
Constructor<?> constructor = Class.forName(
|
"jnpf.elnService.template.ElnPendingBindingCleanupService$MyBatisStore")
|
.getDeclaredConstructor(ElnTemplateBindingMapper.class,
|
ElnFieldVersionMapper.class);
|
constructor.setAccessible(true);
|
ElnPendingBindingCleanupService.Store store =
|
(ElnPendingBindingCleanupService.Store) constructor.newInstance(mapper, null);
|
ElnTemplateBindingEntity binding = new ElnTemplateBindingEntity();
|
binding.setId("b1");
|
binding.setFTenantId("tenant-a");
|
binding.setMubanId("template-1");
|
|
assertTrue(store.cancelIfPending(binding, 1, NOW, "timeout"));
|
String sql = captured[0].getSqlSegment().toLowerCase(Locale.ROOT);
|
assertTrue(sql, sql.contains("f_delete_mark") && sql.contains("is null"));
|
}
|
|
@Test
|
public void reconcilesExpiredFormulaPendingFromAuthoritativeLockSnapshot() {
|
FormulaCleanupStore formulaStore = new FormulaCleanupStore();
|
formulaStore.add("unlocked", "result-unlocked", new Date(1_000_000L));
|
formulaStore.add("locked", "result-locked", new Date(1_000_000L));
|
formulaStore.add("partial", "result-partial", new Date(1_000_000L));
|
List<AuditEventDTO> audits = new ArrayList<>();
|
ElnPendingBindingCleanupService service = new ElnPendingBindingCleanupService(
|
new Fixture().store, formulaStore, audits::add);
|
|
List<ElnTemplateVos.Diagnostic> result = service.reconcileExpired(
|
"tenant-a", "template-1", NOW, Arrays.asList(
|
lockControl("eln.field.result-unlocked", null),
|
lockControl("eln.field.result-unlocked-ref", null),
|
lockControl("eln.field.result-locked", "sdtContentLocked"),
|
lockControl("eln.field.result-locked-ref", "sdtContentLocked"),
|
lockControl("eln.field.result-partial", "sdtContentLocked")));
|
|
assertEquals("CANCELLED", formulaStore.bindings.get("unlocked").getBangdingZhuangtai());
|
assertEquals("PENDING", formulaStore.bindings.get("locked").getBangdingZhuangtai());
|
assertEquals("PENDING", formulaStore.bindings.get("partial").getBangdingZhuangtai());
|
assertBindingCode(result, "unlocked", "PENDING_TIMEOUT_NO_LOCK");
|
assertBindingCode(result, "locked", "PENDING_CONFIRMATION_REQUIRED");
|
assertBindingCode(result, "partial", "FORMULA_RESULT_LOCK_MISMATCH");
|
assertEquals(1, audits.stream().filter(item ->
|
"ELN_TEMPLATE_FORMULA_CANCEL".equals(item.getActionCode())).count());
|
AuditEventDTO cancellation = audits.stream().filter(item ->
|
"ELN_TEMPLATE_FORMULA_CANCEL".equals(item.getActionCode())).findFirst().get();
|
@SuppressWarnings("unchecked")
|
Map<String, Object> snapshot = JsonUtil.getJsonToBean(
|
cancellation.getDataSnapshot(), Map.class);
|
assertTrue(snapshot.containsKey("before"));
|
assertTrue(snapshot.containsKey("after"));
|
}
|
|
@Test
|
public void keepsFormulaPendingWhenPrimaryIsUnlockedButReferenceIsLocked() {
|
FormulaCleanupStore formulaStore = new FormulaCleanupStore();
|
formulaStore.add("partial-unlocked", "result-partial-unlocked", new Date(1_000_000L));
|
List<AuditEventDTO> audits = new ArrayList<>();
|
ElnPendingBindingCleanupService service = new ElnPendingBindingCleanupService(
|
new Fixture().store, formulaStore, audits::add);
|
|
List<ElnTemplateVos.Diagnostic> result = service.reconcileExpired(
|
"tenant-a", "template-1", NOW, Arrays.asList(
|
lockControl("eln.field.result-partial-unlocked", null),
|
lockControl("eln.field.result-partial-unlocked-ref", "sdtContentLocked")));
|
|
assertEquals("PENDING",
|
formulaStore.bindings.get("partial-unlocked").getBangdingZhuangtai());
|
assertBindingCode(result, "partial-unlocked", "FORMULA_RESULT_LOCK_MISMATCH");
|
assertTrue(audits.isEmpty());
|
}
|
|
private static DocxControlSnapshot lockControl(String tag, String lock) {
|
return new DocxControlSnapshot(tag, null, lock, "TEXT", "PRIMARY",
|
"word/document.xml", "OK", Collections.emptyMap());
|
}
|
|
private static DocxControlSnapshot control(String tag, String type, String role,
|
Map<String, Object> config) {
|
return new DocxControlSnapshot(tag, null, null, type, role,
|
"word/document.xml", "OK", config);
|
}
|
|
private static void assertCode(List<ElnTemplateVos.Diagnostic> diagnostics, String code) {
|
assertTrue("缺少诊断: " + code,
|
diagnostics.stream().anyMatch(item -> code.equals(item.getCode())));
|
}
|
|
private static void assertBindingCode(List<ElnTemplateVos.Diagnostic> diagnostics,
|
String bindingId, String code) {
|
assertTrue("缺少诊断: " + bindingId + "/" + code,
|
diagnostics.stream().anyMatch(item -> bindingId.equals(item.getBindingId())
|
&& code.equals(item.getCode())));
|
}
|
|
private static void assertNoBindingCode(List<ElnTemplateVos.Diagnostic> diagnostics,
|
String bindingId, String code) {
|
assertTrue("不应存在诊断: " + bindingId + "/" + code,
|
diagnostics.stream().noneMatch(item -> bindingId.equals(item.getBindingId())
|
&& code.equals(item.getCode())));
|
}
|
|
private static Map<String, Object> dropdownConfig() {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("controlType", "dropdown");
|
result.put("placeholder", "请选择");
|
result.put("defaultOptionValue", "PASS");
|
Map<String, Object> pass = new LinkedHashMap<>();
|
pass.put("display", "合格");
|
pass.put("value", "PASS");
|
Map<String, Object> fail = new LinkedHashMap<>();
|
fail.put("display", "不合格");
|
fail.put("value", "FAIL");
|
result.put("options", Arrays.asList(pass, fail));
|
return result;
|
}
|
|
private static final class Fixture {
|
private final Map<String, ElnTemplateBindingEntity> bindings = new LinkedHashMap<>();
|
private final Map<String, Map<String, Object>> configs = new LinkedHashMap<>();
|
private final List<AuditEventDTO> audits = new ArrayList<>();
|
private final MemoryStore store = new MemoryStore(bindings, configs);
|
private final ElnPendingBindingCleanupService service =
|
new ElnPendingBindingCleanupService(store, audits::add);
|
|
private void add(String id, String tenant, String template, Date expiresAt) {
|
ElnTemplateBindingEntity item = new ElnTemplateBindingEntity();
|
item.setId(id);
|
item.setFTenantId(tenant);
|
item.setMubanId(template);
|
item.setZiduanDingyiId("field-1");
|
item.setZiduanBanbenId("version-1");
|
item.setZiduanBianma("sample_code");
|
item.setKongjianBiaoqian("eln.field.sample_code");
|
item.setKongjianLeixing("TEXT");
|
item.setKongjianJuese("PRIMARY");
|
item.setBangdingZhuangtai("PENDING");
|
item.setDaoqiShijian(expiresAt);
|
item.setFVersion(1);
|
bindings.put(id, item);
|
configs.put(id, Collections.emptyMap());
|
}
|
|
private ElnTemplateBindingEntity binding(String id) {
|
return bindings.get(id);
|
}
|
}
|
|
private static final class MemoryStore implements ElnPendingBindingCleanupService.Store {
|
private final Map<String, ElnTemplateBindingEntity> bindings;
|
private final Map<String, Map<String, Object>> configs;
|
|
private MemoryStore(Map<String, ElnTemplateBindingEntity> bindings,
|
Map<String, Map<String, Object>> configs) {
|
this.bindings = bindings;
|
this.configs = configs;
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listExpired(
|
String tenantId, String templateId, Date now) {
|
List<ElnTemplateBindingEntity> result = new ArrayList<>();
|
for (ElnTemplateBindingEntity item : bindings.values()) {
|
if (tenantId.equals(item.getFTenantId()) && templateId.equals(item.getMubanId())
|
&& "PENDING".equals(item.getBangdingZhuangtai())
|
&& item.getDaoqiShijian() != null && item.getDaoqiShijian().before(now)) {
|
result.add(item);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public Map<String, Object> expectedControlConfig(
|
String tenantId, ElnTemplateBindingEntity binding) {
|
return configs.get(binding.getId());
|
}
|
|
@Override
|
public boolean cancelIfPending(ElnTemplateBindingEntity binding, int expectedVersion,
|
Date now, String reason) {
|
ElnTemplateBindingEntity current = bindings.get(binding.getId());
|
if (current == null || !binding.getFTenantId().equals(current.getFTenantId())
|
|| !"PENDING".equals(current.getBangdingZhuangtai())
|
|| current.getFVersion() != expectedVersion
|
|| current.getDaoqiShijian() == null
|
|| !current.getDaoqiShijian().before(now)) {
|
return false;
|
}
|
current.setBangdingZhuangtai("CANCELLED");
|
current.setZhuangtaiYuanyin(reason);
|
current.setFVersion(expectedVersion + 1);
|
return true;
|
}
|
}
|
|
private static final class FormulaCleanupStore
|
implements ElnPendingBindingCleanupService.FormulaStore {
|
private final Map<String, ElnTemplateFormulaBindingEntity> bindings = new LinkedHashMap<>();
|
private final Map<String, List<ElnTemplateBindingEntity>> resultControls = new LinkedHashMap<>();
|
|
private void add(String id, String resultId, Date expiresAt) {
|
ElnTemplateFormulaBindingEntity item = new ElnTemplateFormulaBindingEntity();
|
item.setId(id); item.setFTenantId("tenant-a"); item.setMubanId("template-1");
|
item.setBangdingZhuangtai("PENDING"); item.setJieguoBangdingId(resultId);
|
item.setDaoqiShijian(expiresAt); item.setFVersion(1); item.setCaozuoBianma("op-" + id);
|
bindings.put(id, item);
|
List<ElnTemplateBindingEntity> controls = new ArrayList<>();
|
controls.add(fieldBinding(resultId, "eln.field." + resultId, "PRIMARY", null));
|
controls.add(fieldBinding(resultId + "-ref", "eln.field." + resultId + "-ref",
|
"REFERENCE", resultId));
|
resultControls.put(id, controls);
|
}
|
|
@Override public List<ElnTemplateFormulaBindingEntity> listExpired(
|
String tenantId, String templateId, Date now) {
|
List<ElnTemplateFormulaBindingEntity> result = new ArrayList<>();
|
for (ElnTemplateFormulaBindingEntity item : bindings.values())
|
if (item.getDaoqiShijian() != null && item.getDaoqiShijian().before(now)
|
&& "PENDING".equals(item.getBangdingZhuangtai())) result.add(item);
|
return result;
|
}
|
@Override public List<ElnTemplateBindingEntity> resultControls(
|
String tenantId, String templateId, String formulaBindingId) {
|
return resultControls.get(formulaBindingId);
|
}
|
@Override public boolean cancelIfPending(ElnTemplateFormulaBindingEntity item,
|
int expectedVersion, Date now, String reason) {
|
if (!"PENDING".equals(item.getBangdingZhuangtai())
|
|| item.getFVersion() != expectedVersion) return false;
|
item.setBangdingZhuangtai("CANCELLED"); item.setZhuangtaiYuanyin(reason);
|
item.setFVersion(expectedVersion + 1); return true;
|
}
|
private static ElnTemplateBindingEntity fieldBinding(String id, String tag,
|
String role, String primary) {
|
ElnTemplateBindingEntity result = new ElnTemplateBindingEntity();
|
result.setId(id); result.setKongjianBiaoqian(tag); result.setKongjianJuese(role);
|
result.setYinyongShiliId(primary); result.setBangdingZhuangtai("ACTIVE");
|
return result;
|
}
|
}
|
}
|