mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-30 09:55:19 +00:00
Tests:
- More tests have been added.
This commit is contained in:
@@ -11,7 +11,7 @@ const localdb_test_setting = {
|
||||
handleFilenameCaseSensitive: false,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
describe("Plugin Integration Test (Local Database)", async () => {
|
||||
describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
let harness: LiveSyncHarness;
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { afterAll, beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
|
||||
import {
|
||||
DummyFileSourceInisialised,
|
||||
FILE_SIZE_BINS,
|
||||
FILE_SIZE_MD,
|
||||
generateBinaryFile,
|
||||
generateFile,
|
||||
} from "../utils/dummyfile";
|
||||
import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common";
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { closeReplication, performReplication, prepareRemote } from "./sync_common";
|
||||
import type { DataWriteOptions } from "obsidian";
|
||||
|
||||
type MTimedDataWriteOptions = DataWriteOptions & { mtime: number };
|
||||
export type TestOptions = {
|
||||
setting: ObsidianLiveSyncSettings;
|
||||
fileOptions: MTimedDataWriteOptions;
|
||||
};
|
||||
function generateName(prefix: string, type: string, ext: string, size: number) {
|
||||
return `${prefix}-${type}-file-${size}.${ext}`;
|
||||
}
|
||||
export function syncBasicCase(label: string, { setting, fileOptions }: TestOptions) {
|
||||
describe("Replication Suite Tests - " + label, () => {
|
||||
const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size);
|
||||
let serverPeerName = "";
|
||||
// TODO: Harness disposal may broke the event loop of P2P replication
|
||||
// so we keep the harnesses alive until all tests are done.
|
||||
// It may trystero's somethong, or not.
|
||||
let harnessUpload: LiveSyncHarness;
|
||||
let harnessDownload: LiveSyncHarness;
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await commands.closeWebPeer();
|
||||
serverPeerName = "t-" + Date.now();
|
||||
setting.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
setting.P2P_AutoSyncPeers = serverPeerName;
|
||||
setting.P2P_DevicePeerName = "client-" + Date.now();
|
||||
await commands.openWebPeer(setting, serverPeerName);
|
||||
}
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
await commands.closeWebPeer();
|
||||
// await closeP2PReplicatorConnections(harnessUpload);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Remote Database Initialization", () => {
|
||||
let harnessInit: LiveSyncHarness;
|
||||
const sync_test_setting_init = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Remote Database Initialization - Vault: ${vaultName}`);
|
||||
harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
expect(harnessInit.plugin).toBeDefined();
|
||||
expect(harnessInit.plugin.app).toBe(harnessInit.app);
|
||||
await waitForIdle(harnessInit);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harnessInit.plugin.services.replicator.getActiveReplicator()?.closeReplication();
|
||||
await harnessInit.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
it("should reset remote database", async () => {
|
||||
// harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
await prepareRemote(harnessInit, sync_test_setting_init, true);
|
||||
});
|
||||
it("should be prepared for replication", async () => {
|
||||
await waitForReady(harnessInit);
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
const status = await harnessInit.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(sync_test_setting_init);
|
||||
console.log("Connected devices after reset:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Upload", () => {
|
||||
const sync_test_setting_upload = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_upload.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_DevicePeerName = "up-" + Date.now();
|
||||
}
|
||||
harnessUpload = await generateHarness(vaultName, sync_test_setting_upload);
|
||||
await waitForReady(harnessUpload);
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
await waitForIdle(harnessUpload);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessUpload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessUpload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessUpload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should prepare remote database", async () => {
|
||||
await prepareRemote(harnessUpload, sync_test_setting_upload, false);
|
||||
});
|
||||
|
||||
// describe("File Creation", async () => {
|
||||
it("should a file has been created", async () => {
|
||||
const content = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
// Perform replication
|
||||
// await harness.plugin.services.replication.replicate(true);
|
||||
});
|
||||
it("should different content of several files have been created correctly", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes has been created", async (size) => {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await checkStoredFileInDB(harnessUpload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
it("Replication after uploads", async () => {
|
||||
await performReplication(harnessUpload);
|
||||
await performReplication(harnessUpload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Download", () => {
|
||||
// Download into a new vault
|
||||
const sync_test_setting_download = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Download - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_download.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_DevicePeerName = "down-" + Date.now();
|
||||
}
|
||||
harnessDownload = await generateHarness(vaultName, sync_test_setting_download);
|
||||
await waitForReady(harnessDownload);
|
||||
await prepareRemote(harnessDownload, sync_test_setting_download, false);
|
||||
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
await delay(1000);
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessDownload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessDownload.plugin).toBeDefined();
|
||||
expect(harnessDownload.plugin.app).toBe(harnessDownload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessDownload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessDownload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should a file has been synchronised", async () => {
|
||||
const expectedContent = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileRead(harnessDownload, path, expectedContent, fileOptions);
|
||||
});
|
||||
it("should different content of several files have been synchronised", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (harnessDownload) {
|
||||
await closeReplication(harnessDownload);
|
||||
await harnessDownload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
if (harnessUpload) {
|
||||
await closeReplication(harnessUpload);
|
||||
await harnessUpload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
});
|
||||
it("Wait for idle state", async () => {
|
||||
await delay(100);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip("Replication Suite Tests (Single)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
+17
-267
@@ -1,57 +1,32 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type FilePath,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import {
|
||||
DummyFileSourceInisialised,
|
||||
FILE_SIZE_BINS,
|
||||
FILE_SIZE_MD,
|
||||
generateBinaryFile,
|
||||
generateFile,
|
||||
} from "../utils/dummyfile";
|
||||
import { checkStoredFileInDB, defaultFileOption, testFileRead, testFileWrite } from "./db_common";
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
const env = (import.meta as any).env;
|
||||
const sync_test_setting_base = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
function generateName(prefix: string, type: string, ext: string, size: number) {
|
||||
return `${prefix}-${type}-file-${size}.${ext}`;
|
||||
}
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
for (const remoteType of [RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_COUCHDB]) {
|
||||
for (const useE2EE of [false, true]) {
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false, true];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
@@ -66,235 +41,10 @@ function* generateCase() {
|
||||
}
|
||||
}
|
||||
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) {
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.services.replicator.getActiveReplicator()?.tryResetRemoteDatabase(harness.plugin.settings);
|
||||
} else {
|
||||
await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.settings);
|
||||
}
|
||||
await harness.plugin.services.replicator.getActiveReplicator()?.markRemoteResolved(harness.plugin.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests", async () => {
|
||||
describe("Replication Suite Tests (Normal)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size);
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
});
|
||||
|
||||
describe("Remote Database Initialization", async () => {
|
||||
let harnessInit: LiveSyncHarness;
|
||||
const sync_test_setting_init = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
it("should initialize remote database", async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeEach - Remote Database Initialization - Vault: ${vaultName}`);
|
||||
harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
expect(harnessInit.plugin).toBeDefined();
|
||||
expect(harnessInit.plugin.app).toBe(harnessInit.app);
|
||||
await waitForIdle(harnessInit);
|
||||
});
|
||||
|
||||
it("should reset remote database", async () => {
|
||||
// harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
await prepareRemote(harnessInit, sync_test_setting_init, true);
|
||||
});
|
||||
it("should be prepared for replication", async () => {
|
||||
// harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
// await prepareRemote(harness, sync_test_setting_init, false);
|
||||
const status = await harnessInit.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(sync_test_setting_init);
|
||||
console.log("Connected devices after reset:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Upload", async () => {
|
||||
let harnessUpload: LiveSyncHarness;
|
||||
|
||||
const sync_test_setting_upload = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
it("Setup Upload Harness", async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`);
|
||||
harnessUpload = await generateHarness(vaultName, sync_test_setting_upload);
|
||||
await waitForReady(harnessUpload);
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
waitForIdle(harnessUpload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", async () => {
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", async () => {
|
||||
expect(harnessUpload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", async () => {
|
||||
expect(harnessUpload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should prepare remote database", async () => {
|
||||
await prepareRemote(harnessUpload, sync_test_setting_upload, false);
|
||||
});
|
||||
|
||||
// describe("File Creation", async () => {
|
||||
it("should store single file", async () => {
|
||||
const content = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
// Perform replication
|
||||
// await harness.plugin.services.replication.replicate(true);
|
||||
});
|
||||
it("should different content of several files are stored correctly", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should handle large file of size %i bytes", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should handle binary file of size %i bytes", async (size) => {
|
||||
// const isTooLarge = harness.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await checkStoredFileInDB(harnessUpload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
// });
|
||||
// Perform final replication after all tests
|
||||
it("Replication after uploads", async () => {
|
||||
await harnessUpload.plugin.services.replication.replicate(true);
|
||||
await waitForIdle(harnessUpload);
|
||||
// Ensure all files are uploaded
|
||||
await harnessUpload.plugin.services.replication.replicate(true);
|
||||
await waitForIdle(harnessUpload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Download", async () => {
|
||||
let harnessDownload: LiveSyncHarness;
|
||||
// Download into a new vault
|
||||
const sync_test_setting_download = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
it("should initialize remote database", async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
harnessDownload = await generateHarness(vaultName, sync_test_setting_download);
|
||||
await waitForReady(harnessDownload);
|
||||
await prepareRemote(harnessDownload, sync_test_setting_download, false);
|
||||
await harnessDownload.plugin.services.replication.replicate(true);
|
||||
await waitForIdle(harnessDownload);
|
||||
// Version info might be downloaded, and then replication will be interrupted,
|
||||
await harnessDownload.plugin.services.replication.replicate(true); // Ensure all files are downloaded
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
|
||||
it("should perform initial replication to download files", async () => {
|
||||
await harnessDownload.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.markRemoteResolved(sync_test_setting_download);
|
||||
await harnessDownload.plugin.services.replication.replicate(true);
|
||||
await waitForIdle(harnessDownload);
|
||||
// Version info might be downloaded, and then replication will be interrupted,
|
||||
await harnessDownload.plugin.services.replication.replicate(true); // Ensure all files are downloaded
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", async () => {
|
||||
expect(harnessDownload.plugin).toBeDefined();
|
||||
expect(harnessDownload.plugin.app).toBe(harnessDownload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", async () => {
|
||||
expect(harnessDownload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", async () => {
|
||||
expect(harnessDownload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
// describe("File Checking", async () => {
|
||||
it("should retrieve the single file", async () => {
|
||||
const expectedContent = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileRead(harnessDownload, path, expectedContent, fileOptions);
|
||||
});
|
||||
it("should retrieve different content of several files correctly", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should retrieve the file %i bytes", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should handle binary file of size %i bytes", async (size) => {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
// });
|
||||
});
|
||||
it("Wait for idle state", async () => {
|
||||
await delay(100);
|
||||
});
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { expect } from "vitest";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { LOG_LEVEL_INFO, RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { waitTaskWithFollowups } from "../lib/util";
|
||||
async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// Wait for peers to connect
|
||||
const maxRetries = 10;
|
||||
let retries = maxRetries;
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
const p2pReplicator = await replicator.getP2PConnection(LOG_LEVEL_INFO);
|
||||
if (!p2pReplicator) {
|
||||
throw new Error("P2P Replicator is not initialized");
|
||||
}
|
||||
while (retries-- > 0) {
|
||||
const peers = p2pReplicator.knownAdvertisements;
|
||||
|
||||
if (peers && peers.length > 0) {
|
||||
console.log("P2P peers connected:", peers);
|
||||
return;
|
||||
}
|
||||
await commands.acceptWebPeer();
|
||||
console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`);
|
||||
console.dir(peers);
|
||||
await delay(3000);
|
||||
await commands.acceptWebPeer();
|
||||
}
|
||||
console.log("Failed to connect P2P peers after retries");
|
||||
throw new Error("P2P peers did not connect in time.");
|
||||
}
|
||||
}
|
||||
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
replicator.closeReplication();
|
||||
await delay(30);
|
||||
replicator.closeReplication();
|
||||
replicator.closeReplication();
|
||||
await delay(1000);
|
||||
console.log("P2P replicator connections closed");
|
||||
// if (replicator instanceof LiveSyncTrysteroReplicator) {
|
||||
// replicator.closeReplication();
|
||||
// await delay(1000);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
export async function performReplication(harness: LiveSyncHarness) {
|
||||
await waitForP2PPeers(harness);
|
||||
await delay(500);
|
||||
const p = harness.plugin.services.replication.replicate(true);
|
||||
const task =
|
||||
harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P
|
||||
? waitTaskWithFollowups(
|
||||
p,
|
||||
() => {
|
||||
// Accept any peer dialogs during replication (fire and forget)
|
||||
void commands.acceptWebPeer();
|
||||
return Promise.resolve();
|
||||
},
|
||||
30000,
|
||||
500
|
||||
)
|
||||
: p;
|
||||
const result = await task;
|
||||
await waitForIdle(harness);
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function closeReplication(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
return await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
console.log("No active replicator to close");
|
||||
return;
|
||||
}
|
||||
await replicator.closeReplication();
|
||||
await waitForIdle(harness);
|
||||
console.log("Replication closed");
|
||||
}
|
||||
|
||||
export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) {
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryResetRemoteDatabase(harness.plugin.settings);
|
||||
} else {
|
||||
await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.settings);
|
||||
}
|
||||
await harness.plugin.services.replicator.getActiveReplicator()?.markRemoteResolved(harness.plugin.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import { settingBase } from "./variables.ts";
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const sync_test_setting_base = settingBase;
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_P2P];
|
||||
const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
// const e2eeOptions = [false, true];
|
||||
const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests (P2P)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DoctorRegulation } from "@/lib/src/common/configForDoc";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
ChunkAlgorithms,
|
||||
AutoAccepting,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
export const env = (import.meta as any).env;
|
||||
export const settingBase = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
usePluginSyncV2: true,
|
||||
chunkSplitterVersion: ChunkAlgorithms.RabinKarp,
|
||||
doctorProcessedVersion: DoctorRegulation.version,
|
||||
notifyThresholdOfRemoteStorageSize: 800,
|
||||
P2P_AutoAccepting: AutoAccepting.ALL,
|
||||
P2P_AutoBroadcast: true,
|
||||
P2P_AutoStart: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_passphrase: "p2psync-test",
|
||||
P2P_roomID: "p2psync-test",
|
||||
P2P_DevicePeerName: "p2psync-test",
|
||||
P2P_relays: "ws://localhost:4000/",
|
||||
P2P_AutoAcceptingPeers: "p2p-livesync-web-peer",
|
||||
P2P_SyncOnReplication: "p2p-livesync-web-peer",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
Reference in New Issue
Block a user