package jnpf.elnService.template.impl;
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
|
import jnpf.elnEntity.template.ElnTemplateCurrentVersionEntity;
|
import jnpf.elnEntity.template.ElnTemplateForms;
|
import jnpf.elnEntity.template.ElnTemplateVersionEntity;
|
import jnpf.elnEntity.template.ElnTemplateVos;
|
import jnpf.elnService.template.ElnTemplateHealthService;
|
import jnpf.elnService.template.ElnTemplatePublishValidator;
|
import jnpf.exception.DataException;
|
import org.junit.Test;
|
import org.springframework.dao.DuplicateKeyException;
|
|
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.Map;
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.fail;
|
|
public class ElnTemplatePublishServiceTest {
|
|
@Test
|
public void publishAggregatesEveryErrorBeforeRejectingWithoutAudit() {
|
Fixture fixture = new Fixture(Arrays.asList(
|
validator(diagnostic("ERROR", "FIRST_ERROR", "第一个错误")),
|
validator(diagnostic("ERROR", "SECOND_ERROR", "第二个错误"))));
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
DataException error = expectDataException(() -> fixture.service.publish("draft-1", "发布原因"));
|
|
assertTrue(error.getMessage().contains("第一个错误"));
|
assertTrue(error.getMessage().contains("第二个错误"));
|
assertFalse(error.getMessage().contains("FIRST_ERROR"));
|
assertFalse(error.getMessage().contains("SECOND_ERROR"));
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void healthValidatorDownloadsAgainAndPromotesPendingAndPreReleaseToErrors() {
|
AtomicInteger checks = new AtomicInteger();
|
ElnTemplateHealthService health = templateId -> {
|
checks.incrementAndGet();
|
ElnTemplateHealthService.HealthReport report = new ElnTemplateHealthService.HealthReport();
|
report.setDiagnostics(Arrays.asList(
|
healthDiagnostic("ERROR", "DUPLICATE_TAG", "标签重复"),
|
healthDiagnostic("WARNING", "PENDING_BINDING", "绑定待确认"),
|
healthDiagnostic("WARNING", "FIELD_VERSION_NOT_PUBLISHED", "字段版本预发布")));
|
return report;
|
};
|
Fixture fixture = new Fixture(Arrays.asList(
|
ElnTemplateCatalogServiceImpl.healthValidator(health)));
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
DataException error = expectDataException(() -> fixture.service.publish("draft-1", "发布原因"));
|
|
assertEquals(1, checks.get());
|
assertTrue(error.getMessage().contains("标签重复"));
|
assertTrue(error.getMessage().contains("绑定待确认"));
|
assertTrue(error.getMessage().contains("字段版本预发布"));
|
assertFalse(error.getMessage().contains("DUPLICATE_TAG"));
|
assertFalse(error.getMessage().contains("PENDING_BINDING"));
|
assertFalse(error.getMessage().contains("FIELD_VERSION_NOT_PUBLISHED"));
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void healthValidatorPromotesSingleCheckboxGroupMemberToPublishError() {
|
ElnTemplateHealthService health = templateId -> {
|
ElnTemplateHealthService.HealthReport report = new ElnTemplateHealthService.HealthReport();
|
report.setDiagnostics(Collections.singletonList(healthDiagnostic(
|
"WARNING", "CHECKBOX_GROUP_SINGLE_MEMBER",
|
"选择组“test_result”只有一个成员,发布前需要补充成员")));
|
return report;
|
};
|
|
List<ElnTemplateVos.Diagnostic> diagnostics =
|
ElnTemplateCatalogServiceImpl.healthValidator(health)
|
.validate("tenant-1", "draft-1");
|
|
assertEquals(1, diagnostics.size());
|
assertEquals("CHECKBOX_GROUP_SINGLE_MEMBER", diagnostics.get(0).getCode());
|
assertEquals("ERROR", diagnostics.get(0).getSeverity());
|
assertEquals("选择组“test_result”只有一个成员,发布前需要补充成员",
|
diagnostics.get(0).getMessage());
|
}
|
|
@Test
|
public void publishFirstVersionAlsoMakesItCurrentAndWritesFixedAudit() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
fixture.service.publish("draft-1", "评审通过");
|
|
assertEquals("PUBLISHED", fixture.store.templates.get("draft-1").getBizStatus());
|
ElnTemplateCurrentVersionEntity current = fixture.store.currents.get("TMP-001");
|
assertEquals("tenant-1", current.getFTenantId());
|
assertEquals("draft-1", current.getDangqianMubanId());
|
assertEquals("HD_jyxm_0001", current.getJianyanXiangmuBianma());
|
assertEquals(Integer.valueOf(0), current.getFVersion());
|
assertEquals(1, fixture.audits.size());
|
AuditEventDTO audit = fixture.audits.get(0);
|
assertEquals("ELN_TEMPLATE_PUBLISH", audit.getActionCode());
|
assertEquals("发布 ELN 模板版本", audit.getActionLabel());
|
assertEquals("eln", audit.getBizModule());
|
assertEquals("eln_template", audit.getBizType());
|
assertEquals("eln_template", audit.getTargetTable());
|
assertEquals("draft-1", audit.getTargetId());
|
assertEquals("评审通过", audit.getReason());
|
}
|
|
@Test
|
public void publishAdditionalVersionDoesNotChangeCurrentPointer() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("published-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.addTemplate("draft-2", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
fixture.addCurrent("TMP-001", "published-1", 3);
|
|
fixture.service.publish("draft-2", "发布新版本");
|
|
assertEquals("PUBLISHED", fixture.store.templates.get("draft-2").getBizStatus());
|
ElnTemplateCurrentVersionEntity current = fixture.store.currents.get("TMP-001");
|
assertEquals("published-1", current.getDangqianMubanId());
|
assertEquals(Integer.valueOf(3), current.getFVersion());
|
}
|
|
@Test
|
public void publishFirstRunnableVersionMakesItCurrentWhenOtherDraftsExist() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
fixture.addTemplate("draft-2", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
fixture.service.publish("draft-2", "发布首个可运行版本");
|
|
assertEquals("draft-2", fixture.store.currents.get("TMP-001").getDangqianMubanId());
|
}
|
|
@Test
|
public void publishNonRunnableVersionDoesNotMakeItCurrent() {
|
Fixture futureFixture = new Fixture(Arrays.asList(validator()));
|
futureFixture.addTemplate(
|
"future-1", "TMP-001", "DRAFT", "enabled", future(), oneDocx());
|
|
futureFixture.service.publish("future-1", "发布未来生效版本");
|
|
assertEquals("PUBLISHED", futureFixture.store.templates.get("future-1").getBizStatus());
|
assertTrue(futureFixture.store.currents.isEmpty());
|
|
Fixture disabledFixture = new Fixture(Arrays.asList(validator()));
|
disabledFixture.addTemplate(
|
"disabled-1", "TMP-001", "DRAFT", "disabled", past(), oneDocx());
|
|
disabledFixture.service.publish("disabled-1", "发布停用版本");
|
|
assertEquals("PUBLISHED", disabledFixture.store.templates.get("disabled-1").getBizStatus());
|
assertTrue(disabledFixture.store.currents.isEmpty());
|
}
|
|
@Test
|
public void publishFreezesDocxToDeterministicSnapshotBeforePublishing() {
|
RecordingFileCopier copier = new RecordingFileCopier();
|
Fixture fixture = new Fixture(Arrays.asList(validator()), copier);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
fixture.service.publish("draft-1", "评审通过");
|
|
assertEquals(Collections.singletonList("template.docx->draft-1-published"), copier.copies);
|
assertEquals("PUBLISHED", fixture.store.templates.get("draft-1").getBizStatus());
|
assertEquals(snapshotDocx("draft-1-published"),
|
fixture.store.templates.get("draft-1").getMubanFile());
|
}
|
|
@Test
|
public void publishCopyFailureLeavesDraftAndOriginalAttachmentUntouched() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()),
|
(attachments, targetId) -> { throw new DataException("模拟复制失败"); });
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
assertEquals(oneDocx(), fixture.store.templates.get("draft-1").getMubanFile());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void publishValidationFailureLeavesDraftAndOriginalAttachmentUntouchedAfterCopy() {
|
RecordingFileCopier copier = new RecordingFileCopier();
|
Fixture fixture = new Fixture(Arrays.asList(
|
validator(diagnostic("ERROR", "HEALTH_FAILED", "快照体检失败"))), copier);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals(Collections.singletonList("template.docx->draft-1-published"), copier.copies);
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
assertEquals(oneDocx(), fixture.store.templates.get("draft-1").getMubanFile());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void publishRetryUsesTheSameSnapshotTarget() {
|
RecordingFileCopier copier = new RecordingFileCopier();
|
AtomicInteger validations = new AtomicInteger();
|
Fixture fixture = new Fixture(Arrays.asList((tenantId, templateId) ->
|
validations.incrementAndGet() == 1
|
? Arrays.asList(diagnostic("ERROR", "HEALTH_FAILED", "快照体检失败"))
|
: new ArrayList<>()), copier);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("draft-1", "第一次发布"));
|
fixture.service.publish("draft-1", "第二次发布");
|
|
assertEquals(Arrays.asList("template.docx->draft-1-published",
|
"template.docx->draft-1-published"), copier.copies);
|
assertEquals(snapshotDocx("draft-1-published"),
|
fixture.store.templates.get("draft-1").getMubanFile());
|
}
|
|
@Test
|
public void publishHealthValidationReadsThePublishedSnapshotInsteadOfTheDraftFile() {
|
List<String> inspectedAttachments = new ArrayList<>();
|
ElnTemplateHealthService health = new ElnTemplateHealthService() {
|
@Override
|
public HealthReport inspect(String templateId) {
|
inspectedAttachments.add(oneDocx());
|
return new HealthReport();
|
}
|
|
@Override
|
public HealthReport inspect(String templateId, String attachments) {
|
inspectedAttachments.add(attachments);
|
return new HealthReport();
|
}
|
};
|
Fixture fixture = new Fixture(Arrays.asList(
|
ElnTemplateCatalogServiceImpl.healthValidator(health)), new RecordingFileCopier());
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
fixture.service.publish("draft-1", "评审通过");
|
|
assertEquals(Collections.singletonList(snapshotDocx("draft-1-published")),
|
inspectedAttachments);
|
}
|
|
@Test
|
public void publishHoldsOnlyOfficeFenceUntilTheDatabaseTransactionCompletes() {
|
List<String> events = new ArrayList<>();
|
RecordingPublishFence fence = new RecordingPublishFence(events);
|
ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner = callback -> {
|
events.add("transaction-begin");
|
ElnTemplateVos.Detail result = callback.get();
|
events.add("transaction-commit");
|
return result;
|
};
|
Fixture fixture = new Fixture(Arrays.asList(validator()),
|
(attachments, targetId) -> {
|
events.add("copy");
|
return snapshotDocx(targetId);
|
}, transactionRunner, fence);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
fixture.service.publish("draft-1", "评审通过");
|
|
assertEquals(Arrays.asList("fence-acquire:tenant-1:draft-1:template.docx:annex",
|
"transaction-begin", "copy", "transaction-commit", "fence-release:fence-1"),
|
events);
|
}
|
|
@Test
|
public void reportsPublishedStateWhenFenceReleaseFailsAfterCommit() {
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence fence =
|
new ElnTemplateCatalogServiceImpl.TemplatePublishFence() {
|
@Override
|
public String acquire(String tenantId, String templateId,
|
String fileId, String fileType) {
|
return "fence-1";
|
}
|
|
@Override
|
public void release(String tenantId, String templateId,
|
String fileId, String fileType, String token) {
|
throw new DataException("模拟发布锁释放失败");
|
}
|
};
|
Fixture fixture = new Fixture(Arrays.asList(validator()), new RecordingFileCopier(),
|
callback -> callback.get(), fence);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
DataException error = expectDataException(
|
() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals("模板已发布成功,但 OnlyOffice 发布锁释放失败,需人工恢复并刷新页面",
|
error.getMessage());
|
assertEquals("PUBLISHED", fixture.store.templates.get("draft-1").getBizStatus());
|
assertEquals(1, fixture.audits.size());
|
}
|
|
@Test
|
public void publishReleasesOnlyOfficeFenceAfterTransactionFailure() {
|
List<String> events = new ArrayList<>();
|
RecordingPublishFence fence = new RecordingPublishFence(events);
|
ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner = callback -> {
|
events.add("transaction-begin");
|
try {
|
return callback.get();
|
} catch (RuntimeException e) {
|
events.add("transaction-rollback");
|
throw e;
|
}
|
};
|
Fixture fixture = new Fixture(Arrays.asList(validator()),
|
(attachments, targetId) -> {
|
throw new DataException("模拟复制失败");
|
}, transactionRunner, fence);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals(Arrays.asList("fence-acquire:tenant-1:draft-1:template.docx:annex",
|
"transaction-begin", "transaction-rollback", "fence-release:fence-1"), events);
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
}
|
|
@Test
|
public void publishRejectsAttachmentChangedAfterFenceAcquireAndThenReleasesFence() {
|
List<String> events = new ArrayList<>();
|
Runnable[] afterAcquire = {() -> { }};
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence fence =
|
new ElnTemplateCatalogServiceImpl.TemplatePublishFence() {
|
@Override
|
public String acquire(String tenantId, String templateId,
|
String fileId, String fileType) {
|
events.add("fence-acquire:" + fileId);
|
afterAcquire[0].run();
|
return "fence-1";
|
}
|
|
@Override
|
public void release(String tenantId, String templateId,
|
String fileId, String fileType, String token) {
|
events.add("fence-release:" + token);
|
}
|
};
|
ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner = callback -> {
|
events.add("transaction-begin");
|
try {
|
return callback.get();
|
} catch (RuntimeException e) {
|
events.add("transaction-rollback");
|
throw e;
|
}
|
};
|
AtomicInteger copies = new AtomicInteger();
|
AtomicInteger validations = new AtomicInteger();
|
Fixture fixture = new Fixture(Arrays.asList((tenantId, templateId) -> {
|
validations.incrementAndGet();
|
return new ArrayList<>();
|
}), (attachments, targetId) -> {
|
copies.incrementAndGet();
|
return snapshotDocx(targetId);
|
}, transactionRunner, fence);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
afterAcquire[0] = () -> fixture.store.templates.get("draft-1")
|
.setMubanFile("[{\"fileId\":\"changed.docx\",\"fileExtension\":\"docx\"}]");
|
|
DataException error = expectDataException(
|
() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals("模板附件已变化,请刷新后重试", error.getMessage());
|
assertEquals(0, copies.get());
|
assertEquals(0, validations.get());
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
assertTrue(fixture.audits.isEmpty());
|
assertEquals(Arrays.asList("fence-acquire:template.docx", "transaction-begin",
|
"transaction-rollback", "fence-release:fence-1"), events);
|
}
|
|
@Test
|
public void publishFenceAcquireFailurePreventsSnapshotAndValidation() {
|
AtomicInteger copies = new AtomicInteger();
|
AtomicInteger validations = new AtomicInteger();
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence rejectingFence =
|
new ElnTemplateCatalogServiceImpl.TemplatePublishFence() {
|
@Override
|
public String acquire(String tenantId, String templateId,
|
String fileId, String fileType) {
|
throw new DataException("文档仍在编辑或保存中");
|
}
|
|
@Override
|
public void release(String tenantId, String templateId,
|
String fileId, String fileType, String token) {
|
fail("未取得 fence 时不得释放");
|
}
|
};
|
Fixture fixture = new Fixture(Arrays.asList((tenantId, templateId) -> {
|
validations.incrementAndGet();
|
return new ArrayList<>();
|
}), (attachments, targetId) -> {
|
copies.incrementAndGet();
|
return snapshotDocx(targetId);
|
}, callback -> callback.get(), rejectingFence);
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("draft-1", "评审通过"));
|
|
assertEquals(0, copies.get());
|
assertEquals(0, validations.get());
|
assertEquals("DRAFT", fixture.store.templates.get("draft-1").getBizStatus());
|
}
|
|
@Test
|
public void publishRejectsNonDraftBeforeRunningValidators() {
|
AtomicInteger validations = new AtomicInteger();
|
Fixture fixture = new Fixture(Arrays.asList((tenantId, templateId) -> {
|
validations.incrementAndGet();
|
return new ArrayList<>();
|
}));
|
fixture.addTemplate("published-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
|
expectDataException(() -> fixture.service.publish("published-1", "重复发布"));
|
|
assertEquals(0, validations.get());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void enableAndDisableRequireReasonAndDoNotChangeCurrentPointer() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("published-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.addCurrent("TMP-001", "published-1", 2);
|
|
expectDataException(() -> fixture.service.disable("published-1", " "));
|
fixture.service.disable("published-1", "临时停止新建记录");
|
assertEquals("disabled", fixture.store.templates.get("published-1").getBizEnabled());
|
assertEquals("published-1", fixture.store.currents.get("TMP-001").getDangqianMubanId());
|
assertEquals("ELN_TEMPLATE_DISABLE", fixture.audits.get(0).getActionCode());
|
assertEquals("临时停止新建记录", fixture.audits.get(0).getReason());
|
|
fixture.service.enable("published-1", "恢复新建记录");
|
assertEquals("enabled", fixture.store.templates.get("published-1").getBizEnabled());
|
assertEquals("published-1", fixture.store.currents.get("TMP-001").getDangqianMubanId());
|
assertEquals("ELN_TEMPLATE_ENABLE", fixture.audits.get(1).getActionCode());
|
}
|
|
@Test
|
public void switchCurrentRevalidatesAndUsesPointerVersionCas() {
|
AtomicInteger validations = new AtomicInteger();
|
Fixture fixture = new Fixture(Arrays.asList((tenantId, templateId) -> {
|
validations.incrementAndGet();
|
return new ArrayList<>();
|
}));
|
fixture.addTemplate("old-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.addTemplate("new-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.addCurrent("TMP-001", "old-1", 4);
|
|
fixture.service.switchCurrent("TMP-001", "new-1", "切换新版本");
|
|
assertEquals(1, validations.get());
|
ElnTemplateCurrentVersionEntity current = fixture.store.currents.get("TMP-001");
|
assertEquals("new-1", current.getDangqianMubanId());
|
assertEquals(Integer.valueOf(5), current.getFVersion());
|
assertEquals("PUBLISHED", fixture.store.templates.get("old-1").getBizStatus());
|
assertEquals("PUBLISHED", fixture.store.templates.get("new-1").getBizStatus());
|
AuditEventDTO audit = fixture.audits.get(0);
|
assertEquals("ELN_TEMPLATE_SWITCH_CURRENT", audit.getActionCode());
|
assertEquals("切换新版本", audit.getReason());
|
assertTrue(audit.getDataSnapshot().contains("old-1"));
|
assertTrue(audit.getDataSnapshot().contains("new-1"));
|
}
|
|
@Test
|
public void switchCurrentSafelyInsertsMissingPointer() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("new-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
|
fixture.service.switchCurrent("TMP-001", "new-1", "首次设置");
|
|
ElnTemplateCurrentVersionEntity current = fixture.store.currents.get("TMP-001");
|
assertEquals("tenant-1", current.getFTenantId());
|
assertEquals("new-1", current.getDangqianMubanId());
|
assertEquals(Integer.valueOf(0), current.getFVersion());
|
assertNull(fixture.audits.get(0).getOperationId());
|
}
|
|
@Test
|
public void switchCurrentRejectsIneligibleTargetsAndTenantOrCodeMismatch() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
fixture.addTemplate("disabled-1", "TMP-001", "PUBLISHED", "disabled", past(), oneDocx());
|
fixture.addTemplate("future-1", "TMP-001", "PUBLISHED", "enabled", future(), oneDocx());
|
fixture.addTemplate("missing-file", "TMP-001", "PUBLISHED", "enabled", past(), "[]");
|
fixture.addTemplate("other-code", "TMP-002", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.addTemplate("other-tenant", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx(), "tenant-2");
|
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "draft-1", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "disabled-1", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "future-1", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "missing-file", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "other-code", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "other-tenant", "原因"));
|
expectDataException(() -> fixture.service.switchCurrent("TMP-001", "other-code", " "));
|
|
assertTrue(fixture.store.currents.isEmpty());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void casOrUniqueInsertConflictDoesNotWriteSuccessAudit() {
|
Fixture casFixture = new Fixture(Arrays.asList(validator()));
|
casFixture.addTemplate("new-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
casFixture.addCurrent("TMP-001", "old-1", 1);
|
casFixture.store.failCurrentCas = true;
|
|
expectDataException(() -> casFixture.service.switchCurrent("TMP-001", "new-1", "并发切换"));
|
assertTrue(casFixture.audits.isEmpty());
|
|
Fixture insertFixture = new Fixture(Arrays.asList(validator()));
|
insertFixture.addTemplate("new-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
insertFixture.store.failCurrentInsert = true;
|
|
expectDataException(() -> insertFixture.service.switchCurrent("TMP-001", "new-1", "并发首次设置"));
|
assertTrue(insertFixture.audits.isEmpty());
|
}
|
|
@Test
|
public void currentInsertReturningZeroDoesNotWriteSuccessAudit() {
|
Fixture fixture = new Fixture(Arrays.asList(validator()));
|
fixture.addTemplate("new-1", "TMP-001", "PUBLISHED", "enabled", past(), oneDocx());
|
fixture.store.skipCurrentInsert = true;
|
|
expectDataException(() -> fixture.service.switchCurrent(
|
"TMP-001", "new-1", "首次设置未落库"));
|
|
assertTrue(fixture.store.currents.isEmpty());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void lifecycleMutationsAcquireTheTemplateWriteLockBeforeValidation() {
|
Fixture publish = new Fixture(Arrays.asList(validator()));
|
publish.addTemplate("draft-1", "TMP-001", "DRAFT", "enabled", past(), oneDocx());
|
publish.service.publish("draft-1", "发布");
|
assertEquals(Arrays.asList("draft-1"), publish.store.lockedTemplateIds);
|
|
Fixture switching = new Fixture(Arrays.asList(validator()));
|
switching.addTemplate("published-1", "TMP-001", "PUBLISHED", "enabled",
|
past(), oneDocx());
|
switching.service.switchCurrent("TMP-001", "published-1", "切换");
|
assertEquals(Arrays.asList("published-1"), switching.store.lockedTemplateIds);
|
|
Fixture disabling = new Fixture(Arrays.asList(validator()));
|
disabling.addTemplate("published-1", "TMP-001", "PUBLISHED", "enabled",
|
past(), oneDocx());
|
disabling.service.disable("published-1", "停用");
|
assertEquals(Arrays.asList("published-1"), disabling.store.lockedTemplateIds);
|
}
|
|
private static ElnTemplatePublishValidator validator(ElnTemplateVos.Diagnostic... diagnostics) {
|
return (tenantId, templateId) -> Arrays.asList(diagnostics);
|
}
|
|
private static ElnTemplateVos.Diagnostic diagnostic(String severity, String code, String message) {
|
ElnTemplateVos.Diagnostic result = new ElnTemplateVos.Diagnostic();
|
result.setSeverity(severity);
|
result.setCode(code);
|
result.setMessage(message);
|
return result;
|
}
|
|
private static ElnTemplateHealthService.HealthDiagnostic healthDiagnostic(
|
String severity, String code, String message) {
|
ElnTemplateHealthService.HealthDiagnostic result =
|
new ElnTemplateHealthService.HealthDiagnostic();
|
result.setSeverity(severity);
|
result.setCode(code);
|
result.setMessage(message);
|
return result;
|
}
|
|
private static DataException expectDataException(Runnable action) {
|
try {
|
action.run();
|
fail("预期抛出 DataException");
|
return null;
|
} catch (DataException expected) {
|
assertFalse(expected.getMessage().trim().isEmpty());
|
return expected;
|
}
|
}
|
|
private static Date past() {
|
return new Date(System.currentTimeMillis() - 60000L);
|
}
|
|
private static Date future() {
|
return new Date(System.currentTimeMillis() + 60000L);
|
}
|
|
private static String oneDocx() {
|
return "[{\"fileId\":\"template.docx\",\"fileExtension\":\"docx\"}]";
|
}
|
|
private static String snapshotDocx(String targetId) {
|
return "[{\"fileId\":\"" + targetId + ".docx\",\"fileExtension\":\"docx\"}]";
|
}
|
|
private static final class Fixture {
|
private final MemoryStore store = new MemoryStore();
|
private final List<AuditEventDTO> audits = new ArrayList<>();
|
private final ElnTemplateCatalogServiceImpl service;
|
|
private Fixture(List<ElnTemplatePublishValidator> validators) {
|
this(validators, (attachments, targetId) -> attachments);
|
}
|
|
private Fixture(List<ElnTemplatePublishValidator> validators,
|
ElnTemplateCatalogServiceImpl.TemplateFileCopier fileCopier) {
|
this(validators, fileCopier, callback -> callback.get(),
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence.noop());
|
}
|
|
private Fixture(List<ElnTemplatePublishValidator> validators,
|
ElnTemplateCatalogServiceImpl.TemplateFileCopier fileCopier,
|
ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner,
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence publishFence) {
|
service = new ElnTemplateCatalogServiceImpl(
|
store,
|
() -> new ElnTemplateCatalogServiceImpl.UserContext("tenant-1", "user-1"),
|
ElnTemplateCatalogServiceImpl::deterministicTargetId,
|
() -> "binding-new",
|
fileCopier,
|
audits::add,
|
transactionRunner,
|
validators,
|
publishFence);
|
}
|
|
private void addTemplate(String id, String code, String status, String enabled,
|
Date effectiveDate, String attachments) {
|
addTemplate(id, code, status, enabled, effectiveDate, attachments, "tenant-1");
|
}
|
|
private void addTemplate(String id, String code, String status, String enabled,
|
Date effectiveDate, String attachments, String tenantId) {
|
ElnTemplateVersionEntity template = new ElnTemplateVersionEntity();
|
template.setId(id);
|
template.setFTenantId(tenantId);
|
template.setMubanBianhao(code);
|
template.setMubanBanben("V1");
|
template.setJianyanXiangmu("test-definition-1");
|
template.setJianyanXiangmuBianma("HD_jyxm_0001");
|
template.setJianyanXiangmuMingcheng("pH值");
|
template.setBizStatus(status);
|
template.setBizEnabled(enabled);
|
template.setShengxiaoRiqi(effectiveDate);
|
template.setMubanFile(attachments);
|
template.setFVersion(0);
|
template.setFDeleteMark(0);
|
store.templates.put(id, template);
|
}
|
|
private void addCurrent(String code, String templateId, int version) {
|
ElnTemplateCurrentVersionEntity current = new ElnTemplateCurrentVersionEntity();
|
current.setId("current-" + code);
|
current.setFTenantId("tenant-1");
|
current.setMubanBianhao(code);
|
current.setDangqianMubanId(templateId);
|
current.setFVersion(version);
|
current.setFDeleteMark(0);
|
store.currents.put(code, current);
|
}
|
}
|
|
private static final class RecordingPublishFence
|
implements ElnTemplateCatalogServiceImpl.TemplatePublishFence {
|
private final List<String> events;
|
|
private RecordingPublishFence(List<String> events) {
|
this.events = events;
|
}
|
|
@Override
|
public String acquire(String tenantId, String templateId, String fileId, String fileType) {
|
events.add("fence-acquire:" + tenantId + ":" + templateId + ":" + fileId
|
+ ":" + fileType);
|
return "fence-1";
|
}
|
|
@Override
|
public void release(String tenantId, String templateId, String fileId,
|
String fileType, String token) {
|
events.add("fence-release:" + token);
|
}
|
}
|
|
private static final class RecordingFileCopier
|
implements ElnTemplateCatalogServiceImpl.TemplateFileCopier {
|
private final List<String> copies = new ArrayList<>();
|
|
@Override
|
public String copy(String attachments, String targetId) {
|
copies.add("template.docx->" + targetId);
|
return snapshotDocx(targetId);
|
}
|
}
|
|
private static final class MemoryStore
|
implements ElnTemplateCatalogServiceImpl.TemplateCatalogStore {
|
private final Map<String, ElnTemplateVersionEntity> templates = new LinkedHashMap<>();
|
private final Map<String, ElnTemplateCurrentVersionEntity> currents = new LinkedHashMap<>();
|
private final List<String> lockedTemplateIds = new ArrayList<>();
|
private boolean failCurrentCas;
|
private boolean failCurrentInsert;
|
private boolean skipCurrentInsert;
|
|
@Override
|
public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) {
|
ElnTemplateVersionEntity template = templates.get(templateId);
|
return template != null && tenantId.equals(template.getFTenantId()) ? template : null;
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findTemplateForUpdate(String tenantId, String templateId) {
|
lockedTemplateIds.add(templateId);
|
return findTemplate(tenantId, templateId);
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findByOperation(String tenantId, String operationCode) {
|
return null;
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findByCodeAndVersion(
|
String tenantId, String templateCode, String businessVersion) {
|
return null;
|
}
|
|
@Override
|
public void insertTemplate(ElnTemplateVersionEntity template) {
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public void updateTemplateFile(ElnTemplateVersionEntity template) {
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public void updateTemplateLifecycle(ElnTemplateVersionEntity template) {
|
ElnTemplateVersionEntity stored = templates.get(template.getId());
|
if (stored == null || !stored.getFVersion().equals(template.getFVersion())) {
|
throw new DataException("模板状态已变化,请刷新后重试");
|
}
|
template.setFVersion(template.getFVersion() + 1);
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public ElnTemplateCurrentVersionEntity findCurrent(String tenantId, String templateCode) {
|
ElnTemplateCurrentVersionEntity current = currents.get(templateCode);
|
return current != null && tenantId.equals(current.getFTenantId()) ? current : null;
|
}
|
|
@Override
|
public List<ElnTemplateCurrentVersionEntity> listCurrentVersions(
|
String tenantId, List<String> templateCodes) {
|
List<ElnTemplateCurrentVersionEntity> result = new ArrayList<>();
|
for (String code : templateCodes) {
|
ElnTemplateCurrentVersionEntity current = findCurrent(tenantId, code);
|
if (current != null) {
|
result.add(current);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public boolean insertCurrent(ElnTemplateCurrentVersionEntity current) {
|
if (failCurrentInsert) {
|
throw new DuplicateKeyException("模拟当前指针唯一冲突");
|
}
|
if (skipCurrentInsert) {
|
return false;
|
}
|
currents.put(current.getMubanBianhao(), current);
|
return true;
|
}
|
|
@Override
|
public void updateCurrent(ElnTemplateCurrentVersionEntity current) {
|
if (failCurrentCas) {
|
throw new DataException("当前模板已变化,请刷新后重试");
|
}
|
ElnTemplateCurrentVersionEntity stored = currents.get(current.getMubanBianhao());
|
if (stored == null || !stored.getFVersion().equals(current.getFVersion())) {
|
throw new DataException("当前模板已变化,请刷新后重试");
|
}
|
current.setFVersion(current.getFVersion() + 1);
|
currents.put(current.getMubanBianhao(), current);
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listActiveBindings(String tenantId, String templateId) {
|
return new ArrayList<>();
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listActiveBindingsByTemplateIds(
|
String tenantId, List<String> templateIds) {
|
return new ArrayList<>();
|
}
|
|
@Override
|
public void insertBinding(ElnTemplateBindingEntity binding) {
|
}
|
|
@Override
|
public IPage<ElnTemplateVersionEntity> page(String tenantId, ElnTemplateForms.Query query) {
|
return new Page<>();
|
}
|
|
@Override
|
public IPage<ElnTemplateVos.CatalogItem> pageDirectory(
|
String tenantId, ElnTemplateForms.Query query) {
|
return new Page<>();
|
}
|
}
|
}
|