package jnpf.limsService.provider; import org.junit.Test; import jnpf.exception.DataException; import java.util.Collections; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class LimsElnSourceCatalogTest { @Test public void savesANewSourceDraftWithoutMakingItSelectable() { LimsElnSourceCatalog catalog = catalog(newStore()); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.original_batch")); assertEquals("DRAFT", draft.getStatus()); assertEquals("sample.original_batch", catalog.detail(draft.getId()).getPropertyCode()); assertEquals("originalBatch", catalog.definition(draft.getId()).getFieldId()); assertTrue(catalog.catalog().isEmpty()); } @Test public void publishesOnlyAfterReadingAnAuthorizedSampleAndFreezesTheDefinition() { LimsElnSourceCatalog catalog = new LimsElnSourceCatalog(newStore(), () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), (definition, actor, sampleId) -> { assertEquals("work-item-a", sampleId); assertEquals("originalBatch", definition.getFieldId()); return new LimsElnSourceCatalog.Preview(Collections.singletonList("ORIGINAL-001"), "{\"root\":\"task\",\"field\":\"pihao_yuanchang\",\"metadataRevision\":1}"); }); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.original_batch")); LimsElnSourceCatalog.Version published = catalog.publish(draft.getId(), 0, "work-item-a"); assertEquals("PUBLISHED", published.getStatus()); assertEquals(published, catalog.catalog().get(0)); assertEquals("work-item-a", published.getValidationSampleId()); assertEquals("field-manager", published.getPublishedBy()); assertEquals("{\"root\":\"task\",\"field\":\"pihao_yuanchang\",\"metadataRevision\":1}", published.getFrozenDefinitionJson()); } @Test public void doesNotReuseAnExistingPropertyIdentityAsANewSource() { LimsElnSourceCatalog catalog = catalog(newStore()); catalog.createDraft(input("sample.original_batch")); try { catalog.createDraft(input("sample.original_batch")); fail("A stable property code must not be reused"); } catch (DataException expected) { assertEquals("Source property already exists", expected.getMessage()); } } @Test public void newPublicationKeepsTheOldDefinitionAndOnlyOffersTheLatestVersion() { LimsElnSourceCatalog catalog = publishingCatalog(newStore()); LimsElnSourceCatalog.Version first = catalog.publish( catalog.createDraft(input("sample.original_batch")).getId(), 0, "work-item-a"); LimsElnSourceCatalog.Draft changed = input("sample.original_batch"); changed.getDefinition().setFieldId("supplierBatch"); LimsElnSourceCatalog.Version next = catalog.newVersion(first.getId(), changed); assertEquals(first, catalog.catalog().get(0)); LimsElnSourceCatalog.Version second = catalog.publish(next.getId(), 0, "work-item-a"); assertEquals(2, second.getRevision()); assertEquals(Collections.singletonList(second), catalog.catalog()); assertEquals(first, catalog.published("sample.original_batch", 1)); assertEquals("originalBatch", catalog.definition(first.getId()).getFieldId()); assertEquals("supplierBatch", catalog.definition(second.getId()).getFieldId()); } @Test public void disablingRemovesNewChoicesButKeepsPublishedVersionsReadable() { LimsElnSourceCatalog catalog = publishingCatalog(newStore()); LimsElnSourceCatalog.Version published = catalog.publish( catalog.createDraft(input("sample.original_batch")).getId(), 0, "work-item-a"); catalog.setEnabled("sample.original_batch", false); assertTrue(catalog.catalog().isEmpty()); assertEquals(published, catalog.published("sample.original_batch", 1)); catalog.setEnabled("sample.original_batch", true); assertEquals(Collections.singletonList(published), catalog.catalog()); } @Test public void updatesDraftsWithOptimisticLockingAndNeverUpdatesPublishedDefinitions() { LimsElnSourceCatalog catalog = publishingCatalog(newStore()); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.original_batch")); LimsElnSourceCatalog.Draft changed = input("sample.original_batch"); changed.getDefinition().setFieldId("supplierBatch"); LimsElnSourceCatalog.Version updated = catalog.updateDraft(draft.getId(), 0, changed); assertEquals(1, updated.getEditVersion()); assertEquals("supplierBatch", catalog.definition(draft.getId()).getFieldId()); try { catalog.updateDraft(draft.getId(), 0, input("sample.original_batch")); fail("Stale editors must not overwrite a draft"); } catch (DataException expected) { assertEquals("Source version has changed", expected.getMessage()); } LimsElnSourceCatalog.Version published = catalog.publish(draft.getId(), updated.getEditVersion(), "work-item-a"); try { catalog.updateDraft(draft.getId(), published.getEditVersion(), changed); fail("A published source must not be edited in place"); } catch (DataException expected) { assertEquals("Published source is immutable", expected.getMessage()); } assertEquals(published, catalog.published("sample.original_batch", 1)); } @Test public void recordCallersCanResolveFixedVersionsWithoutReceivingMaintenancePermission() { LimsElnSourceCatalog.Store store = newStore(); LimsElnSourceCatalog manager = publishingCatalog(store); LimsElnSourceCatalog.Version published = manager.publish( manager.createDraft(input("sample.original_batch")).getId(), 0, "work-item-a"); LimsElnSourceCatalog recordCaller = new LimsElnSourceCatalog(store, () -> new LimsElnSourceCatalog.Actor("0", "inspector", false)); assertEquals(published, recordCaller.published("sample.original_batch", 1)); try { recordCaller.createDraft(input("sample.other")); fail("A record caller must not gain field management permission"); } catch (DataException expected) { assertEquals("Field management permission required", expected.getMessage()); } } @Test public void validatesNativeScalarTypesWithoutReinterpretingTextOrRoundingNumbers() { Map samples = new LinkedHashMap<>(); samples.put("DECIMAL", new BigDecimal("1.234567890123456789")); samples.put("DATE", LocalDate.of(2026, 9, 5)); samples.put("DATETIME", OffsetDateTime.parse("2026-09-05T12:30:00+08:00")); samples.put("BOOLEAN", Boolean.TRUE); for (Map.Entry sample : samples.entrySet()) { LimsElnSourceCatalog catalog = new LimsElnSourceCatalog(newStore(), () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList(sample.getValue()), "{\"metadataRevision\":1}")); LimsElnSourceCatalog.Draft input = input("sample.typed_value_" + sample.getKey()); LimsElnSourceCatalog.Version draft = catalog.createDraft(new LimsElnSourceCatalog.Draft( input.getPropertyCode(), input.getDisplayName(), sample.getKey(), input.getDefinition())); assertEquals(sample.getKey(), catalog.publish(draft.getId(), 0, "work-item-a").getDataType()); } } @Test public void blankSampleValuesDoNotCountAsSuccessfulPublicationValidation() { LimsElnSourceCatalog catalog = new LimsElnSourceCatalog(newStore(), () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList(" "), "{\"metadataRevision\":1}")); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.original_batch")); try { catalog.publish(draft.getId(), 0, "work-item-a"); fail("Blank data is not a successful sample"); } catch (DataException expected) { assertEquals("Source preview must resolve one value", expected.getMessage()); } assertEquals(draft, catalog.detail(draft.getId())); assertTrue(catalog.catalog().isEmpty()); } @Test public void anOldEditorCannotPublishSomeoneElsesUpdatedDraft() { LimsElnSourceCatalog catalog = publishingCatalog(newStore()); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.original_batch")); LimsElnSourceCatalog.Draft changed = input("sample.original_batch"); changed.getDefinition().setFieldId("supplierBatch"); LimsElnSourceCatalog.Version updated = catalog.updateDraft(draft.getId(), 0, changed); try { catalog.publish(draft.getId(), 0, "work-item-a"); fail("Publication must check the editor version before reading data"); } catch (DataException expected) { assertEquals("Source version has changed", expected.getMessage()); } assertEquals(updated, catalog.detail(draft.getId())); } @Test public void failedOrAmbiguousPreviewsNeverPublishTheDraft() { List readers = Arrays.asList( (definition, actor, sampleId) -> { throw new DataException("Sample is not authorized"); }, (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Arrays.asList("same-value", "same-value"), "{\"metadataRevision\":1}"), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList(123), "{\"metadataRevision\":1}"), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList("value"), " "), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList("value"), "{}")); for (int i = 0; i < readers.size(); i++) { LimsElnSourceCatalog catalog = new LimsElnSourceCatalog(newStore(), () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), readers.get(i)); LimsElnSourceCatalog.Version draft = catalog.createDraft(input("sample.invalid_" + i)); try { catalog.publish(draft.getId(), 0, "work-item-a"); fail("Unsuccessful validation must never publish a source"); } catch (DataException expected) { assertEquals(draft, catalog.detail(draft.getId())); } assertTrue(catalog.catalog().isEmpty()); } } @Test public void draftMetadataMustHaveABusinessNameAndASupportedScalarType() { LimsElnSourceCatalog catalog = catalog(newStore()); LimsElnSourceCatalog.Draft source = input("sample.original_batch"); for (LimsElnSourceCatalog.Draft invalid : Arrays.asList( new LimsElnSourceCatalog.Draft(source.getPropertyCode(), " ", "TEXT", source.getDefinition()), new LimsElnSourceCatalog.Draft(source.getPropertyCode(), "Name", "SCRIPT", source.getDefinition()))) { try { catalog.createDraft(invalid); fail("Invalid metadata must be rejected before persistence"); } catch (DataException expected) { assertTrue(catalog.catalog().isEmpty()); } } assertEquals("DRAFT", catalog.createDraft(source).getStatus()); } @Test public void editingDuringPreviewInvalidatesThePendingPublication() { AtomicReference reference = new AtomicReference<>(); AtomicReference draftId = new AtomicReference<>(); LimsElnSourceCatalog catalog = new LimsElnSourceCatalog(newStore(), () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), (definition, actor, sampleId) -> { LimsElnSourceCatalog.Draft changed = input("sample.original_batch"); changed.getDefinition().setFieldId("supplierBatch"); reference.get().updateDraft(draftId.get(), 0, changed); return new LimsElnSourceCatalog.Preview( Collections.singletonList("ORIGINAL-001"), "{\"metadataRevision\":1}"); }); reference.set(catalog); draftId.set(catalog.createDraft(input("sample.original_batch")).getId()); try { catalog.publish(draftId.get(), 0, "work-item-a"); fail("Validation of an old definition must not publish an updated draft"); } catch (DataException expected) { assertEquals("Source version has changed", expected.getMessage()); } assertEquals("supplierBatch", catalog.definition(draftId.get()).getFieldId()); assertTrue(catalog.catalog().isEmpty()); } private static LimsElnSourceCatalog publishingCatalog(LimsElnSourceCatalog.Store store) { return new LimsElnSourceCatalog(store, () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true), (definition, actor, sampleId) -> new LimsElnSourceCatalog.Preview( Collections.singletonList("ORIGINAL-001"), "{\"metadataRevision\":1}")); } private static LimsElnSourceCatalog catalog(LimsElnSourceCatalog.Store store) { return new LimsElnSourceCatalog(store, () -> new LimsElnSourceCatalog.Actor("0", "field-manager", true)); } private static LimsElnSourceCatalog.Draft input(String code) { LimsElnSourceCatalog.Definition definition = new LimsElnSourceCatalog.Definition(); definition.setObjectId("testRequest"); definition.setFieldId("originalBatch"); return new LimsElnSourceCatalog.Draft(code, "Original batch", "TEXT", definition); } protected LimsElnSourceCatalog.Store newStore() { return new MemoryStore(); } private static final class MemoryStore implements LimsElnSourceCatalog.Store { private final Map versions = new LinkedHashMap<>(); private final Map enabled = new LinkedHashMap<>(); @Override public boolean enabled(String tenantId, String code) { return enabled.getOrDefault(tenantId + ":" + code, true); } @Override public Map enabledStates(String tenantId) { Map result = new LinkedHashMap<>(); versions.values().stream().filter(version -> tenantId.equals(version.getTenantId())) .forEach(version -> result.put(version.getPropertyCode(), enabled(tenantId, version.getPropertyCode()))); return result; } @Override public void setEnabled(String tenantId, String code, boolean value) { enabled.put(tenantId + ":" + code, value); } @Override public void insert(LimsElnSourceCatalog.Version version) { versions.put(version.getId(), version); } @Override public boolean replaceDraft(LimsElnSourceCatalog.Version expected, LimsElnSourceCatalog.Version replacement) { return versions.replace(expected.getId(), expected, replacement); } @Override public LimsElnSourceCatalog.Version find(String tenantId, String id) { LimsElnSourceCatalog.Version version = versions.get(id); return version != null && tenantId.equals(version.getTenantId()) ? version : null; } @Override public List list(String tenantId) { return versions.values().stream().filter(v -> tenantId.equals(v.getTenantId())) .collect(Collectors.toList()); } } }