1 import { ConnectionCloseReason } from '@proton/docs-proto'
4 ClientRequiresEditorMethods,
6 InternalEventBusInterface,
8 } from '@proton/docs-shared'
9 import { DecryptedMessage, DocumentRole } from '@proton/docs-shared'
10 import type { DriveCompat, NodeMeta } from '@proton/drive-store'
11 import type { LoggerInterface } from '@proton/utils/logs'
12 import { Result } from '../../Domain/Result/Result'
13 import { MAX_DOC_SIZE, MAX_UPDATE_SIZE } from '../../Models/Constants'
14 import type { DecryptedCommit } from '../../Models/DecryptedCommit'
15 import type { WebsocketConnectionEvent } from '../../Realtime/WebsocketEvent/WebsocketConnectionEvent'
16 import type { WebsocketConnectionEventPayloads } from '../../Realtime/WebsocketEvent/WebsocketConnectionEventPayloads'
17 import type { WebsocketServiceInterface } from '../../Services/Websockets/WebsocketServiceInterface'
18 import type { DocumentEntitlements } from '../../Types/DocumentEntitlements'
19 import type { CreateNewDocument } from '../../UseCase/CreateNewDocument'
20 import type { DuplicateDocument } from '../../UseCase/DuplicateDocument'
21 import type { ExportAndDownload } from '../../UseCase/ExportAndDownload'
22 import type { GetDocumentMeta } from '../../UseCase/GetDocumentMeta'
23 import type { LoadCommit } from '../../UseCase/LoadCommit'
24 import type { LoadDocument } from '../../UseCase/LoadDocument'
25 import type { SeedInitialCommit } from '../../UseCase/SeedInitialCommit'
26 import type { SquashDocument } from '../../UseCase/SquashDocument'
27 import { DocController } from './DocController'
28 import { DocControllerEvent } from './DocControllerEvent'
29 import { DocumentMeta } from '../../Models/DocumentMeta'
30 import type { GetNode } from '../../UseCase/GetNode'
32 describe('DocController', () => {
33 let controller: DocController
36 trashDocument: jest.fn(),
37 restoreDocument: jest.fn(),
38 getShareId: jest.fn(),
41 beforeEach(async () => {
42 controller = new DocController(
44 driveCompat as unknown as jest.Mocked<DriveCompat>,
45 {} as jest.Mocked<SquashDocument>,
46 {} as jest.Mocked<SeedInitialCommit>,
48 executePrivate: jest.fn().mockReturnValue(
50 node: { parentNodeId: 'parent-node-id-123' },
51 meta: { nodeMeta: { linkId: 'link-id-123' } },
53 entitlements: { keys: {}, role: new DocumentRole('Editor') },
56 } as unknown as jest.Mocked<LoadDocument>,
58 execute: jest.fn().mockReturnValue(
60 numberOfUpdates: jest.fn(),
61 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array(0)),
62 needsSquash: jest.fn().mockReturnValue(false),
66 } as unknown as jest.Mocked<LoadCommit>,
67 {} as jest.Mocked<DuplicateDocument>,
68 {} as jest.Mocked<CreateNewDocument>,
70 execute: jest.fn().mockReturnValue(
72 latestCommitId: jest.fn().mockReturnValue('123'),
75 } as unknown as jest.Mocked<GetDocumentMeta>,
76 {} as jest.Mocked<ExportAndDownload>,
78 execute: jest.fn().mockReturnValue(Result.ok({})),
79 } as unknown as jest.Mocked<GetNode>,
81 createConnection: jest.fn().mockReturnValue({ connect: jest.fn().mockResolvedValue(true) }),
82 sendDocumentUpdateMessage: jest.fn(),
83 flushPendingUpdates: jest.fn(),
84 reconnectToDocumentWithoutDelay: jest.fn(),
85 closeConnection: jest.fn(),
86 } as unknown as jest.Mocked<WebsocketServiceInterface>,
88 addEventHandler: jest.fn(),
90 } as unknown as jest.Mocked<InternalEventBusInterface>,
96 } as unknown as jest.Mocked<LoggerInterface>,
99 controller.entitlements = {
104 } as unknown as DocumentEntitlements
106 controller.beginInitialSyncTimer = jest.fn()
107 controller.beginInitialConnectionTimer = jest.fn()
109 controller.editorInvoker = {
110 receiveMessage: jest.fn(),
111 showEditor: jest.fn(),
112 performOpeningCeremony: jest.fn(),
113 changeLockedState: jest.fn().mockResolvedValue(false),
114 performClosingCeremony: jest.fn(),
115 getDocumentState: jest.fn(),
116 } as unknown as jest.Mocked<ClientRequiresEditorMethods>
118 await controller.initialize()
121 it('should queue updates received while editor was not yet ready', async () => {
122 controller.editorInvoker = undefined
124 await controller.handleDocumentUpdatesMessage({} as DecryptedMessage)
126 expect(controller.updatesReceivedWhileEditorInvokerWasNotReady).toHaveLength(1)
129 it('should replay queued updates as soon as editor is ready, and clear the queue', async () => {
130 controller.editorInvoker = undefined
132 await controller.handleDocumentUpdatesMessage(
133 new DecryptedMessage({
134 content: new Uint8Array(),
135 signature: new Uint8Array(),
136 authorAddress: '123',
142 controller.handleDocumentUpdatesMessage = jest.fn()
144 await controller.editorIsReadyToReceiveInvocations({
145 receiveMessage: jest.fn(),
146 showEditor: jest.fn(),
147 performOpeningCeremony: jest.fn(),
148 changeLockedState: jest.fn(),
149 } as unknown as jest.Mocked<ClientRequiresEditorMethods>)
151 expect(controller.handleDocumentUpdatesMessage).toHaveBeenCalled()
152 expect(controller.updatesReceivedWhileEditorInvokerWasNotReady).toHaveLength(0)
155 describe('editorIsReadyToReceiveInvocations', () => {
156 it('should send initial commit to editor', async () => {
157 controller.sendInitialCommitToEditor = jest.fn()
159 const editorInvoker = controller.editorInvoker!
160 controller.editorInvoker = undefined
161 await controller.editorIsReadyToReceiveInvocations(editorInvoker)
163 expect(controller.sendInitialCommitToEditor).toHaveBeenCalled()
167 describe('initialize', () => {
168 it('should set the initial commit', async () => {
169 controller.setInitialCommit = jest.fn()
171 await controller.initialize()
173 expect(controller.setInitialCommit).toHaveBeenCalled()
177 describe('setInitialCommit', () => {
178 it('should send the initial commit to the editor', async () => {
179 controller.sendInitialCommitToEditor = jest.fn()
181 await controller.setInitialCommit({
182 needsSquash: jest.fn(),
183 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array()),
184 } as unknown as jest.Mocked<DecryptedCommit>)
186 expect(controller.sendInitialCommitToEditor).toHaveBeenCalled()
189 it('should set last commit id property', async () => {
190 await controller.setInitialCommit({
191 needsSquash: jest.fn(),
193 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array()),
194 } as unknown as jest.Mocked<DecryptedCommit>)
196 expect(controller.lastCommitIdReceivedFromRtsOrApi).toBe('456')
200 describe('showEditorIfAllConnectionsReady', () => {
201 it('should not show editor if docs server connection is not ready', () => {
202 controller.realtimeConnectionReady = true
203 controller.docsServerDataReady = false
204 controller.showEditor = jest.fn()
206 controller.showEditorIfAllConnectionsReady()
208 expect(controller.showEditor).not.toHaveBeenCalled()
211 it('should not show editor if realtime connection is not ready', () => {
212 controller.realtimeConnectionReady = false
213 controller.docsServerDataReady = true
214 controller.showEditor = jest.fn()
216 controller.showEditorIfAllConnectionsReady()
218 expect(controller.showEditor).not.toHaveBeenCalled()
221 it('should not show editor if editorInvoker is undefined', () => {
222 controller.editorInvoker = undefined
223 controller.realtimeConnectionReady = true
224 controller.docsServerDataReady = true
225 controller.showEditor = jest.fn()
227 controller.showEditorIfAllConnectionsReady()
229 expect(controller.showEditor).not.toHaveBeenCalled()
233 describe('handleDocumentUpdatesMessage', () => {
234 it('should increment size tracker size', async () => {
235 controller.sizeTracker.incrementSize = jest.fn()
237 await controller.handleDocumentUpdatesMessage({
238 byteSize: jest.fn().mockReturnValue(25),
239 } as unknown as DecryptedMessage)
241 expect(controller.sizeTracker.incrementSize).toHaveBeenCalledWith(25)
245 describe('handleRealtimeConnectionReady', () => {
246 it('should show editor', () => {
247 controller.didAlreadyReceiveEditorReadyEvent = true
248 controller.docsServerDataReady = true
250 controller.handleRealtimeConnectionReady()
252 expect(controller.editorInvoker!.showEditor).toHaveBeenCalled()
256 describe('reloadEditingLockedState', () => {
257 it('should lock if user does not have editing permissions', () => {
258 controller.doesUserHaveEditingPermissions = jest.fn().mockReturnValue(false)
260 controller.reloadEditingLockedState()
262 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
265 it('should lock if experiencing errored sync', () => {
266 controller.isExperiencingErroredSync = true
268 controller.reloadEditingLockedState()
270 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
273 it('should lock if websocket status is connecting', () => {
274 controller.websocketStatus = 'connecting'
276 controller.reloadEditingLockedState()
278 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
281 it('should lock if size constraint reached', () => {
282 controller.isLockedDueToSizeContraint = true
284 controller.reloadEditingLockedState()
286 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
289 it('should lock if websocket status is disconnected', () => {
290 controller.websocketStatus = 'disconnected'
292 controller.reloadEditingLockedState()
294 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
297 it('should unlock if all flags are green', () => {
298 controller.doesUserHaveEditingPermissions = jest.fn().mockReturnValue(true)
299 controller.isExperiencingErroredSync = false
300 controller.websocketStatus = 'connected'
302 controller.reloadEditingLockedState()
304 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(false)
307 it('should lock if participation limit reached and user is not owner', () => {
308 controller.doesUserOwnDocument = jest.fn().mockReturnValue(false)
309 controller.participantTracker.isParticipantLimitReached = jest.fn().mockReturnValue(true)
311 controller.reloadEditingLockedState()
313 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
316 it('should not lock if participation limit reached and user is owner', () => {
317 controller.doesUserOwnDocument = jest.fn().mockReturnValue(true)
318 controller.participantTracker.isParticipantLimitReached = jest.fn().mockReturnValue(true)
320 controller.reloadEditingLockedState()
322 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
325 it('should lock if editor has rendering issue', () => {
326 controller.hasEditorRenderingIssue = true
328 controller.reloadEditingLockedState()
330 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
333 it('should lock if trashedState is trashed', () => {
334 controller.trashState = 'trashed'
336 controller.reloadEditingLockedState()
338 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
341 it('should lock if trashedState is trashed', () => {
342 controller.trashState = 'trashed'
344 controller.reloadEditingLockedState()
346 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
349 it('should report metric if locked', () => {
350 controller.hasEditorRenderingIssue = true
351 controller.incrementMetricsReadonlyState = jest.fn()
353 controller.reloadEditingLockedState()
355 expect(controller.incrementMetricsReadonlyState).toHaveBeenCalled()
359 describe('websocket lifecycle', () => {
360 it('should lock document when websocket is still connecting', () => {
361 controller.handleWebsocketConnectingEvent()
363 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
366 it('should begin initial sync timer on connected event', () => {
367 controller.beginInitialSyncTimer = jest.fn()
369 controller.handleWebsocketConnectedEvent()
371 expect(controller.beginInitialSyncTimer).toHaveBeenCalled()
375 describe('handleWebsocketDisconnectedEvent', () => {
376 it('should prevent editing when websocket is closed', () => {
377 const payload: WebsocketConnectionEventPayloads[WebsocketConnectionEvent.Disconnected] = {
378 document: {} as NodeMeta,
379 serverReason: ConnectionCloseReason.create({ code: ConnectionCloseReason.CODES.NORMAL_CLOSURE }),
381 controller.handleWebsocketDisconnectedEvent(payload)
383 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
386 it('should refetch commit if disconnect reason is stale commit id', () => {
387 const payload: WebsocketConnectionEventPayloads[WebsocketConnectionEvent.Disconnected] = {
388 document: {} as NodeMeta,
389 serverReason: ConnectionCloseReason.create({ code: ConnectionCloseReason.CODES.STALE_COMMIT_ID }),
391 controller.refetchCommitDueToStaleContents = jest.fn()
393 controller.handleWebsocketDisconnectedEvent(payload)
395 expect(controller.refetchCommitDueToStaleContents).toHaveBeenCalled()
399 describe('handleCommitIdOutOfSyncEvent', () => {
400 it('should refetch commit', () => {
401 controller.refetchCommitDueToStaleContents = jest.fn()
403 controller.handleFailedToGetTokenDueToCommitIdOutOfSyncEvent()
405 expect(controller.refetchCommitDueToStaleContents).toHaveBeenCalled()
409 describe('refetchCommitDueToStaleContents', () => {
410 it('should post UnableToResolveCommitIdConflict error if unable to resolve', async () => {
411 controller.logger.error = jest.fn()
413 controller._loadCommit.execute = jest.fn().mockReturnValue(Result.fail('Unable to resolve'))
414 controller._getDocumentMeta.execute = jest.fn().mockReturnValue(Result.fail('Unable to resolve'))
416 await controller.refetchCommitDueToStaleContents('rts-disconnect')
418 expect(controller.eventBus.publish).toHaveBeenCalledWith({
419 type: DocControllerEvent.UnableToResolveCommitIdConflict,
424 it('should not reprocess if already processing', async () => {
425 controller._loadCommit.execute = jest.fn().mockReturnValue(
427 numberOfUpdates: jest.fn(),
428 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array()),
429 needsSquash: jest.fn().mockReturnValue(false),
432 controller._getDocumentMeta.execute = jest.fn().mockReturnValue(Result.ok({}))
434 controller.isRefetchingStaleCommit = true
436 await controller.refetchCommitDueToStaleContents('rts-disconnect')
438 expect(controller._loadCommit.execute).not.toHaveBeenCalled()
441 it('should re-set the initial commit if commit is successfully resolved', async () => {
442 controller._loadCommit.execute = jest.fn().mockReturnValue(
444 numberOfUpdates: jest.fn().mockReturnValue(0),
445 needsSquash: jest.fn().mockReturnValue(false),
446 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array()),
450 controller.setInitialCommit = jest.fn()
452 await controller.refetchCommitDueToStaleContents('rts-disconnect')
454 expect(controller.setInitialCommit).toHaveBeenCalled()
457 it('should reconnect websocket without delay if commit is successfully resolved', async () => {
458 controller._loadCommit.execute = jest.fn().mockReturnValue(
460 numberOfUpdates: jest.fn().mockReturnValue(0),
461 needsSquash: jest.fn().mockReturnValue(false),
462 squashedRepresentation: jest.fn().mockReturnValue(new Uint8Array()),
466 await controller.refetchCommitDueToStaleContents('rts-disconnect')
468 expect(controller.websocketService.reconnectToDocumentWithoutDelay).toHaveBeenCalled()
472 describe('handleWebsocketConnectedEvent', () => {
473 it('should allow editing', () => {
474 controller.handleWebsocketConnectedEvent()
476 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(false)
480 describe('handleWebsocketFailedToConnectEvent', () => {
481 it('should block editing', () => {
482 controller.handleWebsocketFailedToConnectEvent()
484 expect(controller.editorInvoker!.changeLockedState).toHaveBeenCalledWith(true)
487 it('should set state to disconnected', () => {
488 controller.handleWebsocketFailedToConnectEvent()
490 expect(controller.websocketStatus).toBe('disconnected')
494 describe('handleWebsocketAckStatusChangeEvent', () => {
495 it('should reload editing locked state', () => {
496 controller.reloadEditingLockedState = jest.fn()
498 controller.handleWebsocketAckStatusChangeEvent({
499 ledger: { hasErroredMessages: jest.fn() },
500 } as unknown as WebsocketConnectionEventPayloads[WebsocketConnectionEvent.AckStatusChange])
502 expect(controller.reloadEditingLockedState).toHaveBeenCalled()
506 describe('editorRequestsPropagationOfUpdate', () => {
507 it('should propagate update', () => {
508 void controller.editorRequestsPropagationOfUpdate(
516 } as RtsMessagePayload,
517 'mock' as BroadcastSource,
520 expect(controller.websocketService.sendDocumentUpdateMessage).toHaveBeenCalled()
523 it('should increment size tracker size', () => {
524 controller.sizeTracker.incrementSize = jest.fn()
526 void controller.editorRequestsPropagationOfUpdate(
534 } as RtsMessagePayload,
535 'mock' as BroadcastSource,
538 expect(controller.sizeTracker.incrementSize).toHaveBeenCalledWith(123)
541 it('should refuse propagation of update if the update is larger than the max update size', () => {
542 controller.handleAttemptingToBroadcastUpdateThatIsTooLarge = jest.fn()
544 void controller.editorRequestsPropagationOfUpdate(
550 byteLength: MAX_UPDATE_SIZE + 1,
552 } as RtsMessagePayload,
553 'mock' as BroadcastSource,
556 expect(controller.handleAttemptingToBroadcastUpdateThatIsTooLarge).toHaveBeenCalled()
557 expect(controller.websocketService.sendDocumentUpdateMessage).not.toHaveBeenCalled()
560 it('should refuse propagation of update if the update would exceed total document size', () => {
561 controller.sizeTracker.resetWithSize(MAX_DOC_SIZE - 1)
563 controller.handleAttemptingToBroadcastUpdateThatIsTooLarge = jest.fn()
565 void controller.editorRequestsPropagationOfUpdate(
573 } as RtsMessagePayload,
574 'mock' as BroadcastSource,
577 expect(controller.handleAttemptingToBroadcastUpdateThatIsTooLarge).toHaveBeenCalled()
578 expect(controller.websocketService.sendDocumentUpdateMessage).not.toHaveBeenCalled()
581 it('should handle special conversion flow', () => {
582 controller.handleEditorProvidingInitialConversionContent = jest.fn()
584 void controller.editorRequestsPropagationOfUpdate(
587 wrapper: 'conversion',
592 } as RtsMessagePayload,
593 'mock' as BroadcastSource,
596 expect(controller.handleEditorProvidingInitialConversionContent).toHaveBeenCalled()
599 it('should not seed initial commit if update is not conversion', () => {
600 controller.handleEditorProvidingInitialConversionContent = jest.fn()
602 void controller.editorRequestsPropagationOfUpdate(
610 } as RtsMessagePayload,
611 'mock' as BroadcastSource,
614 expect(controller.handleEditorProvidingInitialConversionContent).not.toHaveBeenCalled()
618 describe('createInitialCommit', () => {
619 it('should set lastCommitIdReceivedFromRtsOrApi', async () => {
620 controller._createInitialCommit.execute = jest.fn().mockReturnValue(Result.ok({ commitId: '123' }))
622 await controller.createInitialCommit()
624 expect(controller.lastCommitIdReceivedFromRtsOrApi).toBe('123')
628 describe('handleAttemptingToBroadcastUpdateThatIsTooLarge', () => {
629 it('should lock editing', () => {
630 controller.logger.error = jest.fn()
632 controller.reloadEditingLockedState = jest.fn()
634 controller.handleAttemptingToBroadcastUpdateThatIsTooLarge()
636 expect(controller.isLockedDueToSizeContraint).toBe(true)
638 expect(controller.reloadEditingLockedState).toHaveBeenCalled()
642 describe('handleEditorProvidingInitialConversionContent', () => {
644 controller.createInitialCommit = jest.fn().mockReturnValue(Result.ok({ commitId: '123' }))
647 it('should abort existing connection attempt', async () => {
648 controller.websocketService.closeConnection = jest.fn()
650 const promise = controller.handleEditorProvidingInitialConversionContent(new Uint8Array())
652 expect(controller.abortWebsocketConnectionAttempt).toEqual(true)
656 expect(controller.abortWebsocketConnectionAttempt).toEqual(false)
657 expect(controller.websocketService.closeConnection).toHaveBeenCalled()
660 it('should create initial commit', async () => {
661 await controller.handleEditorProvidingInitialConversionContent(new Uint8Array())
663 expect(controller.createInitialCommit).toHaveBeenCalled()
666 it('should reconnect to document without delay', async () => {
667 controller.websocketService.reconnectToDocumentWithoutDelay = jest.fn()
669 await controller.handleEditorProvidingInitialConversionContent(new Uint8Array())
671 expect(controller.websocketService.reconnectToDocumentWithoutDelay).toHaveBeenCalled()
675 describe('editorIsRequestingToLockAfterRenderingIssue', () => {
676 it('should set hasEditorRenderingIssue', () => {
677 controller.editorIsRequestingToLockAfterRenderingIssue()
679 expect(controller.hasEditorRenderingIssue).toBe(true)
682 it('should reload editing locked state', () => {
683 controller.reloadEditingLockedState = jest.fn()
685 controller.editorIsRequestingToLockAfterRenderingIssue()
687 expect(controller.reloadEditingLockedState).toHaveBeenCalled()
691 describe('trashDocument', () => {
692 let publishDocumentTrashStateUpdated: jest.SpyInstance
695 controller.docMeta = new DocumentMeta({ linkId: 'abc', volumeId: 'def' }, ['ghi'], 123, 456, 'jkl')
697 controller._getNode.execute = jest
699 .mockResolvedValue(Result.ok({ node: { parentNodeId: '123', trashed: true } }))
701 publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
704 it('trashState should be trashing initially', () => {
705 const promise = controller.trashDocument()
706 expect(controller.getTrashState()).toBe('trashing')
707 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)
711 it('trashState should be trashed when complete', async () => {
712 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
713 await controller.trashDocument()
714 expect(controller.getTrashState()).toBe('trashed')
715 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(2)
718 it('should refreshNodeAndDocMeta', async () => {
719 const refreshNodeAndDocMeta = jest.spyOn(controller, 'refreshNodeAndDocMeta')
721 await controller.trashDocument()
722 expect(refreshNodeAndDocMeta).toHaveBeenCalled()
725 it('should reload editor locked state', async () => {
726 const reloadEditingLockedState = jest.spyOn(controller, 'reloadEditingLockedState')
728 await controller.trashDocument()
729 expect(reloadEditingLockedState).toHaveBeenCalled()
732 it('should set didTrashDocInCurrentSession to true', async () => {
733 expect(controller.didTrashDocInCurrentSession).toBe(false)
735 await controller.trashDocument()
737 expect(controller.didTrashDocInCurrentSession).toBe(true)
741 describe('restoreDocument', () => {
742 let publishDocumentTrashStateUpdated: jest.SpyInstance
745 controller.docMeta = new DocumentMeta({ linkId: 'abc', volumeId: 'def' }, ['ghi'], 123, 456, 'jkl')
747 controller._getNode.execute = jest
749 .mockResolvedValue(Result.ok({ node: { parentNodeId: '123', trashed: true } }))
751 publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
754 it('trashState should be restoring initially', () => {
755 controller.trashState = 'trashed'
756 const promise = controller.restoreDocument()
757 expect(controller.getTrashState()).toBe('restoring')
758 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)
762 it('trashState should be restored when complete', async () => {
763 controller.trashState = 'trashed'
764 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
765 controller._getNode.execute = jest
767 .mockResolvedValue(Result.ok({ node: { parentNodeId: '123', trashed: false } }))
768 await controller.restoreDocument()
769 expect(controller.getTrashState()).toBe('not_trashed')
770 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(2)
773 it('should refreshNodeAndDocMeta', async () => {
774 const refreshNodeAndDocMeta = jest.spyOn(controller, 'refreshNodeAndDocMeta')
776 await controller.restoreDocument()
777 expect(refreshNodeAndDocMeta).toHaveBeenCalled()
780 it('should reload editor locked state', async () => {
781 const reloadEditingLockedState = jest.spyOn(controller, 'reloadEditingLockedState')
783 await controller.restoreDocument()
784 expect(reloadEditingLockedState).toHaveBeenCalled()
787 it('should reconnect socket', async () => {
788 controller.trashState = 'trashed'
789 const mock = (controller.websocketService.reconnectToDocumentWithoutDelay = jest.fn())
791 await controller.restoreDocument()
793 expect(mock).toHaveBeenCalled()
797 describe.each(['trashed', 'not_trashed', 'restoring', 'trashing'])('setTrashState', (state) => {
798 it(`will set the trashState ${state}`, () => {
799 controller.setTrashState(state as DocTrashState)
800 expect(controller.getTrashState()).toBe(state)
803 it(`will publish that the state has updated ${state}`, () => {
804 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
806 controller.setTrashState(state as DocTrashState)
807 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)
811 describe('refreshNodeAndDocMeta', () => {
812 it('trashed state will be not_trashed if DecryptedNode trashed property is null', async () => {
813 controller.docMeta = new DocumentMeta({ linkId: 'abc', volumeId: 'def' }, ['ghi'], 123, 456, 'jkl')
815 controller._getNode.execute = jest
817 .mockResolvedValueOnce(Result.ok({ node: { parentNodeId: 123, trashed: null } }))
818 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
820 await controller.refreshNodeAndDocMeta({ imposeTrashState: undefined })
821 expect(controller.getTrashState()).toBe('not_trashed')
822 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)
825 it('trashed state will be not_trashed if DecryptedNode trashed property is omitted', async () => {
826 controller.docMeta = new DocumentMeta({ linkId: 'abc', volumeId: 'def' }, ['ghi'], 123, 456, 'jkl')
828 controller._getNode.execute = jest.fn().mockResolvedValueOnce(Result.ok({ node: { parentNodeId: 123 } }))
829 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
831 await controller.refreshNodeAndDocMeta({ imposeTrashState: undefined })
832 expect(controller.getTrashState()).toBe('not_trashed')
833 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)
836 it('trashed state will be trashed if DecryptedNode trashed property is populated', async () => {
837 controller.docMeta = new DocumentMeta({ linkId: 'abc', volumeId: 'def' }, ['ghi'], 123, 456, 'jkl')
839 controller._getNode.execute = jest
841 .mockResolvedValueOnce(Result.ok({ node: { parentNodeId: 123, trashed: 123 } }))
842 const publishDocumentTrashStateUpdated = jest.spyOn(controller, 'publishDocumentTrashStateUpdated')
844 await controller.refreshNodeAndDocMeta({ imposeTrashState: undefined })
845 expect(controller.getTrashState()).toBe('trashed')
846 expect(publishDocumentTrashStateUpdated).toHaveBeenCalledTimes(1)