package jnpf.elnService.impl; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateCrossReferenceEntity; import jnpf.elnMapper.ElnRecordFieldValueMapper; import jnpf.elnMapper.ElnTemplateCrossReferenceMapper; import jnpf.elnService.ElnTestRequestIdentityLookup; import jnpf.elnService.ElnRecordSourceResolver; import jnpf.exception.DataException; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.Test; import java.math.BigDecimal; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.mockito.ArgumentCaptor; public class ElnCrossTemplateSnapshotResolverImplTest { @Test public void resolvesTextValueFromExactSourceRecordRevisionAndFieldVersion() { Fixture fixture = new Fixture(); ElnRecordFieldValueEntity sourceValue = fixture.sourceValue("source text", "MANUAL"); when(fixture.valueMapper.selectOne(any(Wrapper.class))).thenReturn(sourceValue); Map result = fixture.resolve(); assertEquals(1, result.size()); assertSame(sourceValue, result.get("target-binding")); verify(fixture.sourceResolver).resolve("tenant-1", "request-1", "SOURCE-TEMPLATE"); } @Test @SuppressWarnings({"rawtypes", "unchecked"}) public void excludesSoftDeletedSourceValuesFromResolveAndFreezeValidation() { TableInfoHelper.initTableInfo(new MapperBuilderAssistant( new MybatisConfiguration(), "cross-template-soft-delete-test"), ElnRecordFieldValueEntity.class); Fixture fixture = new Fixture(); ElnRecordFieldValueEntity source = fixture.sourceValue("source text", "MANUAL"); when(fixture.valueMapper.selectOne(any(Wrapper.class))).thenReturn(source); fixture.resolve(); fixture.validate(fixture.snapshotOf(source)); ArgumentCaptor queries = ArgumentCaptor.forClass(Wrapper.class); verify(fixture.valueMapper, times(2)).selectOne(queries.capture()); for (Wrapper query : queries.getAllValues()) { assertTrue(query.getSqlSegment().contains("f_delete_mark")); } } @Test public void returnsEmptyWithoutCallingIdentityLookupWhenTemplateHasNoCrossRules() { Fixture fixture = new Fixture(); when(fixture.referenceMapper.selectOne(any(Wrapper.class))).thenReturn(null); assertTrue(fixture.resolve().isEmpty()); verify(fixture.identityLookup, never()).resolve(any(), any()); verify(fixture.sourceResolver, never()).resolve(any(), any(), any()); } @Test public void rejectsMissingTargetTestRequestIdentity() { Fixture fixture = new Fixture(); when(fixture.identityLookup.resolve(eq("tenant-1"), any())).thenReturn(null); expectDataException(fixture::resolve, "SOURCE_RECORD_IDENTITY_INVALID"); verify(fixture.sourceResolver, never()).resolve(any(), any(), any()); } @Test public void rejectsMissingEmptyOrVersionMismatchedSourceValue() { Fixture fixture = new Fixture(); when(fixture.valueMapper.selectOne(any(Wrapper.class))).thenReturn(null); expectDataException(fixture::resolve, "SOURCE_FIELD_VALUE_INVALID"); } @Test public void rejectsCrossTemplateReferenceChains() { Fixture fixture = new Fixture(); when(fixture.valueMapper.selectOne(any(Wrapper.class))) .thenReturn(fixture.sourceValue("source text", "CROSS_TEMPLATE")); expectDataException(fixture::resolve, "SOURCE_FIELD_CHAIN_NOT_SUPPORTED"); } @Test public void propagatesSourceRecordStateFailureWithoutReturningPartialSnapshots() { Fixture fixture = new Fixture(); when(fixture.sourceResolver.resolve("tenant-1", "request-1", "SOURCE-TEMPLATE")) .thenThrow(new DataException("SOURCE_RECORD_STATE_NOT_ALLOWED: source not submitted")); expectDataException(fixture::resolve, "SOURCE_RECORD_STATE_NOT_ALLOWED"); } @Test public void acceptsPersistedSnapshotWhenLineageAndAllValueSlotsAreUnchanged() { Fixture fixture = new Fixture(); ElnRecordFieldValueEntity source = fixture.sourceValue("source text", "MANUAL"); source.setZhiShuzhi(new BigDecimal("12.30")); source.setZhiRiqi(new Date(1722513600000L)); when(fixture.valueMapper.selectOne(any(Wrapper.class))).thenReturn(source); List issues = fixture.validate(fixture.snapshotOf(source)); assertTrue(issues.isEmpty()); verify(fixture.valueMapper, never()).updateById(any(ElnRecordFieldValueEntity.class)); } @Test public void reportsRecordRevisionValueIdentityVersionAndEachValueSlotDrift() { assertSnapshotIssue("SOURCE_RECORD_DRIFT", (fixture, snapshot, current) -> snapshot.setKuamoLaiyuanJiluId("previous-record")); assertSnapshotIssue("SOURCE_REVISION_DRIFT", (fixture, snapshot, current) -> snapshot.setKuamoLaiyuanXiudingId("previous-revision")); assertSnapshotIssue("SOURCE_VALUE_ID_DRIFT", (fixture, snapshot, current) -> snapshot.setKuamoLaiyuanZhiId("previous-value")); assertSnapshotIssue("SOURCE_FIELD_VERSION_DRIFT", (fixture, snapshot, current) -> current.setZiduanBanbenId("new-version")); assertSnapshotIssue("SOURCE_VALUE_DRIFT", (fixture, snapshot, current) -> current.setZhiWenben("new text")); assertSnapshotIssue("SOURCE_VALUE_DRIFT", (fixture, snapshot, current) -> current.setZhiShuzhi(new BigDecimal("99.1"))); assertSnapshotIssue("SOURCE_VALUE_DRIFT", (fixture, snapshot, current) -> current.setZhiRiqi(new Date(1722513601000L))); } @Test public void aggregatesUnavailableSourcesForEveryPersistedTargetBinding() { Fixture fixture = new Fixture(); ElnRecordFieldValueEntity first = fixture.snapshotOf(fixture.sourceValue("one", "MANUAL")); ElnRecordFieldValueEntity second = fixture.snapshotOf(fixture.sourceValue("two", "MANUAL")); second.setMubanBangdingId("target-binding-2"); second.setZiduanBianma("target-field-2"); when(fixture.sourceResolver.resolve("tenant-1", "request-1", "SOURCE-TEMPLATE")) .thenThrow(new DataException("SOURCE_RECORD_STATE_NOT_ALLOWED: source canceled")); List issues = fixture.resolver.validatePersistedSnapshots( "tenant-1", "target-item", java.util.Arrays.asList(first, second)); assertEquals(2, issues.size()); assertEquals("target-binding", issues.get(0).getTargetBindingId()); assertEquals("target-binding-2", issues.get(1).getTargetBindingId()); assertEquals("SOURCE_RECORD_STATE_NOT_ALLOWED", issues.get(0).getCode()); } private static void assertSnapshotIssue(String expectedCode, SnapshotChange change) { Fixture fixture = new Fixture(); ElnRecordFieldValueEntity current = fixture.sourceValue("source text", "MANUAL"); current.setZhiShuzhi(new BigDecimal("12.30")); current.setZhiRiqi(new Date(1722513600000L)); ElnRecordFieldValueEntity snapshot = fixture.snapshotOf(current); change.apply(fixture, snapshot, current); when(fixture.valueMapper.selectOne(any(Wrapper.class))).thenReturn(current); List issues = fixture.validate(snapshot); assertEquals(1, issues.size()); assertEquals(expectedCode, issues.get(0).getCode()); assertEquals("target-binding", issues.get(0).getTargetBindingId()); assertEquals("target-field", issues.get(0).getTargetFieldCode()); assertEquals("SOURCE-TEMPLATE", issues.get(0).getSourceTemplateCode()); } private static void expectDataException(Action action, String messageFragment) { try { action.run(); fail("expected DataException containing " + messageFragment); } catch (DataException expected) { org.junit.Assert.assertTrue(expected.getMessage().contains(messageFragment)); } } private interface Action { void run(); } private interface SnapshotChange { void apply(Fixture fixture, ElnRecordFieldValueEntity snapshot, ElnRecordFieldValueEntity current); } private static final class Fixture { private final ElnTemplateCrossReferenceMapper referenceMapper = mock(ElnTemplateCrossReferenceMapper.class); private final ElnRecordFieldValueMapper valueMapper = mock(ElnRecordFieldValueMapper.class); private final ElnTestRequestIdentityLookup identityLookup = mock(ElnTestRequestIdentityLookup.class); private final ElnRecordSourceResolver sourceResolver = mock(ElnRecordSourceResolver.class); private final ElnCrossTemplateSnapshotResolverImpl resolver; private final ElnTemplateBindingEntity targetBinding = new ElnTemplateBindingEntity(); private Fixture() { targetBinding.setId("target-binding"); ElnTemplateCrossReferenceEntity rule = new ElnTemplateCrossReferenceEntity(); rule.setMubanBangdingId("target-binding"); rule.setLaiyuanMubanBianhao("SOURCE-TEMPLATE"); rule.setLaiyuanZiduanDingyiId("source-field"); rule.setLaiyuanZiduanBanbenId("source-version"); when(identityLookup.resolve(eq("tenant-1"), any())).thenReturn(Collections.singletonMap( "target-item", new ElnTestRequestIdentityLookup.Identity("request-1", "pending_test"))); when(referenceMapper.selectOne(any(Wrapper.class))).thenReturn(rule); ElnRecordEntity source = new ElnRecordEntity(); source.setId("source-record"); source.setDangqianXiudingId("source-revision"); when(sourceResolver.resolve("tenant-1", "request-1", "SOURCE-TEMPLATE")).thenReturn(source); resolver = new ElnCrossTemplateSnapshotResolverImpl( referenceMapper, valueMapper, identityLookup, sourceResolver); } private Map resolve() { return resolver.resolve("tenant-1", "target-item", Collections.singletonList(targetBinding)); } private List validate( ElnRecordFieldValueEntity snapshot) { return resolver.validatePersistedSnapshots( "tenant-1", "target-item", Collections.singletonList(snapshot)); } private ElnRecordFieldValueEntity snapshotOf(ElnRecordFieldValueEntity source) { ElnRecordFieldValueEntity snapshot = new ElnRecordFieldValueEntity(); snapshot.setFTenantId("tenant-1"); snapshot.setMubanBangdingId("target-binding"); snapshot.setZiduanBianma("target-field"); snapshot.setZhiLaiyuan("CROSS_TEMPLATE"); snapshot.setKuamoLaiyuanJiluId(source.getDianziJiluId()); snapshot.setKuamoLaiyuanXiudingId(source.getXiudingId()); snapshot.setKuamoLaiyuanZhiId(source.getId()); snapshot.setKuamoLaiyuanZiduanBanbenId(source.getZiduanBanbenId()); snapshot.setZhiWenben(source.getZhiWenben()); snapshot.setZhiShuzhi(source.getZhiShuzhi()); snapshot.setZhiRiqi(source.getZhiRiqi()); return snapshot; } private ElnRecordFieldValueEntity sourceValue(String text, String source) { ElnRecordFieldValueEntity value = new ElnRecordFieldValueEntity(); value.setId("source-value"); value.setDianziJiluId("source-record"); value.setXiudingId("source-revision"); value.setZiduanDingyiId("source-field"); value.setZiduanBanbenId("source-version"); value.setZhiWenben(text); value.setZhiLaiyuan(source); return value; } } }