package jnpf.elnService.impl; import jnpf.audit.model.AuditEventDTO; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordForms; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordVos; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnRecordFieldValueMapper; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnService.ElnTestWorkItemSnapshotProvider; import jnpf.elnService.ElnRecordFormulaCalculationService; import jnpf.elnService.ElnRecordSharedFillService; import jnpf.exception.DataException; import jnpf.lims.api.model.TestWorkItemSnapshotBatch; import jnpf.lims.api.model.TestWorkItemSnapshot; import org.junit.Test; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class ElnRecordSharedFillServiceImplTest { @Test public void rejectsADifferentValueWithoutExplicitOverwriteReason() { ExecuteFixture fixture = new ExecuteFixture("19.00"); ElnRecordVos.SharedFilled result = fixture.service.execute(fixture.command("20.50")); assertEquals("REJECTED", result.getTargets().get(0).getStatus()); assertEquals("TARGET_COMMIT_REJECTED", result.getTargets().get(0).getErrorCode()); assertEquals("目标字段已有不同值,必须确认覆盖并填写原因", result.getTargets().get(0).getErrorMessage()); assertEquals(0, fixture.recordAdvances.get()); assertEquals(0, fixture.valueWrites.get()); assertEquals(0, fixture.auditEvents.size()); } @Test public void rejectsAChangedPreviewTokenBeforeStartingAnyTargetTransaction() { ExecuteFixture fixture = new ExecuteFixture(null); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); command.setPreviewToken("stale-preview-token"); try { fixture.service.execute(command); fail("changed preview token must reject the whole command"); } catch (DataException expected) { assertEquals("批量填写预览已变化,请重新预览", expected.getMessage()); } assertEquals(0, fixture.transactionCalls.get()); assertEquals(0, fixture.recordAdvances.get()); assertEquals(0, fixture.valueWrites.get()); } @Test public void revalidatesPreviewWhenRetryingAnOperationWithoutCommittedTargets() { ExecuteFixture fixture = new ExecuteFixture(null); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); command.setPreviewToken("stale-preview-token"); for (int attempt = 0; attempt < 2; attempt++) { try { fixture.service.execute(command); fail("a retry without committed targets must revalidate its preview"); } catch (DataException expected) { assertEquals("批量填写预览已变化,请重新预览", expected.getMessage()); } } assertEquals(0, fixture.transactionCalls.get()); assertEquals(0, fixture.recordAdvances.get()); } @Test public void rejectsAnOperationCodeLongerThanTheAuditContract() { ExecuteFixture fixture = new ExecuteFixture(null); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); command.setOperationCode(String.join("", Collections.nCopies(65, "x"))); try { fixture.service.execute(command); fail("an oversized operation code must be rejected before execution"); } catch (DataException expected) { assertEquals("批量填写操作码长度不能超过64个字符", expected.getMessage()); } assertEquals(0, fixture.transactionCalls.get()); } @Test public void rejectsDuplicateFieldVersionsEvenWhenTheFirstValueIsNull() { ExecuteFixture fixture = new ExecuteFixture(null); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); ElnRecordForms.SharedFillValue first = new ElnRecordForms.SharedFillValue(); first.setFieldVersionId("version-shared"); ElnRecordForms.SharedFillValue duplicate = new ElnRecordForms.SharedFillValue(); duplicate.setFieldVersionId("version-shared"); duplicate.setValue("20.50"); command.setValues(Arrays.asList(first, duplicate)); try { fixture.service.execute(command); fail("duplicate field versions must be rejected before execution"); } catch (DataException expected) { assertEquals("批量填写字段不能重复", expected.getMessage()); } assertEquals(0, fixture.transactionCalls.get()); } @Test public void continuesWithOtherTargetsAfterOneRecordVersionConflict() { ExecuteFixture fixture = new ExecuteFixture(null); fixture.casFailureRecordId.set("record-1"); ElnRecordVos.SharedFilled result = fixture.service.execute( fixture.commandForAllTargets("20.50")); assertEquals(2, result.getTargets().size()); assertEquals("REJECTED", result.getTargets().get(0).getStatus()); assertEquals("COMMITTED", result.getTargets().get(1).getStatus()); assertEquals(Integer.valueOf(6), result.getTargets().get(1).getRecordVersion()); assertEquals(2, fixture.transactionCalls.get()); assertEquals(2, fixture.recordAdvances.get()); assertEquals(1, fixture.valueWrites.get()); assertEquals(1, fixture.formulaCalls.get()); assertEquals(1, fixture.auditEvents.size()); assertEquals("record-2", fixture.auditEvents.get(0).getTargetId()); } @Test public void overwritesADifferentValueOnlyWithExplicitReason() { ExecuteFixture fixture = new ExecuteFixture("19.00"); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); command.getTargets().get(0).setOverwrite(true); command.getTargets().get(0).setOverwriteReason("环境温度复测修正"); ElnRecordVos.SharedFilled result = fixture.service.execute(command); assertEquals("COMMITTED", result.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(4), result.getTargets().get(0).getRecordVersion()); assertEquals(1, fixture.recordAdvances.get()); assertEquals(1, fixture.valueWrites.get()); assertEquals(1, fixture.formulaCalls.get()); assertEquals("环境温度复测修正", fixture.auditEvents.get(0).getReason()); } @Test public void treatsTheSameDecimalValueAsUnchangedWithoutAdvancingTheRecord() { ExecuteFixture fixture = new ExecuteFixture("20.5"); ElnRecordVos.SharedFilled result = fixture.service.execute(fixture.command("20.50")); assertEquals(1, result.getTargets().size()); assertEquals("UNCHANGED", result.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(3), result.getTargets().get(0).getRecordVersion()); assertEquals(0, fixture.recordAdvances.get()); assertEquals(0, fixture.valueWrites.get()); assertEquals(0, fixture.formulaCalls.get()); assertEquals(1, fixture.auditEvents.size()); assertEquals("BIZ_ACTION", fixture.auditEvents.get(0).getEventType()); } @Test public void replaysACompletedOperationWithoutRepeatingAnyTargetEffect() { ExecuteFixture fixture = new ExecuteFixture(null); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); ElnRecordVos.SharedFilled first = fixture.service.execute(command); ElnRecordVos.SharedFilled retried = fixture.service.execute(command); assertEquals("COMMITTED", first.getTargets().get(0).getStatus()); assertEquals("COMMITTED", retried.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(4), retried.getTargets().get(0).getRecordVersion()); assertEquals(1, fixture.transactionCalls.get()); assertEquals(1, fixture.recordAdvances.get()); assertEquals(1, fixture.valueWrites.get()); assertEquals(1, fixture.formulaCalls.get()); assertEquals(1, fixture.auditEvents.size()); } @Test public void resumesAfterTheTargetCommittedBeforeTheOperationSummary() { InterruptingOperationCoordinator operations = new InterruptingOperationCoordinator(); ExecuteFixture fixture = new ExecuteFixture(null, operations); ElnRecordForms.SharedFillExecute command = fixture.command("20.50"); try { fixture.service.execute(command); fail("the first operation summary must be interrupted"); } catch (DataException expected) { assertEquals("模拟操作汇总中断", expected.getMessage()); } ElnRecordVos.SharedFilled retried = fixture.service.execute(command); assertEquals("COMMITTED", retried.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(4), retried.getTargets().get(0).getRecordVersion()); assertEquals(2, fixture.transactionCalls.get()); assertEquals(1, fixture.recordAdvances.get()); assertEquals(1, fixture.valueWrites.get()); assertEquals(1, fixture.formulaCalls.get()); assertEquals(1, fixture.auditEvents.size()); } @Test public void writesOneDecimalTargetAndAdvancesItsRecordOnce() { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); Map revisions = new LinkedHashMap<>(); revisions.put("record-1", revision("revision-1", "record-1")); revisions.put("record-2", revision("revision-2", "record-2")); Map> values = new LinkedHashMap<>(); ElnRecordFieldValueEntity selectedValue = value( "value-1", "record-1", "revision-1", null); values.put("record-1", Collections.singletonList(selectedValue)); values.put("record-2", Collections.singletonList(value( "value-2", "record-2", "revision-2", null))); AtomicReference currentItem = new AtomicReference<>(); AtomicInteger recordAdvances = new AtomicInteger(); AtomicInteger valueWrites = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if ("selectLiveByTestWorkItem".equals(method.getName())) { currentItem.set((String) args[1]); return records.get(args[1]); } if ("advanceVersion".equals(method.getName())) { recordAdvances.incrementAndGet(); return 1; } return null; }); ElnRecordRevisionMapper revisionMapper = proxy( ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revisions.get(records.get(currentItem.get()).getId()) : null); ElnRecordFieldValueMapper valueMapper = proxy( ElnRecordFieldValueMapper.class, (ignored, method, args) -> { if ("selectList".equals(method.getName())) { return values.get(records.get(currentItem.get()).getId()); } if ("updateById".equals(method.getName())) { valueWrites.incrementAndGet(); return 1; } return null; }); ElnFieldVersionMapper versionMapper = proxy( ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? sharedVersion() : null); AtomicInteger formulaCalls = new AtomicInteger(); AtomicReference> changedBindings = new AtomicReference<>(); ElnRecordFormulaCalculationService calculation = new ElnRecordFormulaCalculationService() { @Override public List recalculateAffected( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, Set changedBindingIds, String operationCode, String operatorUserId) { formulaCalls.incrementAndGet(); changedBindings.set(changedBindingIds); assertEquals("shared-fill-1", operationCode); return Collections.emptyList(); } @Override public List recalculateAll( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, String operationCode, String operatorUserId) { return Collections.emptyList(); } }; List auditEvents = new ArrayList<>(); ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, snapshots(Arrays.asList(snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))), calculation, auditEvents::add, (action, failureAudit) -> action.get(), () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview previewCommand = new ElnRecordForms.SharedFillPreview(); previewCommand.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview preview = service.preview(previewCommand); ElnRecordForms.SharedFillExecute command = new ElnRecordForms.SharedFillExecute(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); command.setPreviewToken(preview.getPreviewToken()); command.setOperationCode("shared-fill-1"); command.setFieldVersionId("version-shared"); command.setValue("20.50"); ElnRecordForms.SharedFillTarget target = new ElnRecordForms.SharedFillTarget(); target.setTestWorkItemId("item-1"); target.setRecordId("record-1"); target.setExpectedRecordVersion(3); command.setTargets(Collections.singletonList(target)); ElnRecordVos.SharedFilled result = service.execute(command); assertEquals("shared-fill-1", result.getOperationCode()); assertEquals(1, result.getTargets().size()); assertEquals(result.getTargets().get(0).getErrorMessage(), "COMMITTED", result.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(4), result.getTargets().get(0).getRecordVersion()); assertEquals("20.50", selectedValue.getZhiWenben()); assertEquals(new BigDecimal("20.50"), selectedValue.getZhiShuzhi()); assertEquals("MANUAL", selectedValue.getZhiLaiyuan()); assertEquals(1, recordAdvances.get()); assertEquals(1, valueWrites.get()); assertEquals(1, formulaCalls.get()); assertEquals(Collections.singleton("binding-shared"), changedBindings.get()); assertEquals(1, auditEvents.size()); assertEquals("ELN_RECORD_SHARED_FILL", auditEvents.get(0).getActionCode()); assertEquals("shared-fill-1:record-1", auditEvents.get(0).getClientEventId()); } @Test public void writesMultipleFieldsInOneTargetVersionAndDocumentUpdate() { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); Map revisions = new LinkedHashMap<>(); revisions.put("record-1", revision("revision-1", "record-1")); revisions.put("record-2", revision("revision-2", "record-2")); Map> values = new LinkedHashMap<>(); ElnRecordFieldValueEntity temperature = value( "value-temperature", "record-1", "revision-1", "binding-temperature", "definition-temperature", "environment_temperature", "version-temperature", null); ElnRecordFieldValueEntity pressure = value( "value-pressure", "record-1", "revision-1", "binding-pressure", "definition-pressure", "environment_pressure", "version-pressure", null); values.put("record-1", Arrays.asList(temperature, pressure)); values.put("record-2", Arrays.asList( value("value-temperature-2", "record-2", "revision-2", "binding-temperature-2", "definition-temperature", "environment_temperature", "version-temperature", null), value("value-pressure-2", "record-2", "revision-2", "binding-pressure-2", "definition-pressure", "environment_pressure", "version-pressure", null))); AtomicReference currentItem = new AtomicReference<>(); AtomicInteger recordAdvances = new AtomicInteger(); AtomicInteger valueWrites = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if ("selectLiveByTestWorkItem".equals(method.getName())) { currentItem.set((String) args[1]); return records.get(args[1]); } if ("advanceVersion".equals(method.getName())) { recordAdvances.incrementAndGet(); return 1; } return null; }); ElnRecordRevisionMapper revisionMapper = proxy( ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revisions.get(records.get(currentItem.get()).getId()) : null); ElnRecordFieldValueMapper valueMapper = proxy( ElnRecordFieldValueMapper.class, (ignored, method, args) -> { if ("selectList".equals(method.getName())) { return values.get(records.get(currentItem.get()).getId()); } if ("updateById".equals(method.getName())) { valueWrites.incrementAndGet(); return 1; } return null; }); AtomicInteger versionReads = new AtomicInteger(); ElnFieldVersionMapper versionMapper = proxy( ElnFieldVersionMapper.class, (ignored, method, args) -> { if (!"selectOne".equals(method.getName())) return null; return versionReads.getAndIncrement() % 2 == 0 ? sharedVersion("version-temperature", "definition-temperature", "环境温度") : sharedVersion("version-pressure", "definition-pressure", "环境压力"); }); AtomicInteger formulaCalls = new AtomicInteger(); AtomicReference> changedBindings = new AtomicReference<>(); ElnRecordFormulaCalculationService calculation = new ElnRecordFormulaCalculationService() { @Override public List recalculateAffected( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, Set changedBindingIds, String operationCode, String operatorUserId) { formulaCalls.incrementAndGet(); changedBindings.set(changedBindingIds); return Collections.emptyList(); } @Override public List recalculateAll( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, String operationCode, String operatorUserId) { return Collections.emptyList(); } }; AtomicInteger documentUpdates = new AtomicInteger(); AtomicInteger documentValueCount = new AtomicInteger(); List auditEvents = new ArrayList<>(); ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, snapshots(Arrays.asList(snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))), calculation, auditEvents::add, (action, failureAudit) -> action.get(), (tenantId, record, revision, changedValues, operationCode) -> { documentUpdates.incrementAndGet(); documentValueCount.set(changedValues.size()); }, () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview previewCommand = new ElnRecordForms.SharedFillPreview(); previewCommand.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview preview = service.preview(previewCommand); ElnRecordForms.SharedFillExecute command = new ElnRecordForms.SharedFillExecute(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); command.setPreviewToken(preview.getPreviewToken()); command.setOperationCode("shared-fill-multiple"); ElnRecordForms.SharedFillValue temperatureCommand = new ElnRecordForms.SharedFillValue(); temperatureCommand.setFieldVersionId("version-temperature"); temperatureCommand.setValue("20.50"); ElnRecordForms.SharedFillValue pressureCommand = new ElnRecordForms.SharedFillValue(); pressureCommand.setFieldVersionId("version-pressure"); pressureCommand.setValue("101.325"); command.setValues(Arrays.asList(temperatureCommand, pressureCommand)); ElnRecordForms.SharedFillTarget target = new ElnRecordForms.SharedFillTarget(); target.setTestWorkItemId("item-1"); target.setRecordId("record-1"); target.setExpectedRecordVersion(3); command.setTargets(Collections.singletonList(target)); ElnRecordVos.SharedFilled result = service.execute(command); assertEquals("COMMITTED", result.getTargets().get(0).getStatus()); assertEquals(Integer.valueOf(4), result.getTargets().get(0).getRecordVersion()); assertEquals("20.50", temperature.getZhiWenben()); assertEquals("101.325", pressure.getZhiWenben()); assertEquals(1, recordAdvances.get()); assertEquals(2, valueWrites.get()); assertEquals(1, formulaCalls.get()); assertEquals(new java.util.LinkedHashSet<>(Arrays.asList( "binding-temperature", "binding-pressure")), changedBindings.get()); assertEquals(1, documentUpdates.get()); assertEquals(2, documentValueCount.get()); assertEquals(1, auditEvents.size()); assertTrue(auditEvents.get(0).getFieldDiffs().contains("version-temperature")); assertTrue(auditEvents.get(0).getFieldDiffs().contains("version-pressure")); } @Test public void previewsTheCommonSharedFieldAndCurrentValuesWithoutWriting() { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); List revisions = Arrays.asList( revision("revision-1", "record-1"), revision("revision-2", "record-2")); ElnRecordFieldValueEntity untouched = value( "value-1", "record-1", "revision-1", null); untouched.setZhiLaiyuan(null); List> values = Arrays.asList( Collections.singletonList(untouched), Collections.singletonList(value("value-2", "record-2", "revision-2", "21"))); ElnFieldVersionEntity sharedVersion = sharedVersion(); AtomicInteger revisionReads = new AtomicInteger(); AtomicInteger valueReads = new AtomicInteger(); AtomicInteger writes = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if ("selectLiveByTestWorkItem".equals(method.getName())) return records.get(args[1]); countWrite(method.getName(), writes); return null; }); ElnRecordRevisionMapper revisionMapper = proxy(ElnRecordRevisionMapper.class, (ignored, method, args) -> { if ("selectOne".equals(method.getName())) return revisions.get(revisionReads.getAndIncrement()); countWrite(method.getName(), writes); return null; }); ElnRecordFieldValueMapper valueMapper = proxy(ElnRecordFieldValueMapper.class, (ignored, method, args) -> { if ("selectList".equals(method.getName())) return values.get(valueReads.getAndIncrement()); countWrite(method.getName(), writes); return null; }); ElnFieldVersionMapper versionMapper = proxy(ElnFieldVersionMapper.class, (ignored, method, args) -> { if ("selectOne".equals(method.getName())) return sharedVersion; countWrite(method.getName(), writes); return null; }); ElnTestWorkItemSnapshotProvider taskProvider = new ElnTestWorkItemSnapshotProvider() { @Override public TestWorkItemSnapshotBatch resolve( List testWorkItemIds, List properties) { throw new AssertionError("必须使用显式负责人快照接口"); } @Override public TestWorkItemSnapshotBatch resolveAsActor( String tenantId, String userId, List testWorkItemIds, List properties) { assertEquals("tenant-a", tenantId); assertEquals("user-1", userId); assertEquals(Arrays.asList("item-1", "item-2"), testWorkItemIds); assertEquals(Collections.emptyList(), properties); TestWorkItemSnapshot first = snapshot("item-1", null, "project-1-code"); first.setTestDefinitionName("炽灼残渣"); TestWorkItemSnapshot second = snapshot("item-2", null, "project-1-code"); second.setTestDefinitionName("炽灼残渣"); TestWorkItemSnapshotBatch batch = new TestWorkItemSnapshotBatch(); batch.setItems(Arrays.asList(first, second)); return batch; } }; ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, taskProvider, () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview result = service.preview(command); assertEquals(null, result.getTestDefinitionId()); assertEquals("project-1-code", result.getTestDefinitionCode()); assertEquals("炽灼残渣", result.getTestDefinitionName()); assertEquals(64, result.getPreviewToken().length()); assertEquals(1, result.getFields().size()); assertEquals("version-shared", result.getFields().get(0).getFieldVersionId()); assertEquals("environment_temperature", result.getFields().get(0).getFieldCode()); assertEquals("环境温度", result.getFields().get(0).getFieldName()); assertTrue(result.getFields().get(0).isConflictingValues()); assertEquals(Arrays.asList("item-1", "item-2"), result.getFields().get(0).getTargetTestWorkItemIds()); assertEquals(Collections.singletonList("item-2"), result.getFields().get(0).getDefaultExcludedTestWorkItemIds()); assertEquals(2, result.getTargets().size()); assertEquals("炽灼残渣", result.getTargets().get(0).getTestWorkItemName()); assertEquals("炽灼残渣", result.getTargets().get(1).getTestWorkItemName()); assertTrue(result.getTargets().get(0).isEligible()); assertEquals("record-1", result.getTargets().get(0).getRecordId()); assertEquals(Integer.valueOf(3), result.getTargets().get(0).getRecordVersion()); assertTrue(result.getTargets().get(0).getCurrentValues().containsKey("version-shared")); assertEquals(null, result.getTargets().get(0).getCurrentValues().get("version-shared")); assertEquals("21", result.getTargets().get(1).getCurrentValues().get("version-shared")); assertEquals(0, writes.get()); } @Test public void rejectsSharedFillWhenProjectIdsMatchButCodesDiffer() { ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( null, null, null, null, snapshots(Arrays.asList( snapshot("item-1", "project-1", "project-code-1"), snapshot("item-2", "project-1", "project-code-2"))), () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); try { service.preview(command); fail("项目编码不一致时必须拒绝批量填写"); } catch (DataException expected) { assertEquals("批量填写只允许选择同一检验项目", expected.getMessage()); } } @Test public void reportsAnIneligibleTargetWithoutDroppingTheBatchOrReadingItsRecord() { AtomicInteger recordReads = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if ("selectLiveByTestWorkItem".equals(method.getName())) { recordReads.incrementAndGet(); return record("record-1", "item-1", "revision-1", 3); } return null; }); ElnRecordRevisionMapper revisionMapper = proxy(ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revision("revision-1", "record-1") : null); ElnRecordFieldValueMapper valueMapper = proxy(ElnRecordFieldValueMapper.class, (ignored, method, args) -> "selectList".equals(method.getName()) ? Collections.singletonList(value( "value-1", "record-1", "revision-1", "20")) : null); ElnFieldVersionMapper versionMapper = proxy(ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? sharedVersion() : null); ElnTestWorkItemSnapshotProvider taskProvider = new ElnTestWorkItemSnapshotProvider() { @Override public TestWorkItemSnapshotBatch resolve( List testWorkItemIds, List properties) { throw new AssertionError("must use actor snapshot"); } @Override public TestWorkItemSnapshotBatch resolveAsActor( String tenantId, String userId, List testWorkItemIds, List properties) { TestWorkItemSnapshot allowed = snapshot("item-1", "project-1"); TestWorkItemSnapshot forbidden = snapshot("item-2", null); forbidden.setOutcomeCode("NOT_FOUND"); forbidden.setReason("检验项不存在或无权访问"); TestWorkItemSnapshotBatch batch = new TestWorkItemSnapshotBatch(); batch.setItems(Arrays.asList(allowed, forbidden)); return batch; } }; ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, taskProvider, () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview result = service.preview(command); assertEquals(2, result.getTargets().size()); assertTrue(result.getTargets().get(0).isEligible()); ElnRecordVos.SharedTarget rejected = result.getTargets().get(1); assertFalse(rejected.isEligible()); assertEquals("NOT_FOUND", rejected.getErrorCode()); assertEquals("检验项不存在或无权访问", rejected.getErrorMessage()); assertEquals(1, recordReads.get()); } @Test public void reportsAMissingRecordAsAnIneligibleTarget() { ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if (!"selectLiveByTestWorkItem".equals(method.getName())) return null; return "item-1".equals(args[1]) ? record("record-1", "item-1", "revision-1", 3) : null; }); ElnRecordRevisionMapper revisionMapper = proxy(ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revision("revision-1", "record-1") : null); ElnRecordFieldValueMapper valueMapper = proxy(ElnRecordFieldValueMapper.class, (ignored, method, args) -> "selectList".equals(method.getName()) ? Collections.singletonList(value( "value-1", "record-1", "revision-1", "20")) : null); ElnFieldVersionMapper versionMapper = proxy(ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? sharedVersion() : null); ElnTestWorkItemSnapshotProvider taskProvider = snapshots(Arrays.asList( snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))); ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, taskProvider, () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview result = service.preview(command); assertTrue(result.getTargets().get(0).isEligible()); ElnRecordVos.SharedTarget missing = result.getTargets().get(1); assertFalse(missing.isEligible()); assertEquals("RECORD_NOT_FOUND", missing.getErrorCode()); assertEquals("检验项电子记录不存在", missing.getErrorMessage()); } @Test public void reportsANonDraftRevisionWithItsRecordIdentity() { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); ElnRecordRevisionEntity historical = revision("revision-2", "record-2"); historical.setBizStatus("HISTORICAL"); List revisions = Arrays.asList( revision("revision-1", "record-1"), historical); AtomicInteger revisionReads = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> "selectLiveByTestWorkItem".equals(method.getName()) ? records.get(args[1]) : null); ElnRecordRevisionMapper revisionMapper = proxy(ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revisions.get(revisionReads.getAndIncrement()) : null); ElnRecordFieldValueMapper valueMapper = proxy(ElnRecordFieldValueMapper.class, (ignored, method, args) -> "selectList".equals(method.getName()) ? Collections.singletonList(value( "value-1", "record-1", "revision-1", "20")) : null); ElnFieldVersionMapper versionMapper = proxy(ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? sharedVersion() : null); ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, snapshots(Arrays.asList(snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))), () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview result = service.preview(command); ElnRecordVos.SharedTarget rejected = result.getTargets().get(1); assertFalse(rejected.isEligible()); assertEquals("REVISION_NOT_DRAFT", rejected.getErrorCode()); assertEquals("电子记录当前修订不可批量填写", rejected.getErrorMessage()); assertEquals("record-2", rejected.getRecordId()); assertEquals("revision-2", rejected.getRevisionId()); assertEquals(Integer.valueOf(5), rejected.getRecordVersion()); } @Test public void rejectsDuplicateTestWorkItemsBeforeReadingUserOrLimsContext() { ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( null, null, null, null, null, () -> { throw new AssertionError("invalid input must be rejected first"); }); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-1", "item-2")); try { service.preview(command); fail("duplicate test work items must be rejected"); } catch (DataException expected) { assertEquals("批量填写检验项不能重复", expected.getMessage()); } } @Test public void groupsTargetsByTheirExactSharedFieldVersion() { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); List revisions = Arrays.asList( revision("revision-1", "record-1"), revision("revision-2", "record-2")); ElnRecordFieldValueEntity first = value( "value-1", "record-1", "revision-1", "20"); ElnRecordFieldValueEntity second = value( "value-2", "record-2", "revision-2", "21"); second.setZiduanBanbenId("version-shared-v2"); ElnFieldVersionEntity version1 = sharedVersion(); ElnFieldVersionEntity version2 = sharedVersion(); version2.setId("version-shared-v2"); AtomicInteger revisionReads = new AtomicInteger(); AtomicInteger valueReads = new AtomicInteger(); AtomicInteger versionReads = new AtomicInteger(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> "selectLiveByTestWorkItem".equals(method.getName()) ? records.get(args[1]) : null); ElnRecordRevisionMapper revisionMapper = proxy(ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revisions.get(revisionReads.getAndIncrement()) : null); List> values = Arrays.asList( Collections.singletonList(first), Collections.singletonList(second)); ElnRecordFieldValueMapper valueMapper = proxy(ElnRecordFieldValueMapper.class, (ignored, method, args) -> "selectList".equals(method.getName()) ? values.get(valueReads.getAndIncrement()) : null); List versions = Arrays.asList(version1, version2); ElnFieldVersionMapper versionMapper = proxy(ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? versions.get(versionReads.getAndIncrement()) : null); ElnRecordSharedFillService service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, snapshots(Arrays.asList(snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))), () -> new ElnRecordSharedFillServiceImpl.UserContext("tenant-a", "user-1")); ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview result = service.preview(command); assertEquals(2, result.getFields().size()); assertEquals("version-shared", result.getFields().get(0).getFieldVersionId()); assertEquals(Collections.singletonList("item-1"), result.getFields().get(0).getTargetTestWorkItemIds()); assertEquals("version-shared-v2", result.getFields().get(1).getFieldVersionId()); assertEquals(Collections.singletonList("item-2"), result.getFields().get(1).getTargetTestWorkItemIds()); assertEquals("20", result.getTargets().get(0).getCurrentValues() .get("version-shared")); assertEquals("21", result.getTargets().get(1).getCurrentValues() .get("version-shared-v2")); } private static ElnRecordEntity record( String id, String testWorkItemId, String revisionId, int version) { ElnRecordEntity value = new ElnRecordEntity(); value.setId(id); value.setJianyanXiangId(testWorkItemId); value.setDangqianXiudingId(revisionId); value.setMubanId("template-1"); value.setFTenantId("tenant-a"); value.setFVersion(version); value.setFDeleteMark(0); return value; } private static final class ExecuteFixture { private final AtomicInteger recordAdvances = new AtomicInteger(); private final AtomicInteger valueWrites = new AtomicInteger(); private final AtomicInteger formulaCalls = new AtomicInteger(); private final AtomicInteger transactionCalls = new AtomicInteger(); private final AtomicReference casFailureRecordId = new AtomicReference<>(); private final List auditEvents = new ArrayList<>(); private final ElnRecordSharedFillService service; private ExecuteFixture(String currentValue) { this(currentValue, new ElnRecordSharedFillServiceImpl.InMemoryOperationCoordinator()); } private ExecuteFixture( String currentValue, ElnRecordSharedFillServiceImpl.OperationCoordinator operationCoordinator) { Map records = new LinkedHashMap<>(); records.put("item-1", record("record-1", "item-1", "revision-1", 3)); records.put("item-2", record("record-2", "item-2", "revision-2", 5)); Map revisions = new LinkedHashMap<>(); revisions.put("record-1", revision("revision-1", "record-1")); revisions.put("record-2", revision("revision-2", "record-2")); Map> values = new LinkedHashMap<>(); values.put("record-1", Collections.singletonList(value( "value-1", "record-1", "revision-1", currentValue))); values.put("record-2", Collections.singletonList(value( "value-2", "record-2", "revision-2", currentValue))); AtomicReference currentItem = new AtomicReference<>(); ElnRecordMapper recordMapper = proxy(ElnRecordMapper.class, (ignored, method, args) -> { if ("selectLiveByTestWorkItem".equals(method.getName())) { currentItem.set((String) args[1]); return records.get(args[1]); } if ("advanceVersion".equals(method.getName())) { recordAdvances.incrementAndGet(); return args[1].equals(casFailureRecordId.get()) ? 0 : 1; } return null; }); ElnRecordRevisionMapper revisionMapper = proxy( ElnRecordRevisionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? revisions.get(records.get(currentItem.get()).getId()) : null); ElnRecordFieldValueMapper valueMapper = proxy( ElnRecordFieldValueMapper.class, (ignored, method, args) -> { if ("selectList".equals(method.getName())) { return values.get(records.get(currentItem.get()).getId()); } if ("updateById".equals(method.getName())) { valueWrites.incrementAndGet(); return 1; } return null; }); ElnFieldVersionMapper versionMapper = proxy( ElnFieldVersionMapper.class, (ignored, method, args) -> "selectOne".equals(method.getName()) ? sharedVersion() : null); ElnRecordFormulaCalculationService calculation = new ElnRecordFormulaCalculationService() { @Override public List recalculateAffected( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, Set changedBindingIds, String operationCode, String operatorUserId) { formulaCalls.incrementAndGet(); return Collections.emptyList(); } @Override public List recalculateAll( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List currentValues, String operationCode, String operatorUserId) { return Collections.emptyList(); } }; service = new ElnRecordSharedFillServiceImpl( recordMapper, revisionMapper, valueMapper, versionMapper, snapshots(Arrays.asList(snapshot("item-1", "project-1"), snapshot("item-2", "project-1"))), calculation, auditEvents::add, (action, failureAudit) -> { transactionCalls.incrementAndGet(); return action.get(); }, (tenantId, record, revision, changedValues, operationCode) -> { }, operationCoordinator, () -> new ElnRecordSharedFillServiceImpl.UserContext( "tenant-a", "user-1")); } private ElnRecordForms.SharedFillExecute command(String newValue) { ElnRecordForms.SharedFillPreview previewCommand = new ElnRecordForms.SharedFillPreview(); previewCommand.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); ElnRecordVos.SharedFillPreview preview = service.preview(previewCommand); ElnRecordForms.SharedFillExecute command = new ElnRecordForms.SharedFillExecute(); command.setTestWorkItemIds(Arrays.asList("item-1", "item-2")); command.setPreviewToken(preview.getPreviewToken()); command.setOperationCode("shared-fill-1"); command.setFieldVersionId("version-shared"); command.setValue(newValue); ElnRecordForms.SharedFillTarget target = new ElnRecordForms.SharedFillTarget(); target.setTestWorkItemId("item-1"); target.setRecordId("record-1"); target.setExpectedRecordVersion(3); command.setTargets(Collections.singletonList(target)); return command; } private ElnRecordForms.SharedFillExecute commandForAllTargets(String newValue) { ElnRecordForms.SharedFillExecute command = command(newValue); ElnRecordForms.SharedFillTarget second = new ElnRecordForms.SharedFillTarget(); second.setTestWorkItemId("item-2"); second.setRecordId("record-2"); second.setExpectedRecordVersion(5); command.setTargets(Arrays.asList(command.getTargets().get(0), second)); return command; } } private static final class InterruptingOperationCoordinator implements ElnRecordSharedFillServiceImpl.OperationCoordinator { private final Map targets = new LinkedHashMap<>(); private boolean started; private boolean interrupted; private ElnRecordVos.SharedFilled completed; @Override public ElnRecordSharedFillServiceImpl.OperationStart begin( String tenantId, String userId, String operationCode, String requestDigest) { if (completed != null) { return ElnRecordSharedFillServiceImpl.OperationStart.replay(completed); } if (!started) { started = true; return ElnRecordSharedFillServiceImpl.OperationStart.fresh(); } return ElnRecordSharedFillServiceImpl.OperationStart.resuming(); } @Override public ElnRecordVos.SharedFillTargetResult reserveTarget( String tenantId, String userId, String operationCode, ElnRecordForms.SharedFillTarget target) { return targets.get(target.getTestWorkItemId()); } @Override public void completeTarget( String tenantId, String userId, String operationCode, ElnRecordForms.SharedFillTarget target, ElnRecordVos.SharedFillTargetResult result) { targets.put(target.getTestWorkItemId(), result); } @Override public ElnRecordVos.SharedFilled complete( String tenantId, String userId, String operationCode, String requestDigest, ElnRecordVos.SharedFilled result) { if (!interrupted) { interrupted = true; throw new DataException("模拟操作汇总中断"); } completed = result; return result; } } private static ElnRecordRevisionEntity revision(String id, String recordId) { ElnRecordRevisionEntity value = new ElnRecordRevisionEntity(); value.setId(id); value.setDianziJiluId(recordId); value.setBizStatus("DRAFT"); value.setFTenantId("tenant-a"); value.setFDeleteMark(0); return value; } private static ElnRecordFieldValueEntity value( String id, String recordId, String revisionId, String currentValue) { return value(id, recordId, revisionId, "binding-shared", "definition-shared", "environment_temperature", "version-shared", currentValue); } private static ElnRecordFieldValueEntity value( String id, String recordId, String revisionId, String bindingId, String definitionId, String fieldCode, String versionId, String currentValue) { ElnRecordFieldValueEntity value = new ElnRecordFieldValueEntity(); value.setId(id); value.setDianziJiluId(recordId); value.setXiudingId(revisionId); value.setMubanId("template-1"); value.setMubanBangdingId(bindingId); value.setZiduanDingyiId(definitionId); value.setZiduanBianma(fieldCode); value.setZiduanBanbenId(versionId); value.setBangdingJuese("PRIMARY"); value.setBangdingZhuangtai("ACTIVE"); value.setZhiWenben(currentValue); value.setZhiLaiyuan("MANUAL"); value.setFTenantId("tenant-a"); value.setFDeleteMark(0); return value; } private static ElnFieldVersionEntity sharedVersion() { return sharedVersion("version-shared", "definition-shared", "环境温度"); } private static ElnFieldVersionEntity sharedVersion( String id, String definitionId, String name) { ElnFieldVersionEntity value = new ElnFieldVersionEntity(); value.setId(id); value.setZiduanDingyiId(definitionId); value.setZiduanMingcheng(name); value.setShujuLeixing("DECIMAL"); value.setDanwei("℃"); value.setPiciMoshi("BATCH_SHARED"); value.setBangdingPeizhi("{\"read\":{\"source\":\"NONE\"},\"write\":{\"target\":\"ELN_RECORD\"}}"); value.setBizStatus("PUBLISHED"); value.setFTenantId("tenant-a"); value.setFDeleteMark(0); return value; } private static TestWorkItemSnapshot snapshot(String itemId, String projectId) { return snapshot(itemId, projectId, projectId == null ? null : projectId + "-code"); } private static TestWorkItemSnapshot snapshot( String itemId, String projectId, String projectCode) { TestWorkItemSnapshot item = new TestWorkItemSnapshot(); item.setTestWorkItemId(itemId); item.setTestDefinitionId(projectId); item.setTestDefinitionCode(projectCode); item.setOutcomeCode("SUCCESS"); item.setTaskStatus("pending_test"); return item; } private static ElnTestWorkItemSnapshotProvider snapshots( List items) { return new ElnTestWorkItemSnapshotProvider() { @Override public TestWorkItemSnapshotBatch resolve( List testWorkItemIds, List properties) { throw new AssertionError("must use actor snapshot"); } @Override public TestWorkItemSnapshotBatch resolveAsActor( String tenantId, String userId, List testWorkItemIds, List properties) { TestWorkItemSnapshotBatch batch = new TestWorkItemSnapshotBatch(); List selected = new ArrayList<>(); for (String testWorkItemId : testWorkItemIds) { for (TestWorkItemSnapshot item : items) { if (testWorkItemId.equals(item.getTestWorkItemId())) { selected.add(item); break; } } } batch.setItems(selected); return batch; } }; } private static void countWrite(String methodName, AtomicInteger writes) { if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) { writes.incrementAndGet(); } } @SuppressWarnings("unchecked") private static T proxy(Class type, InvocationHandler handler) { return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler); } }