package jnpf.bizcommon.onlyoffice.service.impl;
|
|
import cn.hutool.jwt.JWTUtil;
|
import com.sun.net.httpserver.HttpServer;
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import jnpf.base.ActionResult;
|
import jnpf.base.UserInfo;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeConfigParam;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeSessionEntity;
|
import jnpf.bizcommon.onlyoffice.mapper.OnlyOfficeSessionMapper;
|
import jnpf.exception.DataException;
|
import jnpf.eln.TemplateEditabilityApi;
|
import jnpf.eln.RecordFillApi;
|
import jnpf.eln.RecordFillSaveOperation;
|
import jnpf.file.FileApi;
|
import jnpf.file.FileUploadApi;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.CacheKeyUtil;
|
import jnpf.util.RedisUtil;
|
import jnpf.util.UserProvider;
|
import jnpf.util.context.SpringContext;
|
import org.junit.After;
|
import org.junit.Before;
|
import org.junit.Test;
|
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.context.ApplicationContext;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
import java.net.InetSocketAddress;
|
import java.io.ByteArrayInputStream;
|
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.Charset;
|
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.Method;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Date;
|
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.Future;
|
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import feign.Request;
|
import feign.Response;
|
import org.dromara.x.file.storage.core.FileInfo;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.fail;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.isNull;
|
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
public class OnlyOfficeServiceImplTest {
|
|
private static final int SAVING_STATUS = 2;
|
|
private HttpServer server;
|
private OnlyOfficeSessionMapper sessionMapper;
|
private FileApi fileApi;
|
private FileUploadApi fileUploadApi;
|
private OnlyOfficePluginResolver pluginResolver;
|
private OnlyOfficeAnnotateTicketIssuer annotateTicketIssuer;
|
private ElnRecordFillTicketIssuer recordFillTicketIssuer;
|
private TemplateEditabilityApi templateEditabilityApi;
|
private RecordFillApi recordFillApi;
|
private DmsOnlyOfficePreviewTicketValidator dmsTicketValidator;
|
private AtomicInteger downloadRequests;
|
private OnlyOfficeServiceImpl service;
|
|
@Before
|
public void setUp() throws Exception {
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
|
OnlyOfficeSessionEntity.class);
|
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
when(applicationContext.getBean(RedisUtil.class)).thenReturn(mock(RedisUtil.class));
|
when(applicationContext.getBean(CacheKeyUtil.class)).thenReturn(mock(CacheKeyUtil.class));
|
SpringContext.setApplicationContext(applicationContext);
|
|
downloadRequests = new AtomicInteger();
|
server = HttpServer.create(new InetSocketAddress(0), 0);
|
server.createContext("/saved.docx", exchange -> {
|
downloadRequests.incrementAndGet();
|
byte[] body = "saved-document".getBytes();
|
exchange.sendResponseHeaders(200, body.length);
|
exchange.getResponseBody().write(body);
|
exchange.close();
|
});
|
server.start();
|
|
sessionMapper = mock(OnlyOfficeSessionMapper.class);
|
when(sessionMapper.update(isNull(), any())).thenReturn(1);
|
fileApi = mock(FileApi.class);
|
fileUploadApi = mock(FileUploadApi.class);
|
when(fileApi.getPath("annex")).thenReturn("WebAnnexFile");
|
OnlyOfficeProperties properties = new OnlyOfficeProperties();
|
properties.setJwtSecret("test-secret");
|
properties.setFileFetchBaseUrl("http://gateway");
|
pluginResolver = mock(OnlyOfficePluginResolver.class);
|
annotateTicketIssuer = mock(OnlyOfficeAnnotateTicketIssuer.class);
|
recordFillTicketIssuer = mock(ElnRecordFillTicketIssuer.class);
|
templateEditabilityApi = mock(TemplateEditabilityApi.class);
|
recordFillApi = mock(RecordFillApi.class);
|
ActionResult<Boolean> editable = probeResult(200, true);
|
when(templateEditabilityApi.editable(any(), any(), any())).thenReturn(editable);
|
when(templateEditabilityApi.canAnnotate(any(), any(), any(), any())).thenReturn(editable);
|
OnlyOfficeSceneConfigRegistry sceneRegistry = new OnlyOfficeSceneConfigRegistry(
|
java.util.Arrays.asList(
|
new ElnTemplateAnnotateConfigBuilder(templateEditabilityApi, annotateTicketIssuer),
|
new ElnRecordFillConfigBuilder(recordFillApi, recordFillTicketIssuer)),
|
new DefaultSceneConfigBuilder(new OnlyOfficeEditPermissionEvaluator(properties)));
|
dmsTicketValidator = mock(DmsOnlyOfficePreviewTicketValidator.class);
|
service = new OnlyOfficeServiceImpl(properties, sessionMapper, pluginResolver, sceneRegistry,
|
templateEditabilityApi, recordFillApi, dmsTicketValidator,
|
fileApi, fileUploadApi, new DmsQianpiyeDocxPropertiesWriter());
|
}
|
|
@After
|
public void tearDown() {
|
UserProvider.clearLocalUser();
|
server.stop(0);
|
}
|
|
@Test
|
public void saveBackReplacesOriginalFile() {
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(true);
|
|
invokeSaveBack(session, callback(), true);
|
|
verify(fileUploadApi).replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"));
|
verify(fileUploadApi, never()).uploadFile(any(), any(), any());
|
assertEquals(Integer.valueOf(2), session.getVersionNo());
|
verify(templateEditabilityApi).editable("tenant-a", "template-1", "template.docx");
|
}
|
|
@Test
|
@SuppressWarnings("unchecked")
|
public void qianpiyeSaveBackCreatesNewFileAndReportsUploadItem() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("dms");
|
session.setBizScene("dms.qianpiye");
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
FileInfo stored = mock(FileInfo.class);
|
when(stored.getFilename()).thenReturn("saved-copy.docx");
|
when(stored.getSize()).thenReturn(321L);
|
when(fileUploadApi.uploadFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(stored);
|
|
invokeSaveBack(session, callback(), true);
|
|
verify(fileUploadApi).uploadFile(any(), eq("WebAnnexFile/"), eq("template.docx"));
|
verify(fileUploadApi, never()).replaceFile(any(), any(), any());
|
Map<String, Object> file = JsonUtil.stringToMap(session.getQianpiyeSavedFileJson());
|
assertEquals("saved-copy.docx", file.get("fileId"));
|
assertEquals("template.docx", file.get("name"));
|
assertEquals(321L, ((Number) file.get("fileSize")).longValue());
|
assertEquals("docx", file.get("fileExtension"));
|
assertTrue(String.valueOf(file.get("url")).contains("saved-copy.docx"));
|
}
|
|
@Test
|
@SuppressWarnings("unchecked")
|
public void qianpiyeSaveBackReusesTheFirstCopyWithinOneEditingSession() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("dms");
|
session.setBizScene("dms.qianpiye");
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
Map<String, Object> firstCopy = new LinkedHashMap<>();
|
firstCopy.put("fileId", "saved-copy.docx");
|
firstCopy.put("name", "template.docx");
|
firstCopy.put("fileSize", 1L);
|
firstCopy.put("fileExtension", "docx");
|
firstCopy.put("url", "/api/file/saved-copy.docx");
|
session.setQianpiyeSavedFileJson(JsonUtil.getObjectToString(firstCopy));
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("saved-copy.docx"))).thenReturn(true);
|
|
invokeSaveBack(session, callback(), false);
|
|
verify(fileUploadApi).replaceFile(any(), eq("WebAnnexFile/"), eq("saved-copy.docx"));
|
verify(fileUploadApi, never()).uploadFile(any(), any(), any());
|
Map<String, Object> file = JsonUtil.stringToMap(session.getQianpiyeSavedFileJson());
|
assertEquals("saved-copy.docx", file.get("fileId"));
|
assertEquals("template.docx", file.get("name"));
|
assertEquals("saved-document".getBytes().length, ((Number) file.get("fileSize")).intValue());
|
}
|
|
@Test
|
public void qianpiyeNewEditingLeaseClearsThePreviousCopy() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizScene("dms.qianpiye");
|
session.setQianpiyeSavedFileJson("{\"fileId\":\"previous-copy.docx\"}");
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
|
invokePrivate("acquireLock", new Class<?>[]{OnlyOfficeSessionEntity.class, String.class},
|
session, "user-1");
|
|
assertNull(session.getQianpiyeSavedFileJson());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING), session.getSessionStatus());
|
}
|
|
@Test
|
public void saveStatusIsLoadedFromCurrentTenantSession() {
|
OnlyOfficeSessionEntity session = session();
|
Date savedAt = new Date(1_700_000_000_000L);
|
session.setLastSaveTime(savedAt);
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> status = service.getSaveStatus("template.docx", "annex");
|
|
assertEquals(savedAt.getTime(), status.get("lastSaveTime"));
|
assertEquals("EDITING", status.get("sessionStatus"));
|
verify(sessionMapper).selectOne(argThat(query -> {
|
AbstractWrapper<?, ?, ?> wrapper = (AbstractWrapper<?, ?, ?>) query;
|
wrapper.getSqlSegment();
|
return wrapper.getParamNameValuePairs().containsValue("tenant-a")
|
&& wrapper.getParamNameValuePairs().containsValue("template.docx")
|
&& wrapper.getParamNameValuePairs().containsValue("annex");
|
}));
|
}
|
|
@Test
|
public void uploadStatusReturnsOnlyProgressAfterLeaseOwnershipCheck() throws Exception {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("visualdev"); session.setBizScene("visualdev.uploadFile");
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
UploadEditSessionSupport support = mock(UploadEditSessionSupport.class);
|
java.lang.reflect.Field field = OnlyOfficeServiceImpl.class.getDeclaredField("uploadEditSessionSupport");
|
field.setAccessible(true); field.set(service, support);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> status = service.getSaveStatus("lease", "annex");
|
|
verify(support).requireStatusAccess("tenant-a", "user-1", "lease");
|
verify(support, never()).lease(any(), any(), any());
|
assertFalse(status.containsKey("file"));
|
assertTrue(status.containsKey("sessionStatus"));
|
assertTrue(status.containsKey("lastSaveTime"));
|
assertTrue(status.containsKey("version"));
|
}
|
|
@Test
|
@SuppressWarnings("unchecked")
|
public void qianpiyeSaveStatusReturnsNewUploadItem() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizScene("dms.qianpiye");
|
session.setQianpiyeSavedFileJson("{\"fileId\":\"saved-copy.docx\",\"name\":\"模板.docx\"}");
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> status = service.getSaveStatus("template.docx", "annex");
|
|
Map<String, Object> file = (Map<String, Object>) status.get("file");
|
assertEquals("saved-copy.docx", file.get("fileId"));
|
assertEquals("模板.docx", file.get("name"));
|
}
|
|
@Test
|
public void missingSaveStatusSessionIsIdle() {
|
when(sessionMapper.selectOne(any())).thenReturn(null);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> status = service.getSaveStatus("missing.docx", "annex");
|
|
assertEquals("IDLE", status.get("sessionStatus"));
|
}
|
|
@Test
|
public void statusSixCallbackMarksDocumentSavedAndKeepsEditingLease() {
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date());
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(true);
|
OnlyOfficeCallbackParam forceSave = callback();
|
forceSave.setKey("key-1");
|
forceSave.setStatus(OnlyOfficeCallbackParam.Status.FORCE_SAVING);
|
|
service.handleCallback(forceSave, callbackToken());
|
|
assertTrue(session.getLastSaveTime() != null);
|
assertEquals("user-1", session.getLockUserId());
|
assertEquals("key-1", session.getDocKey());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING),
|
session.getSessionStatus());
|
}
|
|
@Test
|
public void deniedTemplateCallbackDoesNotDownloadAndInvalidatesEditingSession() {
|
OnlyOfficeSessionEntity session = session();
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date());
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
ActionResult<Boolean> denied = probeResult(200, false);
|
when(templateEditabilityApi.editable(any(), any(), any())).thenReturn(denied);
|
|
try {
|
invokeSaveBack(session, callback(), false);
|
fail("published template callback must be rejected");
|
} catch (DataException e) {
|
assertEquals("ELN 模板已不可编辑,拒绝写回", e.getMessage());
|
}
|
|
assertEquals(0, downloadRequests.get());
|
verify(fileApi, never()).getPath(any());
|
verify(fileUploadApi, never()).replaceFile(any(), any(), any());
|
assertEquals(Integer.valueOf(2), session.getVersionNo());
|
assertFalse("key-1".equals(session.getDocKey()));
|
assertNull(session.getLockUserId());
|
assertNull(session.getLockHeartbeat());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE), session.getSessionStatus());
|
verify(sessionMapper, times(1)).update(isNull(), any());
|
}
|
|
@Test
|
public void ordinaryDocumentCallbackDoesNotProbeTemplateAndStillSaves() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("lims.document");
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(true);
|
|
invokeSaveBack(session, callback(), true);
|
|
verify(templateEditabilityApi, never()).editable(any(), any(), any());
|
verify(fileUploadApi).replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"));
|
}
|
|
@Test
|
public void failedReplacementDoesNotAdvanceSession() {
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(false);
|
|
try {
|
invokeSaveBack(session, callback(), true);
|
fail("expected replacement failure");
|
} catch (DataException e) {
|
assertEquals("文件服务写回文档失败", e.getMessage());
|
}
|
|
assertEquals(Integer.valueOf(1), session.getVersionNo());
|
assertNull(session.getLastSaveTime());
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void viewModeHidesCollaborationMenu() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("template.docx");
|
param.setFileName("template.docx");
|
param.setBizScene("ordinary.view");
|
UserInfo user = new UserInfo();
|
user.setUserId("user-1");
|
user.setUserName("Tester");
|
|
Map<String, Object> config = invokeAssembleConfig(param, session(), user);
|
Map<String, Object> document = castMap(config.get("document"));
|
Map<String, Object> permissions = castMap(document.get("permissions"));
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> customization = castMap(editorConfig.get("customization"));
|
|
assertEquals(false, permissions.get("comment"));
|
assertEquals(false, permissions.get("review"));
|
assertEquals(false, customization.get("comments"));
|
assertNull(castMap(customization.get("review")).get("reviewDisplay"));
|
}
|
|
@Test
|
public void editableConfigAllowsDocumentServerSaveWhenDownloadAndPrintAreDisabled() {
|
OnlyOfficeConfigParam param = annotationParam();
|
|
Map<String, Object> config = invokeAssembleConfig(
|
param, session(), user(), "edit", plugins(param, "eln-template-annotator"));
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
|
// Document Server 9.4 routes the editor Save command through /downloadas and rejects a session
|
// JWT only when both permissions are false.
|
assertEquals(true, permissions.get("edit"));
|
assertFalse(permissions.containsKey("fillForms"));
|
assertFalse(permissions.containsKey("modifyContentControl"));
|
assertEquals(true, permissions.get("download"));
|
assertEquals(false, permissions.get("print"));
|
}
|
|
@Test
|
public void reviewLeaseOverridesRequestedFullEditAndKeepsNativeSave() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("review_lease"); param.setFileName("source.docx"); param.setMode("edit");
|
param.setBizScene(UploadEditSceneConfigBuilder.SCENE); param.setBizModule(UploadEditSceneConfigBuilder.MODULE);
|
OnlyOfficeSceneBuildPlan plan = OnlyOfficeSceneBuildPlan.builder().editAdmission(() -> true)
|
.reviewOnly(true).uploadFileAllowDownload(false).build();
|
Map<String, Object> config = castMap(invokePrivate("assembleConfig", new Class<?>[] {
|
OnlyOfficeConfigParam.class, OnlyOfficeSessionEntity.class, String.class, String.class,
|
String.class, UserInfo.class, List.class, OnlyOfficeSceneBuildPlan.class, boolean.class
|
}, param, session(), "word", "docx", "edit", user(), Collections.emptyList(), plan, false));
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
Map<String, Object> editor = castMap(config.get("editorConfig"));
|
assertEquals("edit", editor.get("mode"));
|
assertEquals(false, permissions.get("edit")); assertEquals(true, permissions.get("review"));
|
assertEquals(true, permissions.get("comment")); assertEquals(Collections.emptyList(), permissions.get("reviewGroups"));
|
assertEquals(true, permissions.get("editCommentAuthorOnly"));
|
assertEquals(false, permissions.get("download"));
|
Map<String, Object> customization = castMap(editor.get("customization"));
|
assertEquals(true, customization.get("forcesave"));
|
assertEquals(true, castMap(customization.get("review")).get("trackChanges"));
|
}
|
|
@Test
|
public void commentPreviewShowsMarkupWithoutWritableSession() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("source"); param.setFileName("source.docx"); param.setMode("viewComments");
|
Map<String, Object> config = invokeAssembleConfig(param, session(), user(), "view", Collections.emptyList());
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
Map<String, Object> editor = castMap(config.get("editorConfig"));
|
assertEquals("view", editor.get("mode"));
|
assertEquals(false, permissions.get("edit")); assertEquals(false, permissions.get("review"));
|
assertEquals(true, permissions.get("comment")); assertFalse(editor.containsKey("callbackUrl"));
|
Map<String, Object> customization = castMap(editor.get("customization"));
|
assertEquals(true, customization.get("comments")); assertFalse(customization.containsKey("forcesave"));
|
assertEquals("markup", castMap(customization.get("review")).get("reviewDisplay"));
|
}
|
|
@Test
|
public void legacyDocIsEditableOnlyInUploadSceneAndStillHonorsAdmission() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam(); param.setMode("edit");
|
param.setBizScene(UploadEditSceneConfigBuilder.SCENE); param.setBizModule(UploadEditSceneConfigBuilder.MODULE);
|
OnlyOfficeSessionEntity session = session(); session.setLockUserId(null); session.setLockHeartbeat(null);
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
Class<?>[] signature = {OnlyOfficeConfigParam.class, String.class, OnlyOfficeSessionEntity.class,
|
UserInfo.class, OnlyOfficeSceneBuildPlan.class};
|
OnlyOfficeSceneBuildPlan allowed = OnlyOfficeSceneBuildPlan.builder().editAdmission(() -> true).build();
|
assertEquals("edit", invokePrivate("resolveMode", signature, param, "doc", session, user(), allowed));
|
assertEquals("view", invokePrivate("resolveMode", signature, param, "doc", session, user(),
|
OnlyOfficeSceneBuildPlan.builder().editAdmission(() -> false).build()));
|
param.setBizScene("other.scene");
|
assertEquals("view", invokePrivate("resolveMode", signature, param, "doc", session, user(), allowed));
|
}
|
|
@Test
|
public void uploadEditKeepsDownloadDisabledWithoutChangingNativeSave() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("lease-1"); param.setFileName("source.docx"); param.setFileType("annex");
|
param.setBizScene(UploadEditSceneConfigBuilder.SCENE); param.setBizModule(UploadEditSceneConfigBuilder.MODULE);
|
OnlyOfficeSceneBuildPlan plan = OnlyOfficeSceneBuildPlan.builder().editAdmission(() -> true)
|
.sourceFileId("source-file").uploadFileAllowDownload(false).build();
|
Map<String, Object> config = castMap(invokePrivate("assembleConfig", new Class<?>[] {
|
OnlyOfficeConfigParam.class, OnlyOfficeSessionEntity.class, String.class, String.class,
|
String.class, UserInfo.class, List.class, OnlyOfficeSceneBuildPlan.class, boolean.class
|
}, param, session(), "word", "docx", "edit", user(), Collections.emptyList(), plan, false));
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
assertEquals(true, permissions.get("edit"));
|
assertEquals(false, permissions.get("download"));
|
assertEquals(false, permissions.get("print"));
|
assertEquals(true, castMap(castMap(config.get("editorConfig")).get("customization")).get("forcesave"));
|
assertTrue(config.containsKey("token"));
|
}
|
|
@Test
|
public void workflowPreviewReusesOriginalImageSourceAfterAuthorization() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("config-ticket"); param.setFileName("source.docx"); param.setFileType("annex");
|
param.setBizScene(WorkflowFilePreviewConfigBuilder.SCENE); param.setBizModule(WorkflowFilePreviewConfigBuilder.MODULE);
|
OnlyOfficeSceneBuildPlan plan = OnlyOfficeSceneBuildPlan.builder().editAdmission(() -> false)
|
.sourceFileId("source-file").uploadFileAllowDownload(false).build();
|
Map<String, Object> config = castMap(invokePrivate("assembleConfig", new Class<?>[] {
|
OnlyOfficeConfigParam.class, OnlyOfficeSessionEntity.class, String.class, String.class,
|
String.class, UserInfo.class, List.class, OnlyOfficeSceneBuildPlan.class, boolean.class
|
}, param, session(), "word", "docx", "view", user(), Collections.emptyList(), plan, false));
|
Map<String, Object> document = castMap(config.get("document"));
|
assertTrue(String.valueOf(document.get("url")).contains("/api/file/Image/annex/source-file?s="));
|
assertFalse(String.valueOf(document.get("url")).contains("config-ticket"));
|
assertEquals(false, castMap(document.get("permissions")).get("edit"));
|
assertEquals(false, castMap(document.get("permissions")).get("download"));
|
assertTrue(config.containsKey("token"));
|
}
|
|
@Test
|
public void qianpiyeConfigUsesEditModeAndCarriesTheBusinessConfig() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("qianpiye.docx");
|
param.setFileName("qianpiye.docx");
|
param.setBizScene("dms.qianpiye");
|
param.setMode("qianpiye");
|
|
Map<String, Object> config = invokeAssembleConfig(param, session(), user(), "edit", Collections.emptyList());
|
Map<String, Object> qianpiye = castMap(config.get("dms.qianpiye"));
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
Map<String, Object> customization = castMap(castMap(config.get("editorConfig")).get("customization"));
|
|
assertEquals("edit", castMap(config.get("editorConfig")).get("mode"));
|
assertEquals(true, qianpiye.get("enabled"));
|
assertEquals("qianpiye", qianpiye.get("mode"));
|
assertEquals(false, permissions.get("download"));
|
assertEquals(false, permissions.get("print"));
|
assertEquals(true, customization.get("plugins"));
|
assertEquals(false, customization.get("autosave"));
|
assertEquals(false, customization.get("forcesave"));
|
}
|
|
@Test
|
public void recordFillKeepsEditorCommandsEnabledForTheControlledFiller() {
|
OnlyOfficeConfigParam param = recordParam();
|
|
Map<String, Object> config = invokeAssembleConfig(
|
param, recordSession(), user(), "edit", plugins(param, "eln-record-filler"));
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> context = pluginContext(config, fillGuid());
|
|
assertEquals("edit", editorConfig.get("mode"));
|
assertEquals(true, permissions.get("edit"));
|
assertFalse(permissions.containsKey("fillForms"));
|
assertFalse(permissions.containsKey("modifyContentControl"));
|
assertEquals(false, permissions.get("comment"));
|
assertEquals(false, permissions.get("review"));
|
assertEquals(true, context.get("canFill"));
|
}
|
|
@Test
|
public void recordFillReopenByLockOwnerTakesOverWithANewEditableLease() {
|
OnlyOfficeConfigParam param = recordParam();
|
OnlyOfficeSessionEntity session = recordSession();
|
String previousDocKey = session.getDocKey();
|
ActionResult<Boolean> allowed = probeResult(200, true);
|
when(recordFillApi.canFill(any(), any(), any(), any())).thenReturn(allowed);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-record-filler"));
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> context = pluginContext(config, fillGuid());
|
|
assertEquals("edit", editorConfig.get("mode"));
|
assertEquals(true, context.get("canFill"));
|
assertEquals("user-1", session.getLockUserId());
|
assertEquals(Integer.valueOf(42), session.getVersion());
|
assertNotEquals(previousDocKey, session.getDocKey());
|
|
when(sessionMapper.selectOne(any())).thenReturn(null);
|
OnlyOfficeCallbackParam replacedLeaseCallback = callback();
|
replacedLeaseCallback.setKey(previousDocKey);
|
try {
|
service.handleCallback(replacedLeaseCallback, callbackToken());
|
fail("callback from the replaced lease must be fenced");
|
} catch (DataException expected) {
|
assertTrue(expected.getMessage().contains(previousDocKey));
|
}
|
verify(fileUploadApi, never()).replaceFile(any(), any(), any());
|
}
|
|
@Test
|
public void recordFillOpenByAnotherUserKeepsTheExistingLeaseReadOnly() {
|
OnlyOfficeConfigParam param = recordParam();
|
OnlyOfficeSessionEntity session = recordSession();
|
String existingDocKey = session.getDocKey();
|
ActionResult<Boolean> allowed = probeResult(200, true);
|
when(recordFillApi.canFill(any(), any(), any(), any())).thenReturn(allowed);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-record-filler"));
|
UserProvider.setLocalLoginUser(user("user-2"));
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> context = pluginContext(config, fillGuid());
|
|
assertEquals("view", editorConfig.get("mode"));
|
assertEquals(false, context.get("canFill"));
|
assertEquals("user-1", session.getLockUserId());
|
assertEquals(Integer.valueOf(41), session.getVersion());
|
assertEquals(existingDocKey, session.getDocKey());
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void annotateTargetReceivesTicketAndRepairHint() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setRepairBindingId("binding-1");
|
when(annotateTicketIssuer.issue(any(), any(), eq(annotationGuid()))).thenReturn("annotation-ticket");
|
|
Map<String, Object> context = pluginContext(invokeAssembleConfig(
|
param, session(), user(), "edit",
|
plugins(param, "eln-template-annotator")), annotationGuid());
|
|
assertEquals("onlyoffice:eln-template-annotate", context.get("scope"));
|
assertEquals("annotation-ticket", context.get("ticket"));
|
assertEquals(true, context.get("canAnnotate"));
|
assertEquals("binding-1", context.get("repairBindingId"));
|
}
|
|
@Test
|
public void annotationSceneDoesNotEnterReadOnlyFinalRevisionView() {
|
OnlyOfficeConfigParam param = annotationParam();
|
|
Map<String, Object> config = invokeAssembleConfig(
|
param, session(), user(), "edit",
|
plugins(param, "eln-template-annotator"));
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> customization = castMap(editorConfig.get("customization"));
|
|
assertNull(castMap(customization.get("review")).get("reviewDisplay"));
|
}
|
|
@Test
|
public void ordinaryReadOnlyAnnotationDoesNotEnableAnnotator() {
|
OnlyOfficeConfigParam param = annotationParam();
|
|
Map<String, Object> config = invokeAssembleConfig(
|
param, session(), user(), "view",
|
plugins(param, "eln-template-annotator"));
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
|
assertFalse(editorConfig.containsKey("plugins"));
|
verify(annotateTicketIssuer, never()).issue(any(), any(), any());
|
}
|
|
@Test
|
public void failedAnnotationTicketDoesNotAcquireEditLock() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setFileType("annex");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
when(sessionMapper.update(isNull(), any())).thenReturn(1);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenThrow(new IllegalArgumentException("invalid"));
|
UserProvider.setLocalLoginUser(user());
|
|
try {
|
service.buildEditorConfig(param);
|
fail("expected annotation ticket failure");
|
} catch (IllegalArgumentException e) {
|
assertEquals("invalid", e.getMessage());
|
}
|
|
assertNull(session.getLockUserId());
|
assertNull(session.getLockHeartbeat());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE), session.getSessionStatus());
|
}
|
|
@Test
|
public void failedConfigCompensationDoesNotReleaseNewerLockFromExpiredOtherUser() throws Exception {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity firstSnapshot = session();
|
firstSnapshot.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
firstSnapshot.setVersionNo(17);
|
firstSnapshot.setVersion(40);
|
OnlyOfficeSessionEntity secondSnapshot = session();
|
secondSnapshot.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
secondSnapshot.setLockUserId("user-2");
|
secondSnapshot.setLockHeartbeat(new Date(0));
|
secondSnapshot.setVersionNo(17);
|
secondSnapshot.setVersion(41);
|
AtomicReference<String> persistedLockUser = new AtomicReference<>();
|
AtomicInteger persistedLockVersion = new AtomicInteger(40);
|
CountDownLatch firstTicketStarted = new CountDownLatch(1);
|
CountDownLatch allowFirstTicketFailure = new CountDownLatch(1);
|
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(sessionMapper.selectOne(any())).thenAnswer(invocation ->
|
Thread.currentThread().getName().equals("config-a") ? firstSnapshot : secondSnapshot);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenAnswer(invocation -> {
|
OnlyOfficeSessionEntity update = invocation.getArgument(0);
|
Wrapper<?> wrapper = invocation.getArgument(1);
|
assertTrue(wrapper.getSqlSegment().contains("f_version"));
|
int acquiredVersion = update.getVersion();
|
if (!persistedLockVersion.compareAndSet(acquiredVersion - 1, acquiredVersion)) {
|
return 0;
|
}
|
persistedLockUser.set(update.getLockUserId());
|
return 1;
|
});
|
when(sessionMapper.update(isNull(), any())).thenAnswer(invocation -> {
|
UpdateWrapper<OnlyOfficeSessionEntity> wrapper = invocation.getArgument(1);
|
String sqlSet = wrapper.getSqlSet();
|
if (sqlSet.contains("file_name") && sqlSet.contains("document_type")) {
|
return 1;
|
}
|
|
String sql = wrapper.getSqlSegment();
|
assertTrue(sql.contains("lock_user_id"));
|
assertTrue(sql.contains("f_version"));
|
assertTrue(sql.contains("session_status"));
|
assertTrue(((AbstractWrapper<?, ?, ?>) wrapper).getParamNameValuePairs()
|
.containsValue(OnlyOfficeSessionEntity.SessionStatus.EDITING));
|
if (((AbstractWrapper<?, ?, ?>) wrapper).getParamNameValuePairs()
|
.containsValue(persistedLockVersion.get())) {
|
persistedLockUser.set(null);
|
return 1;
|
}
|
return 0;
|
});
|
when(sessionMapper.updateById(any(OnlyOfficeSessionEntity.class))).thenAnswer(invocation -> {
|
OnlyOfficeSessionEntity update = invocation.getArgument(0);
|
persistedLockUser.set(update.getLockUserId());
|
return 1;
|
});
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenAnswer(invocation -> {
|
if (Thread.currentThread().getName().equals("config-a")) {
|
firstTicketStarted.countDown();
|
assertTrue(allowFirstTicketFailure.await(5, TimeUnit.SECONDS));
|
throw new IllegalArgumentException("config-a failed");
|
}
|
return "annotation-ticket-b";
|
});
|
|
ExecutorService firstExecutor = Executors.newSingleThreadExecutor(
|
task -> new Thread(task, "config-a"));
|
ExecutorService secondExecutor = Executors.newSingleThreadExecutor(
|
task -> new Thread(task, "config-b"));
|
try {
|
Future<Map<String, Object>> first = firstExecutor.submit(() -> {
|
UserProvider.setLocalLoginUser(user());
|
try {
|
return service.buildEditorConfig(param);
|
} finally {
|
UserProvider.clearLocalUser();
|
}
|
});
|
assertTrue(firstTicketStarted.await(5, TimeUnit.SECONDS));
|
|
Future<Map<String, Object>> second = secondExecutor.submit(() -> {
|
UserProvider.setLocalLoginUser(user("user-2"));
|
try {
|
return service.buildEditorConfig(param);
|
} finally {
|
UserProvider.clearLocalUser();
|
}
|
});
|
assertEquals("edit", castMap(second.get(5, TimeUnit.SECONDS)
|
.get("editorConfig")).get("mode"));
|
allowFirstTicketFailure.countDown();
|
try {
|
first.get(5, TimeUnit.SECONDS);
|
fail("expected first config assembly failure");
|
} catch (ExecutionException e) {
|
assertEquals("config-a failed", e.getCause().getMessage());
|
}
|
|
assertEquals("user-2", persistedLockUser.get());
|
assertEquals("user-1", firstSnapshot.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING),
|
firstSnapshot.getSessionStatus());
|
} finally {
|
allowFirstTicketFailure.countDown();
|
firstExecutor.shutdownNow();
|
secondExecutor.shutdownNow();
|
}
|
}
|
|
@Test
|
public void expiredSameUserReacquireGetsNewLeaseKeyAndOldCallbackCannotReleaseIt() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity current = session();
|
current.setDocKey("lease-a");
|
current.setVersion(40);
|
current.setLockUserId("user-1");
|
current.setLockHeartbeat(new Date(0));
|
current.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
when(sessionMapper.selectOne(any())).thenAnswer(invocation -> {
|
Wrapper<?> query = invocation.getArgument(0);
|
return query.getSqlSegment().contains("doc_key") && !"lease-a".equals(current.getDocKey())
|
? null : current;
|
});
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
String currentLeaseKey = (String) castMap(config.get("document")).get("key");
|
|
assertNotEquals("lease-a", currentLeaseKey);
|
assertEquals(currentLeaseKey, current.getDocKey());
|
OnlyOfficeCallbackParam oldClose = callback();
|
oldClose.setKey("lease-a");
|
oldClose.setStatus(OnlyOfficeCallbackParam.Status.CLOSED_NO_CHANGES);
|
try {
|
service.handleCallback(oldClose, callbackToken());
|
fail("old lease callback must not find the current session");
|
} catch (DataException e) {
|
assertTrue(e.getMessage().contains("找不到 doc_key"));
|
}
|
assertEquals("user-1", current.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING), current.getSessionStatus());
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void staleHeartbeatAndReleaseSnapshotsCannotOverwriteNewerLease() {
|
OnlyOfficeSessionEntity stale = session();
|
stale.setDocKey("lease-a");
|
stale.setVersion(41);
|
stale.setLockUserId("user-1");
|
stale.setLockHeartbeat(new Date());
|
stale.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
OnlyOfficeSessionEntity persistedNewerLease = session();
|
persistedNewerLease.setDocKey("lease-b");
|
persistedNewerLease.setVersion(42);
|
persistedNewerLease.setLockUserId("user-2");
|
persistedNewerLease.setLockHeartbeat(new Date());
|
persistedNewerLease.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
AtomicInteger casAttempts = new AtomicInteger();
|
when(sessionMapper.update(isNull(), any())).thenAnswer(invocation -> {
|
UpdateWrapper<OnlyOfficeSessionEntity> update = invocation.getArgument(1);
|
AbstractWrapper<?, ?, ?> wrapper = (AbstractWrapper<?, ?, ?>) update;
|
String sql = wrapper.getSqlSegment();
|
assertTrue(sql.contains("f_id") && sql.contains("doc_key") && sql.contains("version_no")
|
&& sql.contains("f_version") && sql.contains("session_status"));
|
casAttempts.incrementAndGet();
|
return 0;
|
});
|
|
invokePrivate("refreshLockHeartbeat", new Class<?>[]{OnlyOfficeSessionEntity.class}, stale);
|
invokePrivate("releaseLock", new Class<?>[]{OnlyOfficeSessionEntity.class, int.class}, stale,
|
OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
|
assertEquals(2, casAttempts.get());
|
assertEquals("lease-b", persistedNewerLease.getDocKey());
|
assertEquals(Integer.valueOf(42), persistedNewerLease.getVersion());
|
assertEquals("user-2", persistedNewerLease.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING),
|
persistedNewerLease.getSessionStatus());
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void saveDeclaresSavingBeforeExternalWriteAndBlocksCompetingAcquire() {
|
OnlyOfficeSessionEntity savingSession = session();
|
savingSession.setDocKey("lease-a");
|
savingSession.setVersion(41);
|
savingSession.setLockUserId("user-1");
|
savingSession.setLockHeartbeat(new Date());
|
savingSession.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
AtomicBoolean savingDeclared = new AtomicBoolean();
|
when(sessionMapper.update(org.mockito.ArgumentMatchers.nullable(OnlyOfficeSessionEntity.class), any()))
|
.thenAnswer(invocation -> {
|
OnlyOfficeSessionEntity updateEntity = invocation.getArgument(0);
|
UpdateWrapper<OnlyOfficeSessionEntity> update = invocation.getArgument(1);
|
if (updateEntity == null && update != null && update.getSqlSet().contains("session_status")) {
|
savingDeclared.set(true);
|
return 1;
|
}
|
assertTrue("save callback must claim SAVING before external write", savingDeclared.get());
|
AbstractWrapper<?, ?, ?> acquire = (AbstractWrapper<?, ?, ?>) update;
|
assertEquals(3, countOccurrences(acquire.getSqlSegment(), "session_status"));
|
assertTrue(acquire.getParamNameValuePairs()
|
.containsValue(OnlyOfficeSessionEntity.SessionStatus.IDLE));
|
assertTrue(acquire.getParamNameValuePairs()
|
.containsValue(OnlyOfficeSessionEntity.SessionStatus.EDITING));
|
assertFalse(acquire.getParamNameValuePairs().containsValue(SAVING_STATUS));
|
assertFalse(acquire.getParamNameValuePairs()
|
.containsValue(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING));
|
assertFalse(acquire.getParamNameValuePairs()
|
.containsValue(OnlyOfficeSessionEntity.SessionStatus.SAVE_FAILED));
|
return 0;
|
});
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenAnswer(invocation -> {
|
OnlyOfficeSessionEntity competing = session();
|
competing.setDocKey("lease-a");
|
competing.setVersion(41);
|
competing.setLockUserId("user-1");
|
competing.setLockHeartbeat(new Date());
|
competing.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
try {
|
invokePrivate("acquireLock", new Class<?>[]{OnlyOfficeSessionEntity.class, String.class},
|
competing, "user-2");
|
fail("competing acquire must fail while the save owns SAVING");
|
} catch (DataException expected) {
|
assertEquals("文档编辑锁获取失败,请重新打开", expected.getMessage());
|
}
|
return true;
|
});
|
|
try {
|
invokeSaveBack(savingSession, callback(), true);
|
} catch (DataException e) {
|
assertTrue(e.getMessage().contains("save callback must claim SAVING"));
|
fail("save callback must declare SAVING before external write");
|
}
|
|
assertTrue(savingDeclared.get());
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void claimSaveRefreshesHeartbeatAndMarksTheLeaseSaving() {
|
OnlyOfficeSessionEntity session = session();
|
session.setDocKey("lease-a");
|
session.setVersion(41);
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date(1));
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
AtomicReference<UpdateWrapper<OnlyOfficeSessionEntity>> claim = new AtomicReference<>();
|
when(sessionMapper.update(isNull(), any())).thenAnswer(invocation -> {
|
claim.set(invocation.getArgument(1));
|
return 1;
|
});
|
|
assertEquals(true, invokePrivate("claimSave", new Class<?>[]{OnlyOfficeSessionEntity.class}, session));
|
|
assertTrue(claim.get().getSqlSet().contains("session_status"));
|
assertTrue(claim.get().getSqlSet().contains("lock_heartbeat"));
|
assertTrue(session.getLockHeartbeat().after(new Date(1)));
|
assertEquals(Integer.valueOf(SAVING_STATUS), session.getSessionStatus());
|
}
|
|
@Test
|
public void freshSavingLeaseRejectsSameUserAcquire() {
|
OnlyOfficeSessionEntity freshSaving = session();
|
freshSaving.setDocKey("lease-fresh");
|
freshSaving.setVersion(41);
|
freshSaving.setLockUserId("user-1");
|
freshSaving.setLockHeartbeat(new Date());
|
freshSaving.setSessionStatus(SAVING_STATUS);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(0);
|
|
try {
|
invokePrivate("acquireLock", new Class<?>[]{OnlyOfficeSessionEntity.class, String.class},
|
freshSaving, "user-1");
|
fail("fresh SAVING lease must reject same-user acquire");
|
} catch (DataException e) {
|
assertEquals("文档编辑锁获取失败,请重新打开", e.getMessage());
|
}
|
}
|
|
@Test
|
public void expiredSavingLeaseCannotBeTakenOver() {
|
OnlyOfficeSessionEntity expiredSaving = session();
|
expiredSaving.setDocKey("lease-expired");
|
expiredSaving.setVersion(41);
|
expiredSaving.setLockUserId("user-1");
|
expiredSaving.setLockHeartbeat(new Date(0));
|
expiredSaving.setSessionStatus(SAVING_STATUS);
|
|
try {
|
invokePrivate("acquireLock", new Class<?>[]{OnlyOfficeSessionEntity.class, String.class},
|
expiredSaving, "user-2");
|
fail("expired SAVING lease must not be taken over automatically");
|
} catch (DataException e) {
|
assertEquals("文档编辑锁获取失败,请重新打开", e.getMessage());
|
}
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void completedFileWriteWithFailedSessionCasRaisesRecoveryError() {
|
OnlyOfficeSessionEntity session = session();
|
session.setDocKey("lease-a");
|
session.setVersion(41);
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date());
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
AtomicInteger sessionWrites = new AtomicInteger();
|
when(sessionMapper.update(isNull(), any())).thenAnswer(invocation ->
|
sessionWrites.incrementAndGet() == 1 ? 1 : 0);
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(true);
|
|
try {
|
invokeSaveBack(session, callback(), true);
|
fail("completed file write with failed session CAS must not be acknowledged as success");
|
} catch (DataException e) {
|
assertEquals("文件已写回,但 OnlyOffice 会话状态更新失败,请人工确认后重试", e.getMessage());
|
}
|
|
assertEquals(2, sessionWrites.get());
|
assertEquals(Integer.valueOf(SAVING_STATUS), session.getSessionStatus());
|
assertEquals("lease-a", session.getDocKey());
|
}
|
|
@Test
|
public void forceSaveErrorKeepsTheEditingLeaseActive() {
|
OnlyOfficeSessionEntity session = session();
|
session.setDocKey("lease-a");
|
session.setVersion(41);
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date());
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
OnlyOfficeCallbackParam forceSaveError = callback();
|
forceSaveError.setKey("lease-a");
|
forceSaveError.setStatus(OnlyOfficeCallbackParam.Status.FORCE_SAVE_ERROR);
|
|
service.handleCallback(forceSaveError, callbackToken());
|
|
assertEquals("user-1", session.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING),
|
session.getSessionStatus());
|
verify(sessionMapper, never()).update(isNull(), any());
|
}
|
|
@Test
|
public void sameUserFreshLeaseReturnsViewWhileExistingTabCanStillSave() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity existingLease = session();
|
existingLease.setDocKey("lease-a");
|
existingLease.setVersion(41);
|
existingLease.setLockUserId("user-1");
|
existingLease.setLockHeartbeat(new Date());
|
existingLease.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
when(sessionMapper.selectOne(any())).thenReturn(existingLease);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"))).thenReturn(true);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> secondTab = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(secondTab.get("editorConfig")).get("mode"));
|
assertEquals("lease-a", castMap(secondTab.get("document")).get("key"));
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
invokeSaveBack(existingLease, callback(), true);
|
assertEquals(Integer.valueOf(2), existingLease.getVersionNo());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE),
|
existingLease.getSessionStatus());
|
}
|
|
@Test
|
public void sameLeaseSavingCallbackRaisesErrorInsteadOfBeingSilentlyIgnored() {
|
OnlyOfficeSessionEntity savingLease = session();
|
savingLease.setDocKey("lease-a");
|
savingLease.setVersion(41);
|
savingLease.setLockUserId("user-1");
|
savingLease.setLockHeartbeat(new Date());
|
savingLease.setSessionStatus(SAVING_STATUS);
|
when(sessionMapper.selectOne(any())).thenReturn(savingLease, savingLease);
|
when(sessionMapper.update(isNull(), any())).thenReturn(0);
|
OnlyOfficeCallbackParam forceSave = callback();
|
forceSave.setKey("lease-a");
|
forceSave.setStatus(OnlyOfficeCallbackParam.Status.FORCE_SAVING);
|
|
try {
|
service.handleCallback(forceSave, callbackToken());
|
fail("same lease SAVING callback must not be silently acknowledged as stale");
|
} catch (DataException e) {
|
assertEquals("OnlyOffice 同一租约保存回调正在处理", e.getMessage());
|
}
|
|
verify(fileUploadApi, never()).replaceFile(any(), any(), any());
|
}
|
|
@Test
|
public void templateProbeFalseOrFailureReturnsViewWithoutAcquiringLock() {
|
assertProbeDeniedReturnsView(probeResult(200, false), null);
|
assertProbeDeniedReturnsView(probeResult(500, true), null);
|
assertProbeDeniedReturnsView(null, null);
|
assertProbeDeniedReturnsView(null, new IllegalStateException("eln unavailable"));
|
}
|
|
@Test
|
public void successfulTemplateProbeReturnsEditAndAcquiresLock() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("edit", castMap(config.get("editorConfig")).get("mode"));
|
assertEquals("user-1", session.getLockUserId());
|
verify(templateEditabilityApi).canAnnotate(
|
"tenant-a", "user-1", "template-1", "template.docx");
|
verify(templateEditabilityApi, never()).editable(any(), any(), any());
|
}
|
|
@Test
|
public void existingElnSessionIdentityCannotBeOverwrittenByAnotherBusinessContext() {
|
OnlyOfficeConfigParam param = param("ordinary.edit", "document-helper");
|
param.setBizModule("lims.document");
|
param.setBizDataId("other-record");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "document-helper"));
|
UserProvider.setLocalLoginUser(user());
|
|
try {
|
service.buildEditorConfig(param);
|
fail("existing ELN session identity must be immutable");
|
} catch (DataException e) {
|
assertEquals("OnlyOffice 会话业务上下文不匹配", e.getMessage());
|
}
|
|
assertEquals("tenant-a", session.getTenantId());
|
assertEquals("eln.template", session.getBizModule());
|
assertEquals("template-1", session.getBizDataId());
|
assertNull(session.getLockUserId());
|
verify(sessionMapper, never()).update(any(), any());
|
}
|
|
@Test
|
public void elnSessionGuardRejectsForgedFileTypeBeforeGenericLookupOrLock() {
|
OnlyOfficeConfigParam param = param("ordinary.edit", "document-helper");
|
param.setBizModule("lims.document");
|
param.setBizDataId("other-record");
|
param.setFileType("web");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenAnswer(invocation -> {
|
Wrapper<?> query = invocation.getArgument(0);
|
return query.getSqlSegment().contains("biz_module") ? session : null;
|
});
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "document-helper"));
|
UserProvider.setLocalLoginUser(user());
|
|
try {
|
service.buildEditorConfig(param);
|
fail("forged file type must be rejected by ELN session guard");
|
} catch (DataException e) {
|
assertEquals("OnlyOffice 会话业务上下文不匹配", e.getMessage());
|
}
|
|
verify(sessionMapper).selectOne(argThat(query -> {
|
String sql = query.getSqlSegment();
|
return sql.contains("file_id") && sql.contains("biz_module")
|
&& !sql.contains("file_type");
|
}));
|
verify(sessionMapper, never()).insert(any(OnlyOfficeSessionEntity.class));
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
verify(sessionMapper, never()).update(isNull(), any());
|
assertNull(session.getLockUserId());
|
}
|
|
@Test
|
public void sessionLookupsAreTenantScopedAcrossElnGuardAndGenericFileLookup() {
|
OnlyOfficeConfigParam param = annotationParam();
|
OnlyOfficeSessionEntity otherTenant = session();
|
otherTenant.setTenantId("tenant-b");
|
AtomicInteger tenantScopedQueries = new AtomicInteger();
|
when(sessionMapper.selectOne(any())).thenAnswer(invocation -> {
|
AbstractWrapper<?, ?, ?> query = invocation.getArgument(0);
|
query.getSqlSegment();
|
if (!query.getParamNameValuePairs().containsValue("tenant-a")) {
|
return otherTenant;
|
}
|
tenantScopedQueries.incrementAndGet();
|
return null;
|
});
|
|
OnlyOfficeSessionEntity created = (OnlyOfficeSessionEntity) invokePrivate(
|
"loadOrCreateSession",
|
new Class<?>[]{OnlyOfficeConfigParam.class, String.class, String.class},
|
param, "word", "tenant-a");
|
|
assertEquals("tenant-a", created.getTenantId());
|
assertEquals(3, tenantScopedQueries.get());
|
verify(sessionMapper).selectOne(argThat(query -> {
|
String sql = query.getSqlSegment();
|
return sql.contains("file_id") && sql.contains("biz_module")
|
&& sql.contains("f_tenant_id") && !sql.contains("file_type")
|
&& ((AbstractWrapper<?, ?, ?>) query).getParamNameValuePairs()
|
.containsValue("tenant-a")
|
&& ((AbstractWrapper<?, ?, ?>) query).getParamNameValuePairs()
|
.containsValue("eln.template");
|
}));
|
verify(sessionMapper).selectOne(argThat(query -> {
|
String sql = query.getSqlSegment();
|
return sql.contains("file_id") && sql.contains("biz_module")
|
&& sql.contains("f_tenant_id") && !sql.contains("file_type")
|
&& ((AbstractWrapper<?, ?, ?>) query).getParamNameValuePairs()
|
.containsValue("tenant-a")
|
&& ((AbstractWrapper<?, ?, ?>) query).getParamNameValuePairs()
|
.containsValue("eln.record");
|
}));
|
verify(sessionMapper).selectOne(argThat(query -> {
|
String sql = query.getSqlSegment();
|
return sql.contains("file_id") && sql.contains("file_type")
|
&& sql.contains("f_tenant_id")
|
&& ((AbstractWrapper<?, ?, ?>) query).getParamNameValuePairs()
|
.containsValue("tenant-a");
|
}));
|
}
|
|
@Test
|
public void deniedRecordProbeReturnsViewWithoutLockOrFillTicket() {
|
OnlyOfficeConfigParam param = recordParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = recordSession();
|
session.setLockUserId(null);
|
session.setLockHeartbeat(null);
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-record-filler"));
|
ActionResult<Boolean> denied = probeResult(200, false);
|
when(recordFillApi.canFill(any(), any(), any(), any())).thenReturn(denied);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
assertNull(session.getLockUserId());
|
verify(recordFillApi).canFill("tenant-a", "user-1", "record-1", "template.docx");
|
verify(recordFillTicketIssuer).issue(any(), any(), any());
|
}
|
|
@Test
|
public void otherSceneUsesFallbackPermissionWithoutTemplateProbe() {
|
OnlyOfficeConfigParam param = param("other.scene", "document-helper");
|
param.setFileType("annex");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "document-helper"));
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("edit", castMap(config.get("editorConfig")).get("mode"));
|
assertEquals("user-1", session.getLockUserId());
|
verify(templateEditabilityApi, never()).canAnnotate(any(), any(), any(), any());
|
verify(annotateTicketIssuer, never()).issue(any(), any(), any());
|
}
|
|
@Test
|
public void editableRecordProbeAllowsFillOnlyWhenIdentityMatches() {
|
OnlyOfficeConfigParam param = recordParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = recordSession();
|
session.setLockUserId(null);
|
session.setLockHeartbeat(null);
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-record-filler"));
|
ActionResult<Boolean> allowed = probeResult(200, true);
|
when(recordFillApi.canFill(any(), any(), any(), any())).thenReturn(allowed);
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("edit", castMap(config.get("editorConfig")).get("mode"));
|
assertEquals("user-1", session.getLockUserId());
|
verify(recordFillApi).canFill("tenant-a", "user-1", "record-1", "template.docx");
|
verify(recordFillTicketIssuer).issue(any(), any(), any());
|
}
|
|
@Test
|
public void legacyElnSessionWithoutTenantIsRejectedBeforeSessionUpdate() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setTenantId(null);
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
UserProvider.setLocalLoginUser(user());
|
|
try {
|
service.buildEditorConfig(param);
|
fail("ELN session without tenant must be rejected");
|
} catch (DataException e) {
|
assertEquals("OnlyOffice 会话业务上下文不匹配", e.getMessage());
|
}
|
|
assertNull(session.getLockUserId());
|
verify(sessionMapper, never()).update(isNull(), any());
|
}
|
|
@Test
|
public void ordinaryConfigDoesNotProbeTemplate() {
|
OnlyOfficeConfigParam param = param("ordinary.edit", "document-helper");
|
param.setBizModule("lims.document");
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("lims.document");
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "document-helper"));
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("edit", castMap(config.get("editorConfig")).get("mode"));
|
verify(templateEditabilityApi, never()).editable(any(), any(), any());
|
verify(templateEditabilityApi, never()).canAnnotate(any(), any(), any(), any());
|
}
|
|
@Test
|
public void newSessionPersistsTenantIdExplicitly() {
|
OnlyOfficeConfigParam param = annotationParam();
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
UserProvider.setLocalLoginUser(user());
|
|
service.buildEditorConfig(param);
|
|
verify(sessionMapper).insert(argThat((OnlyOfficeSessionEntity session) ->
|
"tenant-a".equals(session.getTenantId())));
|
}
|
|
@Test
|
public void publishingSessionForcesReadOnlyConfig() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING);
|
session.setPublishToken("publish-token");
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void publishingSessionCannotBeTakenByEditorLock() {
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING);
|
session.setPublishToken("publish-token");
|
|
try {
|
invokePrivate("acquireLock",
|
new Class<?>[]{OnlyOfficeSessionEntity.class, String.class}, session, "user-1");
|
fail("PUBLISHING session must reject editor acquire");
|
} catch (DataException e) {
|
assertEquals("文档编辑锁获取失败,请重新打开", e.getMessage());
|
}
|
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void retiringAndHistoricalRecordSessionsCannotBeTakenByEditorLock() {
|
int[] blocked = {
|
OnlyOfficeSessionEntity.SessionStatus.RECORD_RETIRING,
|
OnlyOfficeSessionEntity.SessionStatus.RECORD_HISTORICAL
|
};
|
for (int status : blocked) {
|
OnlyOfficeSessionEntity session = recordSession();
|
session.setSessionStatus(status);
|
session.setPublishToken(status == OnlyOfficeSessionEntity.SessionStatus.RECORD_RETIRING
|
? "revision-token" : null);
|
|
try {
|
invokePrivate("acquireLock",
|
new Class<?>[]{OnlyOfficeSessionEntity.class, String.class}, session, "user-1");
|
fail("retiring or historical record session must reject editor acquire");
|
} catch (DataException e) {
|
assertEquals("历史记录文档不可编辑", e.getMessage());
|
}
|
}
|
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void retiringAndHistoricalRecordSessionsResolveToReadOnly() {
|
ActionResult<Boolean> allowed = probeResult(200, true);
|
when(recordFillApi.canFill(any(), any(), any(), any())).thenReturn(allowed);
|
OnlyOfficeConfigParam param = recordParam();
|
int[] blocked = {
|
OnlyOfficeSessionEntity.SessionStatus.RECORD_RETIRING,
|
OnlyOfficeSessionEntity.SessionStatus.RECORD_HISTORICAL
|
};
|
|
for (int status : blocked) {
|
OnlyOfficeSessionEntity session = recordSession();
|
session.setSessionStatus(status);
|
session.setLockUserId(null);
|
session.setLockHeartbeat(null);
|
assertEquals("view", invokePrivate("resolveMode",
|
new Class<?>[]{OnlyOfficeConfigParam.class, String.class,
|
OnlyOfficeSessionEntity.class, UserInfo.class},
|
param, "docx", session, user()));
|
}
|
}
|
|
@Test
|
public void concurrentFirstSessionInsertReloadsPublishingFenceAsView() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity publishing = session();
|
publishing.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING);
|
publishing.setPublishToken("publish-token");
|
when(sessionMapper.selectOne(any())).thenReturn(null, null, publishing);
|
when(sessionMapper.insert(any(OnlyOfficeSessionEntity.class)))
|
.thenThrow(new DuplicateKeyException("duplicate file identity"));
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
verify(sessionMapper, never()).update(any(OnlyOfficeSessionEntity.class), any());
|
}
|
|
@Test
|
public void publishFenceWonAfterModeResolutionReloadsAsReadOnly() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity idle = session();
|
idle.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
OnlyOfficeSessionEntity publishing = session();
|
publishing.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING);
|
publishing.setPublishToken("publish-token");
|
publishing.setDocKey("publishing-key");
|
when(sessionMapper.selectOne(any())).thenReturn(idle, publishing);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(0);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
assertEquals("publishing-key", castMap(config.get("document")).get("key"));
|
}
|
|
@Test
|
public void publishingSessionCannotClaimSave() {
|
OnlyOfficeSessionEntity publishing = session();
|
publishing.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING);
|
publishing.setPublishToken("publish-token");
|
when(sessionMapper.update(isNull(), any())).thenReturn(0);
|
|
assertEquals(false, invokePrivate("claimSave",
|
new Class<?>[]{OnlyOfficeSessionEntity.class}, publishing));
|
}
|
|
@Test
|
public void locateControlTagForcesViewAndIsInjectedOnlyIntoAnnotator() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setLocateControlTag("ELN_FIELD_orphan");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(
|
plugins(param, "eln-template-annotator", "document-helper"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
Map<String, Object> annotationContext = pluginContext(config, annotationGuid());
|
assertEquals("ELN_FIELD_orphan", annotationContext.get("locateControlTag"));
|
assertEquals(false, annotationContext.get("canAnnotate"));
|
assertFalse(annotationContext.containsKey("scope"));
|
assertFalse(annotationContext.containsKey("ticket"));
|
assertFalse(pluginContext(config, "asc.{HELPER}").containsKey("locateControlTag"));
|
assertNull(session.getLockUserId());
|
verify(annotateTicketIssuer, never()).issue(any(), any(), any());
|
|
OnlyOfficeConfigParam fill = recordParam();
|
fill.setLocateControlTag("ELN_FIELD_orphan");
|
assertFalse(pluginContext(invokeAssembleConfig(
|
fill, recordSession(), user(), plugins(fill, "eln-record-filler")), fillGuid())
|
.containsKey("locateControlTag"));
|
}
|
|
@Test
|
public void locateControlTagAndRepairBindingIdAreMutuallyExclusive() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setLocateControlTag("ELN_FIELD_orphan");
|
param.setRepairBindingId("binding-1");
|
UserProvider.setLocalLoginUser(user());
|
|
try {
|
service.buildEditorConfig(param);
|
fail("locate and repair must not be active together");
|
} catch (DataException e) {
|
assertEquals("locateControlTag 与 repairBindingId 不能同时使用", e.getMessage());
|
}
|
|
verify(sessionMapper, never()).insert(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void competingEditorCannotReturnEditConfigAfterLockCasFails() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setFileType("annex");
|
param.setMode("edit");
|
OnlyOfficeSessionEntity firstSnapshot = session();
|
firstSnapshot.setId("session-1");
|
firstSnapshot.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
OnlyOfficeSessionEntity secondSnapshot = session();
|
secondSnapshot.setId("session-1");
|
secondSnapshot.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(firstSnapshot, secondSnapshot);
|
when(sessionMapper.update(isNull(), any())).thenReturn(1);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1, 0);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
|
UserProvider.setLocalLoginUser(user("user-1"));
|
Map<String, Object> firstConfig = service.buildEditorConfig(param);
|
assertEquals("edit", castMap(firstConfig.get("editorConfig")).get("mode"));
|
|
UserProvider.setLocalLoginUser(user("user-2"));
|
try {
|
service.buildEditorConfig(param);
|
fail("expected edit lock conflict");
|
} catch (DataException e) {
|
assertEquals("文档编辑锁获取失败,请重新打开", e.getMessage());
|
}
|
|
assertEquals("user-1", firstSnapshot.getLockUserId());
|
assertNull(secondSnapshot.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE),
|
secondSnapshot.getSessionStatus());
|
verify(sessionMapper, times(2)).update(any(OnlyOfficeSessionEntity.class), argThat(wrapper -> {
|
String sql = wrapper.getSqlSegment();
|
return sql.contains("f_id") && sql.contains("doc_key") && sql.contains("version_no")
|
&& sql.contains("f_version")
|
&& sql.contains("f_delete_mark") && sql.contains("session_status")
|
&& sql.contains("lock_user_id") && sql.contains("lock_heartbeat")
|
&& sql.contains(" OR ");
|
}));
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
@Test
|
public void annotationSceneDoesNotLeakContextToOtherPlugin() {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setRepairBindingId("binding-1");
|
List<OnlyOfficePluginResolver.ResolvedPlugin> plugins = plugins(
|
param, "eln-template-annotator", "document-helper");
|
|
Map<String, Object> helper = pluginContext(
|
invokeAssembleConfig(param, session(), user(), plugins), "asc.{HELPER}");
|
|
assertFalse(helper.containsKey("scope"));
|
assertFalse(helper.containsKey("ticket"));
|
assertFalse(helper.containsKey("canAnnotate"));
|
assertFalse(helper.containsKey("repairBindingId"));
|
}
|
|
@Test
|
public void repairHintIsIgnoredOutsideTargetSceneAndPlugin() {
|
OnlyOfficeConfigParam fillParam = recordParam();
|
fillParam.setRepairBindingId("binding-1");
|
Map<String, Object> fillContext = pluginContext(invokeAssembleConfig(
|
fillParam, recordSession(), user(), plugins(fillParam, "eln-record-filler")), fillGuid());
|
|
OnlyOfficeConfigParam otherParam = param("other.scene", "document-helper");
|
otherParam.setRepairBindingId("binding-1");
|
Map<String, Object> otherContext = pluginContext(invokeAssembleConfig(
|
otherParam, session(), user(), plugins(otherParam, "document-helper")), "asc.{HELPER}");
|
|
assertFalse(fillContext.containsKey("repairBindingId"));
|
assertFalse(otherContext.containsKey("repairBindingId"));
|
}
|
|
@Test
|
public void recordSaveCommitsStructuredValuesBeforeCompletingSession() {
|
OnlyOfficeSessionEntity session = recordSession();
|
ActionResult<RecordFillSaveOperation> prepared = operationResult(
|
RecordFillSaveOperation.PREPARED, "operation-1", null);
|
ActionResult<RecordFillSaveOperation> committed = operationResult(
|
RecordFillSaveOperation.COMMITTED, "operation-1", null);
|
when(fileUploadApi.downFile(any())).thenReturn(downloadResponse("original-document"));
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx")))
|
.thenReturn(true);
|
when(recordFillApi.preflight(any())).thenReturn(prepared);
|
when(recordFillApi.commit(any())).thenReturn(committed);
|
|
invokeSaveBack(session, callback(), true);
|
|
verify(recordFillApi).preflight(argThat(command -> "tenant-a".equals(command.getTenantId())
|
&& "user-1".equals(command.getUserId())
|
&& "record-1".equals(command.getRecordId())
|
&& "lease-a".equals(command.getSessionDocKey())));
|
verify(recordFillApi).commit(argThat(command -> "operation-1".equals(command.getOperationCode())));
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE), session.getSessionStatus());
|
assertEquals(Integer.valueOf(2), session.getVersionNo());
|
}
|
|
@Test
|
public void rejectedRecordSaveRestoresOriginalAndReturnsLeaseToEditing() {
|
OnlyOfficeSessionEntity session = recordSession();
|
ActionResult<RecordFillSaveOperation> prepared = operationResult(
|
RecordFillSaveOperation.PREPARED, "operation-1", null);
|
ActionResult<RecordFillSaveOperation> rejected = operationResult(
|
RecordFillSaveOperation.REJECTED, "operation-1", "记录版本已过期");
|
when(fileUploadApi.downFile(any())).thenReturn(downloadResponse("original-document"));
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx")))
|
.thenReturn(true, true);
|
when(recordFillApi.preflight(any())).thenReturn(prepared);
|
when(recordFillApi.commit(any())).thenReturn(rejected);
|
|
try {
|
invokeSaveBack(session, callback(), true);
|
fail("rejected record save must fail the callback");
|
} catch (DataException expected) {
|
assertEquals("记录版本已过期", expected.getMessage());
|
}
|
|
verify(fileUploadApi, times(2)).replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"));
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING), session.getSessionStatus());
|
assertEquals("lease-a", session.getDocKey());
|
}
|
|
@Test
|
public void uncertainRecordSaveKeepsCandidateAndSavingLease() {
|
OnlyOfficeSessionEntity session = recordSession();
|
ActionResult<RecordFillSaveOperation> prepared = operationResult(
|
RecordFillSaveOperation.PREPARED, "operation-1", null);
|
when(fileUploadApi.downFile(any())).thenReturn(downloadResponse("original-document"));
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx")))
|
.thenReturn(true);
|
when(recordFillApi.preflight(any())).thenReturn(prepared);
|
when(recordFillApi.commit(any())).thenReturn(prepared);
|
|
try {
|
invokeSaveBack(session, callback(), true);
|
fail("uncertain record save must require manual reconciliation");
|
} catch (DataException expected) {
|
assertTrue(expected.getMessage().contains("保存结果不确定"));
|
}
|
|
verify(fileUploadApi, times(1)).replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx"));
|
assertEquals(Integer.valueOf(SAVING_STATUS), session.getSessionStatus());
|
assertEquals("lease-a", session.getDocKey());
|
}
|
|
@Test
|
public void replacementFailureAbortsPreparedRecordSave() {
|
OnlyOfficeSessionEntity session = recordSession();
|
ActionResult<RecordFillSaveOperation> prepared = operationResult(
|
RecordFillSaveOperation.PREPARED, "operation-1", null);
|
ActionResult<RecordFillSaveOperation> aborted = operationResult(
|
RecordFillSaveOperation.REJECTED, "operation-1", "文件写回失败");
|
when(fileUploadApi.downFile(any())).thenReturn(downloadResponse("original-document"));
|
when(fileUploadApi.replaceFile(any(), eq("WebAnnexFile/"), eq("template.docx")))
|
.thenReturn(false);
|
when(recordFillApi.preflight(any())).thenReturn(prepared);
|
when(recordFillApi.abort(any())).thenReturn(aborted);
|
|
try {
|
invokeSaveBack(session, callback(), true);
|
fail("replacement failure must fail the callback");
|
} catch (DataException expected) {
|
assertEquals("文件服务写回文档失败", expected.getMessage());
|
}
|
|
verify(recordFillApi).abort(argThat(command -> "operation-1".equals(command.getOperationCode())));
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.EDITING), session.getSessionStatus());
|
}
|
|
@Test
|
public void onlyValidatedPrintTicketsEnableDmsPrinting() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("018f0000-0000-7000-8000-000000000702");
|
param.setFileName("quality-manual.docx");
|
param.setFileType("dms");
|
param.setMode("view");
|
param.setDmsAccessTicket("validated-ticket");
|
UserProvider.setLocalLoginUser(user());
|
jnpf.dmsEntity.preview.model.DmsFilePreviewTicket ticket = new jnpf.dmsEntity.preview.model.DmsFilePreviewTicket();
|
when(dmsTicketValidator.validate(any(), any())).thenReturn(ticket);
|
OnlyOfficeSessionEntity dmsSession = session();
|
dmsSession.setBizModule(null);
|
dmsSession.setBizDataId(null);
|
dmsSession.setFileId(param.getFileId());
|
dmsSession.setFileType("dms");
|
when(sessionMapper.selectOne(any())).thenReturn(dmsSession);
|
for (String scope : List.of(jnpf.dmsEntity.preview.model.DmsFilePreviewTicket.SCOPE,
|
jnpf.dmsEntity.preview.model.DmsFilePreviewTicket.PRINT_SCOPE)) {
|
ticket.setScope(scope);
|
Map<String, Object> config = service.buildEditorConfig(param);
|
Map<String, Object> permissions = castMap(castMap(config.get("document")).get("permissions"));
|
assertEquals(scope.equals(jnpf.dmsEntity.preview.model.DmsFilePreviewTicket.PRINT_SCOPE), permissions.get("print"));
|
assertEquals(false, permissions.get("edit"));
|
assertEquals(false, permissions.get("download"));
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
}
|
verify(dmsTicketValidator, times(2)).validate(any(), any());
|
}
|
|
@Test
|
public void dmsPreviewIsReadOnlyAndUsesTicketedFileEndpoint() {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("018f0000-0000-7000-8000-000000000702");
|
param.setFileName("quality-manual.docx");
|
param.setFileType("dms");
|
param.setDmsAccessTicket("ticket-value");
|
UserInfo user = new UserInfo();
|
user.setUserId("user-1");
|
user.setUserName("Tester");
|
|
Map<String, Object> config = invokeAssembleConfig(param, session(), user);
|
Map<String, Object> document = castMap(config.get("document"));
|
Map<String, Object> permissions = castMap(document.get("permissions"));
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
|
assertEquals(OnlyOfficeUiConfig.previewPermissions(), permissions);
|
assertEquals(OnlyOfficeUiConfig.previewCustomization(), editorConfig.get("customization"));
|
assertEquals("view", editorConfig.get("mode"));
|
assertEquals(false, permissions.get("edit"));
|
assertEquals(false, permissions.get("comment"));
|
assertEquals(false, permissions.get("review"));
|
assertEquals(false, permissions.get("download"));
|
assertEquals(false, permissions.get("print"));
|
assertEquals(false, permissions.get("protect"));
|
assertEquals("http://gateway/api/file/Dms/OnlyOffice/018f0000-0000-7000-8000-000000000702?ticket=ticket-value",
|
document.get("url"));
|
assertNotEquals("key-1", document.get("key"));
|
|
param.setDmsAccessTicket("next-ticket-value");
|
Map<String, Object> nextConfig = invokeAssembleConfig(param, session(), user);
|
Map<String, Object> nextDocument = castMap(nextConfig.get("document"));
|
assertNotEquals(document.get("key"), nextDocument.get("key"));
|
}
|
|
@Test
|
public void dmsPreviewCallbackNeverWritesBackFile() {
|
OnlyOfficeSessionEntity session = session();
|
session.setFileType("dms");
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
OnlyOfficeCallbackParam callback = callback();
|
callback.setKey(session.getDocKey());
|
String token = JWTUtil.createToken(Collections.singletonMap("scope", "test"),
|
"test-secret".getBytes(StandardCharsets.UTF_8));
|
|
service.handleCallback(callback, "Bearer " + token);
|
|
verify(fileApi, never()).getPath(any());
|
verify(fileUploadApi, never()).replaceFile(any(), any(), any());
|
verify(sessionMapper, never()).updateById(any(OnlyOfficeSessionEntity.class));
|
}
|
|
private OnlyOfficeSessionEntity session() {
|
OnlyOfficeSessionEntity session = new OnlyOfficeSessionEntity();
|
session.setId("session-1");
|
session.setFileId("template.docx");
|
session.setFileType("annex");
|
session.setVersionNo(1);
|
session.setDocKey("key-1");
|
session.setTenantId("tenant-a");
|
session.setBizModule("eln.template");
|
session.setBizDataId("template-1");
|
return session;
|
}
|
|
@Test
|
public void uploadEditEnabledAnnotatorAutostartsForDocAndDocxWithPhysicalSource() {
|
for (String extension : List.of("doc", "docx")) {
|
Map<String, Object> config = uploadAnnotatorConfig(extension, "edit", "edit", true, false);
|
Map<String, Object> plugins = castMap(castMap(config.get("editorConfig")).get("plugins"));
|
String guid = "asc.{C764F7D6-055D-4C7D-97FD-83D2C4440585}";
|
assertEquals(List.of(guid), plugins.get("autostart"));
|
assertEquals(List.of("http://plugins/0.1.0-dev/dms-qianpiye-annotator/config.json"), plugins.get("pluginsData"));
|
Map<String, Object> context = castMap(castMap(plugins.get("options")).get(guid));
|
assertEquals("physical-file", context.get("documentId"));
|
assertEquals("dms", context.get("bizModule"));
|
assertEquals("dms.qianpiye", context.get("bizScene"));
|
assertEquals("onlyoffice:dms-qianpiye", context.get("scope"));
|
assertEquals("signed-ticket", context.get("ticket"));
|
assertEquals(true, context.get("canAnnotate"));
|
assertFalse(context.containsKey("bizDataId"));
|
assertTrue(String.valueOf(castMap(config.get("document")).get("url")).contains("/annex/physical-file?"));
|
}
|
}
|
|
@Test
|
public void uploadAnnotatorDoesNotLoadWhenDisabledReviewOrReadonly() {
|
assertNoUploadAnnotator(uploadAnnotatorConfig("docx", "edit", "edit", false, false));
|
assertNoUploadAnnotator(uploadAnnotatorConfig("docx", "review", "edit", true, false));
|
assertNoUploadAnnotator(uploadAnnotatorConfig("docx", "edit", "review", true, false));
|
assertNoUploadAnnotator(uploadAnnotatorConfig("docx", "view", "edit", true, false));
|
assertNoUploadAnnotator(uploadAnnotatorConfig("docx", "edit", "edit", true, true));
|
}
|
|
private void assertNoUploadAnnotator(Map<String, Object> config) {
|
assertFalse(castMap(config.get("editorConfig")).containsKey("plugins"));
|
}
|
|
private Map<String, Object> uploadAnnotatorConfig(String extension, String requestedMode,
|
String leaseMode, boolean enabled, boolean lockedByAnotherUser) {
|
OnlyOfficeProperties properties = new OnlyOfficeProperties();
|
properties.setJwtSecret("test-secret");
|
properties.setFileFetchBaseUrl("http://gateway");
|
properties.getUploadFile().setEditEnabled(true);
|
OnlyOfficeProperties.PluginDefinition definition = new OnlyOfficeProperties.PluginDefinition();
|
definition.setGuid("asc.{C764F7D6-055D-4C7D-97FD-83D2C4440585}");
|
definition.setVersion("0.1.0");
|
definition.setRelease("0.1.0-dev");
|
definition.setPublicBaseUrl("http://plugins");
|
definition.setApiBaseUrl("http://gateway");
|
properties.getPlugins().setDefinitions(Map.of("dms-qianpiye-annotator", definition));
|
OnlyOfficeProperties.PluginSceneMapping mapping = new OnlyOfficeProperties.PluginSceneMapping();
|
mapping.setBizScene("dms.qianpiye");
|
mapping.setBizModule("dms");
|
mapping.setPluginCodes(List.of("dms-qianpiye-annotator"));
|
// 上传开关自身要求自动启动,不依赖独立签批页场景的自动启动设置。
|
properties.getPlugins().setScenes(List.of(mapping));
|
UploadEditSessionSupport support = mock(UploadEditSessionSupport.class);
|
jnpf.base.model.uploadedit.UploadEditLease lease = new jnpf.base.model.uploadedit.UploadEditLease();
|
lease.setDraftId("draft");
|
lease.setMode(leaseMode);
|
lease.setQianpiyeAnnotatorEnabled(enabled);
|
lease.setFile(Map.of("fileId", "physical-file", "name", "source." + extension));
|
when(support.lease("tenant-a", "user-1", "lease")).thenReturn(lease);
|
DmsQianpiyeTicketIssuer issuer = mock(DmsQianpiyeTicketIssuer.class);
|
when(issuer.issue(any(), any(), any())).thenReturn("signed-ticket");
|
OnlyOfficeSceneConfigRegistry registry = new OnlyOfficeSceneConfigRegistry(
|
List.of(new UploadEditSceneConfigBuilder(support, properties, issuer)),
|
new DefaultSceneConfigBuilder(new OnlyOfficeEditPermissionEvaluator(properties)));
|
OnlyOfficeServiceImpl uploadService = new OnlyOfficeServiceImpl(properties, sessionMapper,
|
new OnlyOfficePluginResolver(properties), registry, templateEditabilityApi, recordFillApi,
|
dmsTicketValidator, fileApi, fileUploadApi, new DmsQianpiyeDocxPropertiesWriter());
|
OnlyOfficeSessionEntity session = session();
|
session.setFileId("lease");
|
session.setFileName("source." + extension);
|
session.setBizModule("visualdev");
|
session.setBizScene("visualdev.uploadFile");
|
session.setBizDataId("draft");
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
if (lockedByAnotherUser) {
|
session.setLockUserId("other-user");
|
session.setLockHeartbeat(new Date());
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
}
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(sessionMapper.update(any(OnlyOfficeSessionEntity.class), any())).thenReturn(1);
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("lease");
|
param.setFileName(session.getFileName());
|
param.setFileType("annex");
|
param.setBizModule("visualdev");
|
param.setBizScene("visualdev.uploadFile");
|
param.setBizDataId("draft");
|
param.setMode(requestedMode);
|
UserProvider.setLocalLoginUser(user());
|
Map<String, Object> config = uploadService.buildEditorConfig(param);
|
if (!castMap(config.get("editorConfig")).containsKey("plugins")) verifyNoTicket(issuer);
|
return config;
|
}
|
|
private void verifyNoTicket(DmsQianpiyeTicketIssuer issuer) {
|
verify(issuer, never()).issue(any(), any(), any());
|
}
|
|
private OnlyOfficeSessionEntity recordSession() {
|
OnlyOfficeSessionEntity session = session();
|
session.setBizModule("eln.record");
|
session.setBizDataId("record-1");
|
session.setDocKey("lease-a");
|
session.setLockUserId("user-1");
|
session.setLockHeartbeat(new Date());
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.EDITING);
|
session.setVersion(41);
|
return session;
|
}
|
|
@SuppressWarnings("unchecked")
|
private ActionResult<RecordFillSaveOperation> operationResult(
|
String status, String operationCode, String reason) {
|
RecordFillSaveOperation operation = new RecordFillSaveOperation();
|
operation.setStatus(status);
|
operation.setOperationCode(operationCode);
|
operation.setReason(reason);
|
ActionResult<RecordFillSaveOperation> result = mock(ActionResult.class);
|
when(result.getCode()).thenReturn(200);
|
when(result.getData()).thenReturn(operation);
|
return result;
|
}
|
|
private Response downloadResponse(String body) {
|
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
return Response.builder().status(200).reason("OK")
|
.request(Request.create(Request.HttpMethod.GET, "http://file/download",
|
Collections.<String, Collection<String>>emptyMap(), null,
|
Charset.defaultCharset()))
|
.body(new Response.Body() {
|
@Override public Integer length() { return bytes.length; }
|
@Override public boolean isRepeatable() { return true; }
|
@Override public ByteArrayInputStream asInputStream() {
|
return new ByteArrayInputStream(bytes);
|
}
|
@Override public java.io.Reader asReader(Charset charset) {
|
throw new UnsupportedOperationException();
|
}
|
@Override public void close() { }
|
}).build();
|
}
|
|
private void assertProbeDeniedReturnsView(ActionResult<Boolean> result, RuntimeException failure) {
|
OnlyOfficeConfigParam param = annotationParam();
|
param.setMode("edit");
|
OnlyOfficeSessionEntity session = session();
|
session.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.IDLE);
|
when(sessionMapper.selectOne(any())).thenReturn(session);
|
when(pluginResolver.resolve(param)).thenReturn(plugins(param, "eln-template-annotator"));
|
when(annotateTicketIssuer.issue(any(), any(), any())).thenReturn("annotation-ticket");
|
if (failure == null) {
|
when(templateEditabilityApi.canAnnotate(any(), any(), any(), any())).thenReturn(result);
|
} else {
|
when(templateEditabilityApi.canAnnotate(any(), any(), any(), any())).thenThrow(failure);
|
}
|
UserProvider.setLocalLoginUser(user());
|
|
Map<String, Object> config = service.buildEditorConfig(param);
|
|
assertEquals("view", castMap(config.get("editorConfig")).get("mode"));
|
assertNull(session.getLockUserId());
|
assertEquals(Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE), session.getSessionStatus());
|
assertFalse(castMap(config.get("editorConfig")).containsKey("plugins"));
|
verify(annotateTicketIssuer, never()).issue(any(), any(), any());
|
}
|
|
@SuppressWarnings("unchecked")
|
private ActionResult<Boolean> probeResult(int code, boolean data) {
|
ActionResult<Boolean> result = mock(ActionResult.class);
|
when(result.getCode()).thenReturn(code);
|
when(result.getData()).thenReturn(data);
|
return result;
|
}
|
|
private UserInfo user() {
|
return user("user-1");
|
}
|
|
private UserInfo user(String userId) {
|
UserInfo user = new UserInfo();
|
user.setTenantId("tenant-a");
|
user.setUserId(userId);
|
user.setUserName("Tester");
|
return user;
|
}
|
|
private OnlyOfficeConfigParam annotationParam() {
|
OnlyOfficeConfigParam param = param("eln.template.annotate", "eln-template-annotator");
|
param.setFileType("annex");
|
return param;
|
}
|
|
private OnlyOfficeConfigParam recordParam() {
|
OnlyOfficeConfigParam param = param("eln.record.fill", "eln-record-filler");
|
param.setBizModule("eln.record");
|
param.setBizDataId("record-1");
|
param.setFileType("annex");
|
return param;
|
}
|
|
private OnlyOfficeConfigParam param(String scene, String pluginCode) {
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId("template.docx");
|
param.setFileName("template.docx");
|
param.setBizDataId("template-1");
|
param.setBizModule("eln.template");
|
param.setBizScene(scene);
|
return param;
|
}
|
|
private List<OnlyOfficePluginResolver.ResolvedPlugin> plugins(
|
OnlyOfficeConfigParam param, String... pluginCodes) {
|
OnlyOfficeProperties properties = new OnlyOfficeProperties();
|
Map<String, OnlyOfficeProperties.PluginDefinition> definitions = new LinkedHashMap<>();
|
for (String pluginCode : pluginCodes) {
|
OnlyOfficeProperties.PluginDefinition definition = new OnlyOfficeProperties.PluginDefinition();
|
definition.setGuid(guid(pluginCode));
|
definition.setVersion("0.1.0");
|
definition.setRelease("0.1.0");
|
definition.setPublicBaseUrl("https://plugins.example.com");
|
definition.setApiBaseUrl("https://api.example.com");
|
definitions.put(pluginCode, definition);
|
}
|
properties.getPlugins().setDefinitions(definitions);
|
OnlyOfficeProperties.PluginSceneMapping mapping = new OnlyOfficeProperties.PluginSceneMapping();
|
mapping.setBizScene(param.getBizScene());
|
mapping.setBizModule(param.getBizModule());
|
mapping.setPluginCodes(java.util.Arrays.asList(pluginCodes));
|
properties.getPlugins().setScenes(Collections.singletonList(mapping));
|
return new OnlyOfficePluginResolver(properties).resolve(param);
|
}
|
|
private String guid(String pluginCode) {
|
if ("eln-template-annotator".equals(pluginCode)) {
|
return annotationGuid();
|
}
|
if ("eln-record-filler".equals(pluginCode)) {
|
return fillGuid();
|
}
|
return "asc.{HELPER}";
|
}
|
|
private int countOccurrences(String source, String target) {
|
int count = 0;
|
int index = 0;
|
while ((index = source.indexOf(target, index)) >= 0) {
|
count++;
|
index += target.length();
|
}
|
return count;
|
}
|
|
private String annotationGuid() {
|
return "asc.{2D6D6CB6-F6FC-45B7-8EC7-0CC1940F1A6F}";
|
}
|
|
private String fillGuid() {
|
return "asc.{7F4E8F35-5D66-47F8-A5B4-6AB3FAACF565}";
|
}
|
|
private OnlyOfficeCallbackParam callback() {
|
OnlyOfficeCallbackParam callback = new OnlyOfficeCallbackParam();
|
callback.setStatus(OnlyOfficeCallbackParam.Status.READY_FOR_SAVING);
|
callback.setUrl("http://127.0.0.1:" + server.getAddress().getPort() + "/saved.docx");
|
return callback;
|
}
|
|
private String callbackToken() {
|
return "Bearer " + cn.hutool.jwt.JWTUtil.createToken(Collections.emptyMap(),
|
"test-secret".getBytes(StandardCharsets.UTF_8));
|
}
|
|
private Object invokePrivate(String methodName, Class<?>[] parameterTypes, Object... arguments) {
|
try {
|
Method method = OnlyOfficeServiceImpl.class.getDeclaredMethod(methodName, parameterTypes);
|
method.setAccessible(true);
|
return method.invoke(service, arguments);
|
} catch (InvocationTargetException e) {
|
if (e.getCause() instanceof RuntimeException) {
|
throw (RuntimeException) e.getCause();
|
}
|
throw new RuntimeException(e.getCause());
|
} catch (ReflectiveOperationException e) {
|
throw new RuntimeException(e);
|
}
|
}
|
|
private void invokeSaveBack(OnlyOfficeSessionEntity session, OnlyOfficeCallbackParam callback, boolean rotateKey) {
|
try {
|
Method method = OnlyOfficeServiceImpl.class.getDeclaredMethod(
|
"saveBack", OnlyOfficeSessionEntity.class, OnlyOfficeCallbackParam.class, boolean.class);
|
method.setAccessible(true);
|
method.invoke(service, session, callback, rotateKey);
|
} catch (InvocationTargetException e) {
|
if (e.getCause() instanceof RuntimeException) {
|
throw (RuntimeException) e.getCause();
|
}
|
throw new RuntimeException(e.getCause());
|
} catch (ReflectiveOperationException e) {
|
throw new RuntimeException(e);
|
}
|
}
|
|
@SuppressWarnings("unchecked")
|
private Map<String, Object> invokeAssembleConfig(OnlyOfficeConfigParam param,
|
OnlyOfficeSessionEntity session,
|
UserInfo user) {
|
return invokeAssembleConfig(param, session, user, Collections.emptyList());
|
}
|
|
@SuppressWarnings("unchecked")
|
private Map<String, Object> invokeAssembleConfig(OnlyOfficeConfigParam param,
|
OnlyOfficeSessionEntity session,
|
UserInfo user,
|
List<OnlyOfficePluginResolver.ResolvedPlugin> plugins) {
|
return invokeAssembleConfig(param, session, user, "view", plugins);
|
}
|
|
@SuppressWarnings("unchecked")
|
private Map<String, Object> invokeAssembleConfig(OnlyOfficeConfigParam param,
|
OnlyOfficeSessionEntity session,
|
UserInfo user,
|
String mode,
|
List<OnlyOfficePluginResolver.ResolvedPlugin> plugins) {
|
try {
|
Method method = OnlyOfficeServiceImpl.class.getDeclaredMethod("assembleConfig",
|
OnlyOfficeConfigParam.class, OnlyOfficeSessionEntity.class, String.class, String.class,
|
String.class, UserInfo.class, List.class);
|
method.setAccessible(true);
|
return (Map<String, Object>) method.invoke(service, param, session, "word", "docx", mode, user,
|
plugins);
|
} catch (ReflectiveOperationException e) {
|
throw new RuntimeException(e);
|
}
|
}
|
|
@SuppressWarnings("unchecked")
|
private Map<String, Object> castMap(Object value) {
|
return (Map<String, Object>) value;
|
}
|
|
private Map<String, Object> pluginContext(Map<String, Object> config, String guid) {
|
Map<String, Object> editorConfig = castMap(config.get("editorConfig"));
|
Map<String, Object> plugins = castMap(editorConfig.get("plugins"));
|
return castMap(castMap(plugins.get("options")).get(guid));
|
}
|
}
|