package jnpf.bizcommon.onlyoffice.service.impl; import cn.hutool.jwt.JWTUtil; import jnpf.base.UserInfo; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties; import jnpf.dmsEntity.compare.DmsCompareTicket; import jnpf.dmsEntity.preview.vo.DmsFilePreviewTicketVO; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import org.junit.Before; import org.junit.Test; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.List; import java.util.UUID; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class OnlyOfficeCompareServiceTest { private final OnlyOfficeProperties properties = new OnlyOfficeProperties(); private final DmsOnlyOfficePreviewTicketValidator validator = mock(DmsOnlyOfficePreviewTicketValidator.class); private final DmsCompareTicket ticket = new DmsCompareTicket(); private final UserInfo user = new UserInfo(); private OnlyOfficeCompareService service; private final OnlyOfficeComparisonRenderer renderer = mock(OnlyOfficeComparisonRenderer.class); @Before public void setup() { properties.setJwtSecret("test-only-compare-secret"); properties.setFileFetchBaseUrl("http://gateway"); properties.setCallbackBaseUrl("http://gateway"); ticket.setTenantId("t1"); ticket.setUserId("u1"); ticket.setDocumentId(UUID.randomUUID()); ticket.setExpiresAt(System.currentTimeMillis() + 300_000); ticket.setHistorical(source("old-ticket")); ticket.setCurrent(source("new-ticket")); ticket.setHistoricalLabel("v1"); ticket.setCurrentLabel("v3"); user.setTenantId("t1"); user.setUserId("u1"); service = new OnlyOfficeCompareService(properties, new DmsDocumentCompareConfigBuilder(), validator, value -> JsonUtil.getObjectToString(ticket), renderer); when(renderer.generate(anyString(), anyString())).thenReturn("/cache/files/compare/output/Result.docx?md5=test"); } @SuppressWarnings("unchecked") @Test public void signsCorrectDirectionAndIsolatedSessionWithoutNormalSaveRoute() { Map result = service.createConfig("ticket", user); Map config = (Map) result.get("config"); Map doc = (Map) config.get("document"); Map editor = (Map) config.get("editorConfig"); assertFalse(result.containsKey("revisedDocument")); assertEquals("view", editor.get("mode")); Map permissions = (Map) doc.get("permissions"); Map customization = (Map) editor.get("customization"); assertEquals(false, permissions.get("edit")); assertEquals(false, permissions.get("review")); for (String permission : List.of("download", "print", "comment", "chat", "protect")) { assertEquals(permission, false, permissions.get(permission)); } for (String option : List.of("autosave", "forcesave", "plugins", "macros", "comments", "help", "feedback", "suggestFeature")) { assertEquals(option, false, customization.get(option)); } for (String option : List.of("toolbarHideFileName", "hideRightMenu", "compactToolbar")) { assertEquals(option, true, customization.get(option)); } assertEquals(Map.of("spellcheck", false), customization.get("features")); assertEquals(Map.of("trackChanges", false, "reviewDisplay", "markup", "showReviewChanges", true), customization.get("review")); verify(renderer).generate(endsWith("ticket=old-ticket"), endsWith("ticket=new-ticket")); assertTrue(doc.get("url").toString().startsWith("http://gateway/api/biz/onlyoffice/compare/result?ticket=")); assertEquals("http://gateway/api/biz/onlyoffice/compare/callback", editor.get("callbackUrl")); assertTrue(JWTUtil.verify(config.get("token").toString(), properties.getJwtSecret().getBytes(StandardCharsets.UTF_8))); Map second = (Map) service.createConfig("ticket", user).get("config"); assertNotEquals(doc.get("key"), ((Map)second.get("document")).get("key")); assertFalse(new DmsDocumentCompareConfigBuilder().build(null).canEdit()); } @SuppressWarnings("unchecked") @Test public void inheritsPreviewPolicyWithoutMutatingPreviewDefaults() { Map previewUi = OnlyOfficeUiConfig.previewCustomization(); Map previewPermissions = OnlyOfficeUiConfig.previewPermissions(); Map config = new DmsDocumentCompareConfigBuilder() .comparisonConfig("key", "title", "source", "callback", user); Map editor = (Map) config.get("editorConfig"); Map actualUi = (Map) editor.get("customization"); Map doc = (Map) config.get("document"); Map actualPermissions = (Map) doc.get("permissions"); Map expectedUi = new java.util.LinkedHashMap<>(previewUi); expectedUi.put("forcesave", false); expectedUi.put("close", Map.of("visible", false)); expectedUi.put("review", Map.of("trackChanges", false, "reviewDisplay", "markup", "showReviewChanges", true)); assertEquals(expectedUi, actualUi); Map expectedPermissions = new java.util.LinkedHashMap<>(previewPermissions); assertEquals(expectedPermissions, actualPermissions); assertEquals(previewUi, OnlyOfficeUiConfig.previewCustomization()); assertEquals(previewPermissions, OnlyOfficeUiConfig.previewPermissions()); } @Test public void rejectsExpiredAndCrossUserTickets() { user.setUserId("intruder"); rejects(() -> service.createConfig("ticket", user)); user.setUserId("u1"); ticket.setExpiresAt(0); rejects(() -> service.createConfig("ticket", user)); verifyNoInteractions(validator); } @SuppressWarnings("unchecked") @Test public void resultStreamRequiresAnUnexpiredSignedResultTicket() { Map config = (Map) service.createConfig("ticket", user).get("config"); String url = ((Map) config.get("document")).get("url").toString(); String resultTicket = url.substring(url.indexOf("?ticket=") + 8); when(renderer.readResult(anyString())).thenReturn(new byte[]{1, 2}); assertArrayEquals(new byte[]{1, 2}, service.readResult(resultTicket)); verify(renderer).readResult("/cache/files/compare/output/Result.docx?md5=test"); clearInvocations(renderer); rejects(() -> service.readResult(resultTicket + "tampered")); String expired = JWTUtil.createToken(Map.of("scope", "dms:onlyoffice:compare-result", "expiresAt", 0), properties.getJwtSecret().getBytes(StandardCharsets.UTF_8)); rejects(() -> service.readResult(expired)); verifyNoInteractions(renderer); } @Test public void generationFailureNeverReturnsAnEditableFallback() { when(renderer.generate(anyString(), anyString())).thenThrow(new DataException("生成失败")); rejects(() -> service.createConfig("ticket", user)); } @Test public void acknowledgesSaveStatusesWithoutFetchingOutputOrWritingSource() { OnlyOfficeCallbackParam callback = new OnlyOfficeCallbackParam(); callback.setKey("dms-compare-" + UUID.randomUUID()); callback.setUrl("http://must-not-be-fetched.invalid/output.docx"); String token = JWTUtil.createToken(Map.of("key", callback.getKey()), properties.getJwtSecret().getBytes(StandardCharsets.UTF_8)); for (int status : new int[]{1, 2, 3, 4, 6, 7}) { callback.setStatus(status); service.acknowledgeCallback(callback, "Bearer " + token); } verifyNoInteractions(validator); rejects(() -> service.acknowledgeCallback(callback, null)); callback.setKey("ordinary-edit-key"); rejects(() -> service.acknowledgeCallback(callback, "Bearer " + token)); } private void rejects(Runnable action) { try { action.run(); fail("Expected DataException"); } catch (DataException expected) { } } private DmsFilePreviewTicketVO source(String value) { DmsFilePreviewTicketVO s = new DmsFilePreviewTicketVO(); s.setFileVersionId(UUID.randomUUID()); s.setFileName("procedure.docx"); s.setAccessTicket(value); return s; } }