package jnpf.base.service; import jnpf.base.ActionResult; import jnpf.base.UserInfo; import jnpf.base.model.flow.DataModel; import jnpf.base.model.uploadedit.*; import jnpf.dms.DmsRevisionEditApi; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import jnpf.util.TicketUtil; import jnpf.util.UserProvider; import org.junit.Test; import org.mockito.MockedStatic; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import java.util.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class DmsAuditTemporaryEditServiceTest { @org.junit.BeforeClass public static void initializeStandaloneContext() { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); } @Test public void txtAndPdfSourcesCanSaveButCannotStartOnlineEditing() { when(access.isDmsAuditTemporary(any())).thenReturn(true); try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { for (String extension : List.of("txt", "pdf")) { proof.put("file", Map.of("fileId", "original", "name", "source." + extension, "laiyuan_muban_id", "template")); DataModel model = new DataModel(); model.setVisualId("model"); Map data = new HashMap<>(); data.put("xiuding_shenqing_zixiang_id", "source"); data.put("yuan_wenjian_banben_id", "version"); data.put("wenjian", List.of(Map.of("fileId", "original", "name", "tampered.docx", "uploadReceipt", "expired-upload", "revisionEditReceipt", "receipt"))); model.setDataNewMap(data); service.prepareSave(model); Map saved = (Map)((List)data.get("wenjian")).get(0); assertEquals("source." + extension, saved.get("name")); assertEquals("template", saved.get("laiyuan_muban_id")); assertFalse(saved.containsKey("uploadReceipt")); assertFalse(saved.containsKey("revisionEditReceipt")); assertThrows(DataException.class, () -> service.begin(command())); assertTrue(cache.isEmpty()); } } } @Test public void unchangedHistoricalFilesReachNativeSaveWithFullSourceAndWithoutClientTemplate() throws Exception { when(access.isDmsAuditTemporary(any())).thenReturn(true); try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { for (boolean asText : List.of(false, true)) for (String template : List.of("", "templateA")) { String sourceJson = "{\"fileId\":\"original\",\"name\":\"source.txt\",\"uploadReceipt\":\"historical-owner-token\",\"fileVersionId\":null}"; sourceJson = template.isEmpty() ? sourceJson : sourceJson.substring(0, sourceJson.length()-1) + ",\"laiyuan_muban_id\":\"" + template + "\"}"; final String expectedJson = sourceJson; proof.put("purpose", "dms-revision-edit"); proof.put("originFileId", "original"); proof.put("sourceFileJson", sourceJson); Map submitted = new LinkedHashMap<>(Map.of("fileId", "original", "name", "forged.txt", "laiyuan_muban_id", "forged-template", "revisionEditReceipt", "receipt")); DataModel model = new DataModel(); model.setVisualId("model"); Map child = new HashMap<>(); child.put("yuan_wenjian_banben_id", "version"); child.put("jilu_wenjian", asText ? JsonUtil.getObjectToString(List.of(submitted)) : List.of(submitted)); Map data = new HashMap<>(); data.put("xiuding_shenqing_zixiang_id", "source"); data.put("yuan_wenjian_banben_id", "version"); data.put("wenjian", List.of(submitted)); data.put("jilu_mingxi", List.of(child)); model.setDataNewMap(data); // Run the real native create entry before the existing persistence boundary. UploadEditDraftService drafts = mock(UploadEditDraftService.class); doAnswer(call -> { service.prepareSave(call.getArgument(0)); return null; }).when(drafts).prepareRevisionSave(model); jnpf.base.util.FlowFormDataUtil forms = spy(new jnpf.base.util.FlowFormDataUtil( null, null, null, null, null, null, drafts, mock(UploadFilePermissionService.class))); doAnswer(call -> { com.fasterxml.jackson.databind.ObjectMapper json = new com.fasterxml.jackson.databind.ObjectMapper(); assertEquals(json.readTree("[" + expectedJson + "]"), json.readTree(JsonUtil.getObjectToString(data.get("wenjian")))); Object saved = child.get("jilu_wenjian"); assertEquals(json.readTree("[" + expectedJson + "]"), json.readTree(saved instanceof String ? (String)saved : JsonUtil.getObjectToString(saved))); return new HashMap<>(); }).when(forms).createDataList(model); forms.create(model); Map saved = jnpf.base.util.UploadEditStateMachine.files(data.get("wenjian")).get(0); // Subsequent save uses persisted provenance, never treats historical receipt as a new upload. jnpf.base.util.UploadTemplateSource.normalize(List.of(new LinkedHashMap<>(saved)), List.of(saved), "tenant", "owner"); tickets.verify(() -> TicketUtil.parseTicket("historical-owner-token"), never()); } } } private static void assertThrows(Class type, Runnable action) { try { action.run(); } catch (Throwable error) { if (type.isInstance(error)) return; throw error; } fail("Expected " + type.getSimpleName()); } private final UploadEditAccess access = mock(UploadEditAccess.class); private final DmsRevisionEditApi api = mock(DmsRevisionEditApi.class); private final StringRedisTemplate redis = mock(StringRedisTemplate.class); private final ValueOperations values = mock(ValueOperations.class); private final Map cache = new HashMap<>(); private final DmsAuditTemporaryEditService service = new DmsAuditTemporaryEditService(access, api, redis); private final Map proof = new HashMap<>(); private final UserInfo owner = new UserInfo(); public DmsAuditTemporaryEditServiceTest() { owner.setUserId("owner"); owner.setTenantId("tenant"); proof.put("file", Map.of("fileId","original","name","source.docx")); proof.put("expiresAt",System.currentTimeMillis()+60000); proof.put("sourceItemId","source"); proof.put("versionId","version"); ActionResult> verified = mock(ActionResult.class); when(verified.getCode()).thenReturn(200); when(verified.getData()).thenReturn(proof); when(api.verify(any())).thenReturn(verified); when(redis.opsForValue()).thenReturn(values); when(values.get(anyString())).thenAnswer(call -> cache.get(call.getArgument(0))); doAnswer(call -> {cache.put(call.getArgument(0),call.getArgument(1));return null;}) .when(values).set(anyString(),anyString(),anyLong(),any()); when(redis.delete(anyString())).thenAnswer(call -> cache.remove(call.getArgument(0)) != null); } private MockedStatic user() { MockedStatic u = mockStatic(UserProvider.class); u.when(UserProvider::getUser).thenReturn(owner); u.when(UserProvider::getLoginUserId).thenReturn("owner"); return u; } private UploadEditCommand command() { UploadEditContext context = new UploadEditContext(); context.setModelId("model"); context.setField("wenjian"); UploadEditCommand c = new UploadEditCommand();c.setContext(context);c.setFileId("original"); c.setFiles(List.of(Map.of("fileId","original","revisionEditReceipt","receipt")));return c; } @Test public void beginRejectsSavedRecordsForeignFilesAndExpiredProof() { try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { UploadEditCommand c=command();c.getContext().setRecordId("saved");assertThrows(DataException.class,()->service.begin(c)); c.getContext().setRecordId(null);c.setFileId("foreign");assertThrows(DataException.class,()->service.begin(c)); c.setFileId("original");proof.put("expiresAt",1);assertThrows(DataException.class,()->service.begin(c)); assertTrue(cache.isEmpty()); } } @Test public void callbackCannotResurrectCleanedLeaseAndCannotWriteOutsideLease() { try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { String id=(String)service.begin(command()).get("leaseId"); assertTrue(service.find("tenant","owner",id).isRevisionTemporary()); assertThrows(DataException.class,()->service.find("tenant","other",id)); UploadEditSaveCommand c=new UploadEditSaveCommand();c.setTenantId("tenant");c.setOwnerId("owner");c.setLeaseId(id); c.setOperationId("operation");c.setFile(Map.of("fileId","foreign","name","source.docx")); assertThrows(DataException.class,()->service.save(c)); service.cleanup("tenant","owner",id);assertFalse(service.save(c)); } } @Test public void editedCopyKeepsAbsoluteExpiryAndCasFailureDoesNotAdopt() { try(MockedStatic u=user();MockedStatic tickets=mockStatic(TicketUtil.class)) { proof.put("file", Map.of("fileId", "original", "name", "source.docx", "contentSavedAt", 100L, "laiyuan_muban_id", "templateA")); String id=(String)service.begin(command()).get("leaseId"); UploadEditSaveCommand c=new UploadEditSaveCommand();c.setTenantId("tenant");c.setOwnerId("owner");c.setLeaseId(id); c.setOperationId("operation");c.setFile(Map.of("fileId","dir,"+id+",copy.docx","name","source.docx", "contentSavedAt", 200L)); tickets.when(()->TicketUtil.createTicket(anyString(),anyLong())).thenAnswer(call->{ Map p=JsonUtil.getJsonToBean(call.getArgument(0),Map.class); Map saved = (Map) p.get("file"); assertEquals(200L, ((Number) saved.get("contentSavedAt")).longValue()); assertEquals("templateA", saved.get("laiyuan_muban_id")); assertEquals(((Number)proof.get("expiresAt")).longValue(),((Number)p.get("expiresAt")).longValue());return "next"; }); when(redis.execute(any(org.springframework.data.redis.core.script.RedisScript.class),anyList(),any(),any())).thenReturn(0L); assertFalse(service.save(c));tickets.verify(()->TicketUtil.deleteTicket("next")); assertEquals("original",service.find("tenant","owner",id).getFile().get("fileId")); when(redis.execute(any(org.springframework.data.redis.core.script.RedisScript.class),anyList(),any(),any())) .thenAnswer(call -> { List keys=call.getArgument(1); String old=call.getArgument(2), updated=call.getArgument(3); if (!Objects.equals(cache.get(keys.get(0)),old)) return 0L; cache.put(keys.get(0),updated); return 1L; }); assertTrue(service.save(c)); assertEquals("dir,"+id+",copy.docx",service.result(id).get("source") instanceof Map ? ((Map)service.result(id).get("source")).get("fileId") : null); assertEquals(200L, ((Number) service.find("tenant", "owner", id).getFile().get("contentSavedAt")).longValue()); clearInvocations(redis); assertTrue(service.save(c)); verify(redis,never()).execute(any(org.springframework.data.redis.core.script.RedisScript.class),anyList(),any(),any()); } } @Test public void nativeCreateBindsSourceVersionAndFileThenStripsReceipt() { try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { proof.put("file", Map.of("fileId","original","name","source.docx","laiyuan_muban_id","templateA")); DataModel model=new DataModel();model.setVisualId("model"); Map data=new HashMap<>();data.put("xiuding_shenqing_zixiang_id","source");data.put("yuan_wenjian_banben_id","wrong"); data.put("wenjian",List.of(Map.of("fileId","original","revisionEditReceipt","receipt")));model.setDataNewMap(data); assertThrows(DataException.class,()->service.prepareSave(model)); data.put("yuan_wenjian_banben_id","version");data.put("xiuding_shenqing_zixiang_id","wrong"); assertThrows(DataException.class,()->service.prepareSave(model)); data.put("xiuding_shenqing_zixiang_id","source");service.prepareSave(model); Map saved=(Map)((List)data.get("wenjian")).get(0); assertFalse(saved.containsKey("revisionEditReceipt"));assertEquals("source.docx",saved.get("name")); assertEquals("templateA", saved.get("laiyuan_muban_id")); } } @Test public void newAuditUploadAndTemplateUseSameCopyAndNativeSaveLifecycle() { when(access.isDmsAuditTemporary(any())).thenReturn(true); try(MockedStatic u=user(); MockedStatic tickets=mockStatic(TicketUtil.class)) { Map grants=new HashMap<>(); tickets.when(()->TicketUtil.createTicket(anyString(),anyLong())).thenAnswer(call->{ grants.put("upload-grant",call.getArgument(0));return "upload-grant"; }); tickets.when(()->TicketUtil.parseTicket(anyString())).thenAnswer(call->grants.get(call.getArgument(0))); Map result=service.beginUpload(command(),Map.of("fileId","original","name","source.docx","laiyuan_muban_id","templateA")); assertEquals(true,result.get("revisionTemporary")); String id=(String)result.get("leaseId");assertTrue(service.find("tenant","owner",id).isRevisionTemporary()); DataModel model=new DataModel();model.setVisualId("model"); Map data=new HashMap<>();data.put("wenjian",result.get("files"));model.setDataNewMap(data); service.prepareSave(model); assertFalse(((Map)((List)data.get("wenjian")).get(0)).containsKey("revisionEditReceipt")); assertEquals("templateA", ((Map)((List)data.get("wenjian")).get(0)).get("laiyuan_muban_id")); verifyNoInteractions(api); } } }