文件
mynode/packages/lib/services/synchronizer/handleConflictAction.test.ts
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

212 行
5.8 KiB
TypeScript

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,
});
});
});