feat: Initial clean release of CHM
一些检查失败了
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
一些检查失败了
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
这个提交包含在:
@@ -0,0 +1,152 @@
|
||||
import { afterAllTests, beforeAllDb, beforeEachDb, createFolder, createUserAndSession, db, dbSlave, expectThrow, getDatabaseClientType, models, packageRootDir, updateFolder } from './utils/testing/testUtils';
|
||||
import { connectDb, disconnectDb, reconnectDb, sqliteSyncSlave } from './db';
|
||||
import { ChangeType, Event } from './services/database/types';
|
||||
import { DatabaseConfig, DatabaseConfigClient } from './utils/types';
|
||||
import { createDb } from './tools/dbTools';
|
||||
import { msleep } from './utils/time';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
const eventId1 = '4f405391-bd72-4a4f-809f-344fc6cd4b31';
|
||||
const eventId2 = '4f405391-bd72-4a4f-809f-344fc6cd4b32';
|
||||
|
||||
const event1: Event = {
|
||||
id: eventId1,
|
||||
type: 1,
|
||||
name: 'test',
|
||||
created_time: Date.now(),
|
||||
};
|
||||
|
||||
const event2 = {
|
||||
...event1,
|
||||
id: eventId2,
|
||||
};
|
||||
|
||||
let testIndex = 0;
|
||||
const beforeTest = async (envValues: Record<string, string> = null) => {
|
||||
// Use `beforeAllDb` in `beforeEach` to ensure each test has its own database.
|
||||
// To work around file locking issues on Windows, each test needs its own database instance:
|
||||
const databaseKey = `db.replication.${testIndex ++}`;
|
||||
await beforeAllDb(databaseKey, envValues ? { envValues } : null);
|
||||
await beforeEachDb();
|
||||
};
|
||||
|
||||
const afterTest = async () => {
|
||||
await afterAllTests();
|
||||
};
|
||||
|
||||
describe('db.replication', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await afterTest();
|
||||
});
|
||||
|
||||
it('should reconnect a database', async () => {
|
||||
if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;
|
||||
|
||||
await beforeTest();
|
||||
|
||||
await disconnectDb(db());
|
||||
await expectThrow(async () => db().insert(event1).into('events'));
|
||||
|
||||
await reconnectDb(db());
|
||||
await db().insert(event1).into('events');
|
||||
|
||||
{
|
||||
const results = await db().select('*').from('events');
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].id).toBe(eventId1);
|
||||
}
|
||||
|
||||
await reconnectDb(db());
|
||||
await db().insert(event2).into('events');
|
||||
|
||||
{
|
||||
const results = await db().select('*').from('events');
|
||||
expect(results.length).toBe(2);
|
||||
expect([results[0].id, results[1].id].sort()).toEqual([eventId1, eventId2]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should manually sync an SQLite slave instance', async () => {
|
||||
if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;
|
||||
|
||||
await beforeTest();
|
||||
|
||||
const masterConfig: DatabaseConfig = {
|
||||
client: DatabaseConfigClient.SQLite,
|
||||
name: `${packageRootDir}/db-master-test.sqlite`,
|
||||
};
|
||||
|
||||
const slaveConfig: DatabaseConfig = {
|
||||
client: DatabaseConfigClient.SQLite,
|
||||
name: `${packageRootDir}/db-slave-test.sqlite`,
|
||||
};
|
||||
|
||||
await createDb(masterConfig, { dropIfExists: true });
|
||||
await createDb(slaveConfig, { dropIfExists: true });
|
||||
|
||||
const master = await connectDb(masterConfig);
|
||||
const slave = await connectDb(slaveConfig);
|
||||
|
||||
await master.insert(event1).into('events');
|
||||
|
||||
expect((await master.select('*').from('events')).length).toBe(1);
|
||||
expect((await slave.select('*').from('events')).length).toBe(0);
|
||||
|
||||
await sqliteSyncSlave(master, slave);
|
||||
|
||||
expect((await master.select('*').from('events')).length).toBe(1);
|
||||
expect((await slave.select('*').from('events')).length).toBe(1);
|
||||
|
||||
await disconnectDb(master);
|
||||
await disconnectDb(slave);
|
||||
});
|
||||
|
||||
test('should track changes - using replication', async () => {
|
||||
if (getDatabaseClientType() === DatabaseConfigClient.PostgreSQL) return;
|
||||
|
||||
await beforeTest({ DB_USE_SLAVE: '1' });
|
||||
|
||||
const { session, user } = await createUserAndSession(1, true);
|
||||
const changeModel = models().change();
|
||||
|
||||
const folder = {
|
||||
id: '000000000000000000000000000000F1',
|
||||
title: 'title 1',
|
||||
};
|
||||
|
||||
const folderItem = await createFolder(session.id, folder);
|
||||
await msleep(1);
|
||||
|
||||
let result = await changeModel.delta(user.id);
|
||||
|
||||
// We get nothing because the slave has not been synced yet
|
||||
expect(result.items.length).toBe(0);
|
||||
|
||||
// But we still get the item because it doesn't use the slave database
|
||||
expect((await models().item().loadAsJoplinItem<FolderEntity>(folderItem.id)).title).toBe('title 1');
|
||||
|
||||
// After sync, we should get the change
|
||||
await sqliteSyncSlave(db(), dbSlave());
|
||||
result = await changeModel.delta(user.id);
|
||||
expect(result.items.length).toBe(1);
|
||||
expect(result.items[0].type).toBe(ChangeType.Create);
|
||||
|
||||
await updateFolder(session.id, { ...folder, title: 'title 2' });
|
||||
result = await changeModel.delta(user.id, { cursor: result.cursor });
|
||||
|
||||
// Nothing because it hasn't been synced yet
|
||||
expect(result.items.length).toBe(0);
|
||||
|
||||
// But we get the latest item if requesting it directly
|
||||
expect((await models().item().loadAsJoplinItem<FolderEntity>(folderItem.id)).title).toBe('title 2');
|
||||
|
||||
// After sync, we should get the change
|
||||
await sqliteSyncSlave(db(), dbSlave());
|
||||
result = await changeModel.delta(user.id, { cursor: result.cursor });
|
||||
|
||||
expect(result.items.length).toBe(1);
|
||||
expect(result.items[0].type).toBe(ChangeType.Update);
|
||||
});
|
||||
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户