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<UserProvider> u=user(); MockedStatic<TicketUtil> 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<String,Object> 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<UserProvider> u=user(); MockedStatic<TicketUtil> 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<String,Object> 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<String,Object> child = new HashMap<>(); child.put("yuan_wenjian_banben_id", "version");
|
child.put("jilu_wenjian", asText ? JsonUtil.getObjectToString(List.of(submitted)) : List.of(submitted));
|
Map<String,Object> 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<String,Object> 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<? extends Throwable> 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<String,String> values = mock(ValueOperations.class);
|
private final Map<String,String> cache = new HashMap<>();
|
private final DmsAuditTemporaryEditService service = new DmsAuditTemporaryEditService(access, api, redis);
|
private final Map<String,Object> 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<Map<String,Object>> 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<UserProvider> user() {
|
MockedStatic<UserProvider> 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<UserProvider> u=user(); MockedStatic<TicketUtil> 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<UserProvider> u=user(); MockedStatic<TicketUtil> 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<UserProvider> u=user();MockedStatic<TicketUtil> 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<String> 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<UserProvider> u=user(); MockedStatic<TicketUtil> 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<String,Object> 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<UserProvider> u=user(); MockedStatic<TicketUtil> tickets=mockStatic(TicketUtil.class)) {
|
Map<String,String> 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<String,Object> 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<String,Object> 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);
|
}
|
}
|
|
}
|