package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.fenfa.*; import jnpf.dmsEntity.fenfa.DmsWenjianDayinFasongEntity.Batch; import jnpf.dmsEntity.fenfa.form.DmsWenjianChongdaForm; import jnpf.dmsMapper.*; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class DmsWenjianChongdaServiceTest { final DmsCurrentIdentity identity = mock(DmsCurrentIdentity.class); final DmsWenjianFenfaMapper copies = mock(DmsWenjianFenfaMapper.class); final DmsWenjianDayinQuerenMapper versions = mock(DmsWenjianDayinQuerenMapper.class); final DmsWenjianChongdaMapper mapper = mock(DmsWenjianChongdaMapper.class); final DmsWenjianBupinghengMapper documents = mock(DmsWenjianBupinghengMapper.class); final DmsFileVersionMapper fileVersions = mock(DmsFileVersionMapper.class); final DmsPermissionDecisionService permissions = mock(DmsPermissionDecisionService.class); final jnpf.dmsIntegration.audit.DmsBusinessChangeAudit businessAudit = mock(jnpf.dmsIntegration.audit.DmsBusinessChangeAudit.class); final DmsWenjianChongdaService service = new DmsWenjianChongdaService(businessAudit,identity,copies,versions,mapper,documents,fileVersions,permissions); final DmsWenjianChongdaForm form = new DmsWenjianChongdaForm(); final DmsWenjianDayinJiluEntity copy = new DmsWenjianDayinJiluEntity(); DmsWenjianChongdaFasong saved; @BeforeEach void setup() { UserInfo user = new UserInfo(); user.setUserId("actor"); when(identity.requireUser()).thenReturn(user); when(identity.tenantId(user)).thenReturn("tenant"); copy.setId("copy"); copy.setDayinRenwuId("task"); copy.setBizStatus("daifafang"); copy.setDayinLiushuihao("original"); copy.setDayinJishuLiushuihao("20260003"); when(copies.selectPrintForUpdate("tenant","copy",null)).thenReturn(copy); var version = new DmsWenjianDayinQuerenEntity(); version.setFileVersionId("019a1234-1234-7123-8123-123456789012"); when(versions.selectTaskVersion("tenant","task")).thenReturn(version); var source = new jnpf.dmsEntity.permission.entity.DmsFileVersionEntity(); source.setShifouShoukong("no"); when(fileVersions.selectReadableByTenantAndId(eq("tenant"),any())).thenReturn(source); when(mapper.validSignature(eq("tenant"),eq("actor"),eq("signed"),eq("copy"),anyString(),eq("纸张损坏"))).thenReturn(new DmsWenjianChongdaFasong()); when(mapper.selectActive("tenant","copy")).thenAnswer(c -> saved != null && List.of("OPEN","CONFIRMED").contains(saved.getState()) ? saved : null); when(mapper.selectAttempt(eq("tenant"),anyString())).thenAnswer(c -> saved != null && saved.getId().equals(c.getArgument(1)) ? saved : null); when(mapper.insertAttempt(any())).thenAnswer(c -> { saved=c.getArgument(0); return 1; }); when(mapper.updateAttempt(any())).thenReturn(1); form.setRequestId(UUID.randomUUID().toString()); form.setPrintRecordId("copy"); form.setReason("纸张损坏"); form.setBizSign("signed"); } Batch batch(int... pages) { Batch b = new Batch(); b.setId(UUID.randomUUID().toString()); b.setItems(List.of(0)); b.setPages(Arrays.stream(pages).boxed().toList()); return b; } void registerNativeDocument() { var linked = new DmsWenjianChongdaFasong(); linked.setDocumentId("doc"); when(mapper.findDocument("tenant", saved.getId())).thenReturn(linked); var doc = new DmsWenjianBupinghengExecution(); doc.setBizSign("signed"); doc.setDengjiRenId("actor"); when(documents.lockDocument("tenant", "doc")).thenReturn(doc); var item = new DmsWenjianBupinghengExecution.Item(); item.setDayinJiluId("copy"); item.setCuoshi("cuoshi3"); item.setYuanyin("纸张损坏"); when(documents.lockItems("tenant", "doc")).thenReturn(List.of(item)); } void begin() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1,2))); service.plan(form, saved.getFileVersionId(), 2); registerNativeDocument(); form.setBatchId(form.getBatches().get(0).getId()); form.setToken(UUID.randomUUID().toString()); service.begin(form); } @Test void dispatchBeforeNativeRegistrationAndCancelWithoutBusinessDocument() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1))); service.plan(form, saved.getFileVersionId(), 1); form.setBatchId(form.getBatches().getFirst().getId()); form.setToken(UUID.randomUUID().toString()); service.begin(form); assertTrue(saved.getBatchesJson().contains("SENDING")); verifyNoInteractions(documents); assertThrows(DataException.class, () -> service.finish(form)); service.submitted(form); assertThrows(DataException.class, () -> service.finish(form)); assertEquals("CONFIRMED", saved.getState()); registerNativeDocument(); when(mapper.completeDocument(any())).thenReturn(1); service.finish(form); assertEquals("CLOSED", saved.getState()); } @Test void dispatchChecksExistingLegacyRegistrationIfPresent() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1))); service.plan(form, saved.getFileVersionId(), 1); form.setBatchId(form.getBatches().getFirst().getId()); form.setToken(UUID.randomUUID().toString()); registerNativeDocument(); documents.lockItems("tenant", "doc").getFirst().setYuanyin("changed"); assertThrows(DataException.class, () -> service.begin(form)); documents.lockItems("tenant", "doc").getFirst().setYuanyin("纸张损坏"); documents.lockDocument("tenant", "doc").setZhixingShijian(new Date()); assertThrows(DataException.class, () -> service.begin(form)); documents.lockDocument("tenant", "doc").setZhixingShijian(null); service.begin(form); assertEquals("doc", saved.getDocumentId()); assertTrue(saved.getBatchesJson().contains("SENDING")); verify(mapper, never()).completeDocument(any()); } @Test void freezesOriginalNumbersAndOneCopyWithoutConsumingTask() throws Exception { form.setCopies(99); service.prepare(form); assertEquals(1,saved.getCopies()); assertEquals("actor",saved.getUserId()); var number=JsonUtil.getJsonToList(saved.getNumbersJson(),DmsWenjianDayinHaoma.class).getFirst(); assertEquals("original",number.getSerialNumber()); assertEquals("20260003",number.getCountSerialNumber()); verify(copies,never()).countConsumedPrints(anyString(),anyString()); verifyNoInteractions(documents); } @Test void restoresFrozenReasonAndSignatureInsteadOfNewInput() throws Exception { service.prepare(form); String id=saved.getId(); form.setReason("changed"); form.setBizSign("new sign"); assertEquals(id,service.prepare(form).getId()); assertEquals("纸张损坏",saved.getReason()); assertEquals("signed",saved.getBizSign()); verify(mapper,times(1)).insertAttempt(any()); } @Test void anotherRequestCannotReplaceActiveAttempt() throws Exception { service.prepare(form); form.setRequestId(UUID.randomUUID().toString()); assertThrows(DataException.class,()->service.prepare(form)); verify(mapper,times(1)).insertAttempt(any()); } @Test void forgedSignatureCannotStart() { when(mapper.validSignature(anyString(),anyString(),anyString(),anyString(),anyString(),anyString())).thenReturn(null); assertThrows(DataException.class,()->service.prepare(form)); verify(mapper,never()).insertAttempt(any()); } @Test void missingVersionCannotStart() { when(fileVersions.selectReadableByTenantAndId(anyString(),any())).thenReturn(null); assertThrows(DataException.class,()->service.prepare(form)); verify(mapper,never()).insertAttempt(any()); } @Test void historicalRecordKeepsMissingCountSerial() throws Exception { copy.setDayinJishuLiushuihao(null); service.prepare(form); assertNull(JsonUtil.getJsonToList(saved.getNumbersJson(),DmsWenjianDayinHaoma.class).getFirst().getCountSerialNumber()); } @Test void completedRequestResponseLossDoesNotPrintAgain() throws Exception { service.prepare(form); saved.setState("CLOSED"); assertEquals("CLOSED",service.prepare(form).getState()); verify(mapper,times(1)).insertAttempt(any()); } @Test void rejectsSignedReceivedAndVoidedRecords() { for (String status: List.of("yiqianshou","yizuofei","yihuishou")) { copy.setBizStatus(status); assertThrows(DataException.class,()->service.prepare(form)); } verify(mapper,never()).insertAttempt(any()); } @Test void anotherActorCannotResume() throws Exception { service.prepare(form); saved.setUserId("other"); assertThrows(DataException.class,()->service.prepare(form)); assertThrows(DataException.class,()->service.active("copy")); var summary=service.lockStatus("copy"); assertTrue(summary.isLocked()); assertFalse(summary.isOwner()); assertFalse(summary.isCanCancel()); assertFalse(summary.isCanResume()); assertThrows(DataException.class,()->service.cancel(form)); } @Test void overlongReasonRejectedBeforePrintToMatchNativeColumn() { form.setReason("字".repeat(256)); assertThrows(DataException.class,()->service.prepare(form)); verify(mapper,never()).insertAttempt(any()); } @Test void noSignatureCannotStart() { form.setBizSign(""); assertThrows(DataException.class,()->service.prepare(form)); } @Test void unknownBatchNeverGetsSecondPermit() throws Exception { begin(); assertThrows(DataException.class,()->service.begin(form)); assertThrows(DataException.class,()->service.finish(form)); verify(mapper,never()).completeDocument(any()); } @Test void receiptSurvivesStatusChangeAndDuplicateKeepsSuccessTime() throws Exception { begin(); copy.setBizStatus("yiqianshou"); service.submitted(form); Date time=saved.getSubmittedAt(); service.submitted(form); assertEquals(time,saved.getSubmittedAt()); assertEquals("CONFIRMED",saved.getState()); form.setToken(UUID.randomUUID().toString()); assertThrows(DataException.class,()->service.submitted(form)); } @Test void partialReceiptDoesNotAllowRegistration() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1),batch(2))); service.plan(form, saved.getFileVersionId(), 2); registerNativeDocument(); form.setBatchId(form.getBatches().getFirst().getId()); form.setToken(UUID.randomUUID().toString()); service.begin(form); service.submitted(form); assertEquals("OPEN",saved.getState()); assertNull(saved.getSubmittedAt()); assertThrows(DataException.class,()->service.finish(form)); } @Test void finishChecksNativeDocumentAndUsesOriginalActorOnce() throws Exception { begin(); service.submitted(form); var linked=new DmsWenjianChongdaFasong(); linked.setDocumentId("doc"); when(mapper.findDocument("tenant",saved.getId())).thenReturn(linked); var doc=new DmsWenjianBupinghengExecution(); doc.setBizSign("signed"); doc.setDengjiRenId("actor"); when(documents.lockDocument("tenant","doc")).thenReturn(doc); var item=new DmsWenjianBupinghengExecution.Item(); item.setDayinJiluId("copy"); item.setCuoshi("cuoshi3"); item.setYuanyin("纸张损坏"); when(documents.lockItems("tenant","doc")).thenReturn(List.of(item)); when(mapper.completeDocument(any())).thenReturn(1); item.setCuoshi("cuoshi1"); assertThrows(DataException.class,()->service.finish(form)); item.setCuoshi("cuoshi3"); service.finish(form); service.finish(form); assertEquals("CLOSED",saved.getState()); assertEquals("actor",saved.getUserId()); verify(mapper,times(1)).completeDocument(saved); verify(documents,never()).voidPrint(anyString(),anyString(),anyString(),any()); } @Test void originalOwnerCanCancelOnlyBeforeAnyPermitAndCancellationIsIdempotent() throws Exception { service.prepare(form); assertTrue(service.lockStatus("copy").isCanCancel()); service.cancel(form); Date cancelled=saved.getCancelledAt(); service.cancel(form); assertEquals(cancelled,saved.getCancelledAt()); assertEquals("CANCELLED",saved.getState()); assertFalse(service.lockStatus("copy").isLocked()); assertThrows(DataException.class,()->service.prepare(form)); verifyNoInteractions(documents); } @Test void cancelWithPlanBeforeSendingAndBeginCannotReviveCancelledTask() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1,2))); service.plan(form,saved.getFileVersionId(),2); form.setBatchId(form.getBatches().getFirst().getId()); form.setToken(UUID.randomUUID().toString()); service.cancel(form); assertThrows(DataException.class,()->service.begin(form)); } @Test void unknownAndConfirmedTasksCannotBeCancelled() throws Exception { begin(); assertEquals("UNKNOWN",service.lockStatus("copy").getPhase()); assertFalse(service.lockStatus("copy").isCanCancel()); assertFalse(service.lockStatus("copy").isCanResume()); assertThrows(DataException.class,()->service.cancel(form)); service.submitted(form); assertEquals("REGISTRATION_PENDING",service.lockStatus("copy").getPhase()); assertTrue(service.lockStatus("copy").isCanResume()); assertThrows(DataException.class,()->service.cancel(form)); } @Test void incompleteOrWrongVersionPlanRejectedAgainstAuthoritativeCount() throws Exception { service.prepare(form); form.setBatches(List.of(batch(1))); assertThrows(DataException.class,()->service.plan(form,saved.getFileVersionId(),2)); assertNull(saved.getPageCount()); form.setBatches(List.of(batch(1,2))); assertThrows(DataException.class,()->service.plan(form,"other-version",2)); service.plan(form,saved.getFileVersionId(),2); assertEquals(2,saved.getPageCount()); } @Test void controlAuditTracksOnlyChangesAndDoesNotExposeDispatchToken() throws Exception { service.prepare(form); verifyNoInteractions(businessAudit); form.setBatches(List.of(batch(1))); service.plan(form, saved.getFileVersionId(), 1); clearInvocations(businessAudit); service.plan(form, saved.getFileVersionId(), 1); verifyNoInteractions(businessAudit); form.setBatchId(form.getBatches().getFirst().getId()); form.setToken(UUID.randomUUID().toString()); service.begin(form); verify(businessAudit).record(eq("tenant"), eq("DMS_REPRINT_BATCH_BEGIN"), anyString(), eq("dms_wenjian_chongda_fasong"), eq(form.getRequestId()), argThat(m -> m.toString().contains("READY")), argThat(m -> m.toString().contains("SENDING") && !m.toString().contains(form.getToken())), eq("纸张损坏"), argThat(m -> "copy".equals(m.get("print_record_id")) && "signed".equals(m.get("biz_sign")))); clearInvocations(businessAudit); service.submitted(form); service.submitted(form); verify(businessAudit, times(1)).record(eq("tenant"), eq("DMS_REPRINT_BATCH_RECEIPT"), anyString(), anyString(), anyString(), anyMap(), argThat(m -> "CONFIRMED".equals(m.get("fasong_zhuangtai"))), anyString(), anyMap()); clearInvocations(businessAudit); registerNativeDocument(); when(mapper.completeDocument(any())).thenReturn(1); service.finish(form); service.finish(form); verify(businessAudit, times(1)).record(eq("tenant"), eq("DMS_REPRINT_EXECUTION_CONFIRMED"), anyString(), eq("dms_wenjian_bupingheng_dengji"), eq("doc"), argThat(m -> m.get("zhixing_shijian") == null), argThat(m -> m.get("zhixing_shijian") != null), anyString(), anyMap()); verify(businessAudit, times(1)).record(eq("tenant"), eq("DMS_REPRINT_COMPLETE"), anyString(), anyString(), anyString(), anyMap(), argThat(m -> "CLOSED".equals(m.get("fasong_zhuangtai"))), anyString(), anyMap()); verifyNoMoreInteractions(businessAudit); } }