import BaseItem from '../../models/BaseItem'; import Note from '../../models/Note'; import { encryptionService, loadEncryptionMasterKey, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils'; import handleConflictAction from './utils/handleConflictAction'; import { SyncAction } from './utils/types'; describe('handleConflictAction', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await switchClient(1); }); test('note conflict is created', async () => { const local = await Note.save({ title: 'Test', body: 'body' }); // Pass the local note with unsaved changes to verify that the note is reloaded before creating the conflict const changedLocal = { ...local, title: 'TestChanged' }; const remoteContent = { ...local, title: 'TestRemote' }; const initialSyncItem = await BaseItem.syncItem(1, local.id); await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, changedLocal, 1, false, (action) => (action), ); const createdSyncItem = await BaseItem.syncItem(1, local.id); const updatedLocal = await Note.load(local.id); const notes = await Note.all(); const conflictNote = await Note.loadByTitle('Test'); expect(initialSyncItem).toBeUndefined(); expect(createdSyncItem).toBeDefined(); expect(updatedLocal.title).toBe('TestRemote'); expect(notes.length).toBe(2); expect(conflictNote.id).not.toBe(local.id); }); test('note conflict is not created when remote and local contents match', async () => { const local = await Note.save({ title: 'Test', body: 'body' }); // Pass the local note with unsaved changes to verify that the note is reloaded before checking if eligible to create a conflict const changedLocal = { ...local, title: 'TestChanged' }; const remoteContent = local; const initialSyncItem = await BaseItem.syncItem(1, local.id); await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, changedLocal, 1, false, (action) => (action), ); const createdSyncItem = await BaseItem.syncItem(1, local.id); const notes = await Note.all(); expect(initialSyncItem).toBeUndefined(); expect(createdSyncItem).toBeDefined(); expect(notes.length).toBe(1); }); test('note conflict is not created when encrypted remote and local contents match', async () => { await loadEncryptionMasterKey(); const local = await Note.save({ title: 'Test', body: 'body' }); const encryption_cipher_text = await encryptionService().encryptString(await Note.serialize(local)); const remoteContent = { ...local, title: '', body: '', encryption_applied: 1, encryption_cipher_text, }; await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, local, 1, false, jest.fn(), ); expect(await Note.all()).toHaveLength(1); }); test('conflict created for a remote deletion does not reference the deleted local note', async () => { const local = await Note.save({ title: 'Locally changed', body: 'local body' }); await handleConflictAction( SyncAction.NoteConflict, Note, false, null, local, 1, false, jest.fn(), ); expect(await Note.load(local.id)).toBeFalsy(); const notes = await Note.all(); expect(notes).toHaveLength(1); expect(notes[0]).toMatchObject({ title: local.title, body: local.body, is_conflict: 1, conflict_original_id: '', }); }); test('conflict of a conflict is not created for a read-only note', async () => { const local = await Note.save({ title: 'Local conflict', body: 'local', is_conflict: 1 }); const remoteContent = { ...local, title: 'Remote conflict', body: 'remote' }; await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, local, 1, true, jest.fn(), ); const notes = await Note.all(); expect(notes).toHaveLength(1); expect(notes[0].id).toBe(local.id); expect(notes[0].title).toBe(remoteContent.title); }); test('locked note becomes a conflict and the lock is restored when a read-only remote lost its lock', async () => { const local = await Note.save({ title: 'Locked', body: 'JLD01cipher', is_locked: 1 }); const remoteContent = { ...local, body: 'edited on an old client' }; delete remoteContent.is_locked; await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, local, 1, true, jest.fn(), ); expect(await Note.load(local.id)).toMatchObject({ body: remoteContent.body, is_locked: 1 }); expect((await Note.all()).find(note => note.is_conflict)).toMatchObject({ body: 'JLD01cipher', is_locked: 1 }); expect((await BaseItem.itemsThatNeedSync(1)).items.map(item => item.id)).not.toContain(local.id); }); test('remote content is not decrypted for an existing conflict note', async () => { const local = await Note.save({ title: 'Local conflict', body: 'local', is_conflict: 1 }); const remoteContent = { ...local, title: '', body: '', encryption_applied: 1, encryption_cipher_text: 'invalid', }; const decryptStringSpy = jest.spyOn(encryptionService(), 'decryptString'); await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, local, 1, false, jest.fn(), ); expect(decryptStringSpy).not.toHaveBeenCalled(); decryptStringSpy.mockRestore(); expect(await Note.all()).toHaveLength(1); }); test('editor reload event is emitted for note conflict', async () => { const local = await Note.save({ title: 'Test', body: 'body' }); const remoteContent = { ...local, title: 'TestRemote' }; const dispatch = jest.fn(); await handleConflictAction( SyncAction.NoteConflict, Note, true, remoteContent, local, 1, false, dispatch, ); expect(dispatch).toHaveBeenCalledWith({ type: 'EDITOR_NOTE_NEEDS_RELOAD', noteId: local.id, }); }); });