test: improve overall test coverage

This commit is contained in:
thecodrr
2022-08-15 10:57:25 +05:00
parent 6d45c23e79
commit 87b778db94
25 changed files with 908 additions and 714 deletions

View File

@@ -1,421 +0,0 @@
import Database from "../../index";
import { NodeStorageInterface } from "../../../__mocks__/node-storage.mock";
import FS from "../../../__mocks__/fs.mock";
import { CHECK_IDS, EV, EVENTS } from "../../../common";
import EventSource from "eventsource";
import { delay } from "../../../__tests__/utils";
test.skip(
"case 1: device A & B should only download the changes from device C (no uploading)",
async () => {
const types = [];
function onSyncProgress({ type }) {
types.push(type);
}
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
deviceA.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
deviceB.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
const deviceC = await initializeDevice("deviceC");
await deviceC.notes.add({ title: "new note 1" });
await syncAndWait(deviceC, deviceC);
expect(types.every((t) => t === "download")).toBe(true);
await cleanup(deviceA, deviceB, deviceC);
},
600 * 1000
);
test.skip(
"case 3: Device A & B have unsynced changes but server has nothing",
async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const note1Id = await deviceA.notes.add({
title: "Test note from device A",
});
const note2Id = await deviceB.notes.add({
title: "Test note from device B",
});
await syncAndWait(deviceA, deviceB);
expect(deviceA.notes.note(note2Id)).toBeTruthy();
expect(deviceB.notes.note(note1Id)).toBeTruthy();
expect(deviceA.notes.note(note1Id)).toBeTruthy();
expect(deviceB.notes.note(note2Id)).toBeTruthy();
await cleanup(deviceA, deviceA);
},
30 * 1000
);
// test.skip(
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
// async () => {
// const deviceA = await initializeDevice("deviceA");
// const deviceB = await initializeDevice("deviceB");
// const unsyncedNoteIds = [];
// for (let i = 0; i < 10; ++i) {
// const id = await deviceA.notes.add({
// title: `Test note ${i} from device A`,
// });
// unsyncedNoteIds.push(id);
// }
// const half = unsyncedNoteIds.length / 2 + 1;
// deviceA.eventManager.subscribe(
// EVENTS.syncProgress,
// async ({ type, current }) => {
// if (type === "upload" && current === half) {
// await deviceA.syncer.stop();
// }
// }
// );
// await expect(deviceA.sync(true)).rejects.toThrow();
// let syncedNoteIds = [];
// for (let i = 0; i < unsyncedNoteIds.length; ++i) {
// const expectedNoteId = unsyncedNoteIds[i];
// if (deviceB.notes.note(expectedNoteId))
// syncedNoteIds.push(expectedNoteId);
// }
// expect(
// syncedNoteIds.length === half - 1 || syncedNoteIds.length === half
// ).toBe(true);
// const deviceBNoteId = await deviceB.notes.add({
// title: "Test note of case 4 from device B",
// });
// await deviceB.sync(true);
// await syncAndWait(deviceA, deviceB);
// expect(deviceA.notes.note(deviceBNoteId)).toBeTruthy();
// expect(
// unsyncedNoteIds
// .map((id) => !!deviceB.notes.note(id))
// .every((res) => res === true)
// ).toBe(true);
// await cleanup(deviceA, deviceB);
// },
// 60 * 1000
// );
// test.only(
// "case 5: Device A's sync is interrupted halfway and Device B makes changes on the same note's content that didn't get synced on Device A due to interruption.",
// async () => {
// const deviceA = await initializeDevice("deviceA");
// const deviceB = await initializeDevice("deviceB");
// const noteIds = [];
// for (let i = 0; i < 10; ++i) {
// const id = await deviceA.notes.add({
// content: {
// type: "tiptap",
// data: `<p>deviceA=true</p>`,
// },
// });
// noteIds.push(id);
// }
// await deviceA.sync(true);
// await deviceB.sync(true);
// const unsyncedNoteIds = [];
// for (let id of noteIds) {
// const noteId = await deviceA.notes.add({
// id,
// content: {
// type: "tiptap",
// data: `<p>deviceA=true+changed=true</p>`,
// },
// });
// unsyncedNoteIds.push(noteId);
// }
// deviceA.eventManager.subscribe(
// EVENTS.syncProgress,
// async ({ type, total, current }) => {
// const half = total / 2 + 1;
// if (type === "upload" && current === half) {
// await deviceA.syncer.stop();
// }
// }
// );
// await expect(deviceA.sync(true)).rejects.toThrow();
// await delay(10 * 1000);
// for (let id of unsyncedNoteIds) {
// await deviceB.notes.add({
// id,
// content: {
// type: "tiptap",
// data: "<p>changes from device B</p>",
// },
// });
// }
// const error = await withError(async () => {
// await deviceB.sync(true);
// await deviceA.sync(true);
// });
// expect(error).not.toBeInstanceOf(NoErrorThrownError);
// expect(error.message.includes("Merge")).toBeTruthy();
// await cleanup(deviceA, deviceB);
// },
// 60 * 1000
// );
test.skip(
"issue: running force sync from device A makes device B always download everything",
async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
await syncAndWait(deviceA, deviceB, true);
const handler = jest.fn();
deviceB.eventManager.subscribe(EVENTS.syncProgress, handler);
await deviceB.sync(true);
expect(handler).not.toHaveBeenCalled();
await cleanup(deviceB);
},
60 * 1000
);
test.skip(
"issue: colors are not properly created if multiple notes are synced together",
async () => {
const deviceA = await initializeDevice("deviceA", [CHECK_IDS.noteColor]);
const deviceB = await initializeDevice("deviceB", [CHECK_IDS.noteColor]);
const noteIds = [];
for (let i = 0; i < 3; ++i) {
const id = await deviceA.notes.add({
content: {
type: "tiptap",
data: `<p>deviceA=true</p>`,
},
});
noteIds.push(id);
}
await syncAndWait(deviceA, deviceB);
for (let noteId of noteIds) {
await deviceA.notes.note(noteId).color("purple");
expect(deviceB.notes.note(noteId)).toBeTruthy();
expect(deviceB.notes.note(noteId).data.color).toBeUndefined();
}
await syncAndWait(deviceA, deviceB);
await delay(2000);
const purpleColor = deviceB.colors.tag("purple");
console.log(noteIds, purpleColor.noteIds);
expect(noteIds.every((id) => purpleColor.noteIds.indexOf(id) > -1)).toBe(
true
);
await cleanup(deviceA, deviceB);
},
60 * 1000
);
test.skip(
"issue: new topic on device A gets replaced by the new topic on device B",
async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const id = await deviceA.notebooks.add({ title: "Notebook 1" });
await syncAndWait(deviceA, deviceB, false);
expect(deviceB.notebooks.notebook(id)).toBeDefined();
await deviceA.notebooks.notebook(id).topics.add("Topic 1");
// to create a conflict
await delay(1500);
await deviceB.notebooks.notebook(id).topics.add("Topic 2");
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
await syncAndWait(deviceA, deviceB, false);
await delay(1000);
await syncAndWait(deviceB, deviceB, false);
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceA.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
await cleanup(deviceA, deviceB);
},
60 * 1000
);
test.skip(
"issue: remove notebook reference from notes that are removed from topic during merge",
async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const id = await deviceA.notebooks.add({
title: "Notebook 1",
topics: ["Topic 1"],
});
await syncAndWait(deviceA, deviceB, false);
expect(deviceB.notebooks.notebook(id)).toBeDefined();
const noteA = await deviceA.notes.add({ title: "Note 1" });
await deviceA.notes.move({ id, topic: "Topic 1" }, noteA);
expect(
deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
await delay(2000);
const noteB = await deviceB.notes.add({ title: "Note 2" });
await deviceB.notes.move({ id, topic: "Topic 1" }, noteB);
expect(
deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
await syncAndWait(deviceB, deviceA, false);
expect(
deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
expect(
deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
expect(deviceA.notes.note(noteA).data.notebooks).toHaveLength(0);
await cleanup(deviceA, deviceB);
},
60 * 1000
);
/**
*
* @param {string} id
* @returns {Promise<Database>}
*/
async function initializeDevice(id, capabilities = []) {
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
return {
type,
result: capabilities.indexOf(type) > -1,
};
});
const device = new Database(new NodeStorageInterface(), EventSource, FS);
device.host({
API_HOST: "http://192.168.10.29:5264",
AUTH_HOST: "http://192.168.10.29:8264",
SSE_HOST: "http://192.168.10.29:7264",
ISSUES_HOST: "http://192.168.10.29:2624",
SUBSCRIPTIONS_HOST: "http://192.168.10.29:9264",
});
await device.init(id);
await device.user.login("enkaboot@gmail.com", process.env.PASSWORD);
await device.user.resetUser(false);
device.eventManager.subscribe(
EVENTS.databaseSyncRequested,
async (full, force) => {
await device.sync(full, force);
}
);
await syncAndWait(device, device);
return device;
}
async function cleanup(...devices) {
for (let device of devices) {
await device.user.logout();
device.eventManager.unsubscribeAll();
}
EV.unsubscribeAll();
}
/**
*
* @param {Database} device
* @returns
*/
// function waitForSyncCompleted(device) {
// return new Promise((resolve) =>
// device.eventManager.subscribe(EVENTS.syncCompleted, () => {
// resolve();
// })
// );
// }
/**
*
* @param {Database} deviceA
* @param {Database} deviceB
* @returns
*/
function syncAndWait(deviceA, deviceB, force = false) {
return new Promise((resolve) => {
const ref = deviceB.eventManager.subscribe(EVENTS.syncCompleted, () => {
ref.unsubscribe();
resolve();
});
deviceA.sync(true, force);
});
}
class NoErrorThrownError extends Error {}
/**
*
* @param {Function} call
* @returns {Promise<Error>}
*/
async function withError(call) {
try {
await call();
throw new NoErrorThrownError();
} catch (error) {
return error;
}
}

View File

@@ -0,0 +1,375 @@
import Database from "../../index";
import { NodeStorageInterface } from "../../../__mocks__/node-storage.mock";
import FS from "../../../__mocks__/fs.mock";
import { CHECK_IDS, EV, EVENTS } from "../../../common";
import EventSource from "eventsource";
import { delay } from "../../../__tests__/utils";
jest.setTimeout(100 * 1000);
test("case 1: device A & B should only download the changes from device C (no uploading)", async () => {
const types = [];
function onSyncProgress({ type }) {
types.push(type);
}
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
deviceA.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
deviceB.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
const deviceC = await initializeDevice("deviceC");
await deviceC.notes.add({ title: "new note 1" });
await syncAndWait(deviceC, deviceC);
expect(types.every((t) => t === "download")).toBe(true);
await cleanup(deviceA, deviceB, deviceC);
});
test("case 3: Device A & B have unsynced changes but server has nothing", async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const note1Id = await deviceA.notes.add({
title: "Test note from device A",
});
const note2Id = await deviceB.notes.add({
title: "Test note from device B",
});
await syncAndWait(deviceA, deviceB);
expect(deviceA.notes.note(note2Id)).toBeTruthy();
expect(deviceB.notes.note(note1Id)).toBeTruthy();
expect(deviceA.notes.note(note1Id)).toBeTruthy();
expect(deviceB.notes.note(note2Id)).toBeTruthy();
await cleanup(deviceA, deviceA);
});
// test(
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
// async () => {
// const deviceA = await initializeDevice("deviceA");
// const deviceB = await initializeDevice("deviceB");
// const unsyncedNoteIds = [];
// for (let i = 0; i < 10; ++i) {
// const id = await deviceA.notes.add({
// title: `Test note ${i} from device A`,
// });
// unsyncedNoteIds.push(id);
// }
// const half = unsyncedNoteIds.length / 2 + 1;
// deviceA.eventManager.subscribe(
// EVENTS.syncProgress,
// async ({ type, current }) => {
// if (type === "upload" && current === half) {
// await deviceA.syncer.stop();
// }
// }
// );
// await expect(deviceA.sync(true)).rejects.toThrow();
// let syncedNoteIds = [];
// for (let i = 0; i < unsyncedNoteIds.length; ++i) {
// const expectedNoteId = unsyncedNoteIds[i];
// if (deviceB.notes.note(expectedNoteId))
// syncedNoteIds.push(expectedNoteId);
// }
// expect(
// syncedNoteIds.length === half - 1 || syncedNoteIds.length === half
// ).toBe(true);
// const deviceBNoteId = await deviceB.notes.add({
// title: "Test note of case 4 from device B",
// });
// await deviceB.sync(true);
// await syncAndWait(deviceA, deviceB);
// expect(deviceA.notes.note(deviceBNoteId)).toBeTruthy();
// expect(
// unsyncedNoteIds
// .map((id) => !!deviceB.notes.note(id))
// .every((res) => res === true)
// ).toBe(true);
// await cleanup(deviceA, deviceB);
// },
//
// );
// test.only(
// "case 5: Device A's sync is interrupted halfway and Device B makes changes on the same note's content that didn't get synced on Device A due to interruption.",
// async () => {
// const deviceA = await initializeDevice("deviceA");
// const deviceB = await initializeDevice("deviceB");
// const noteIds = [];
// for (let i = 0; i < 10; ++i) {
// const id = await deviceA.notes.add({
// content: {
// type: "tiptap",
// data: `<p>deviceA=true</p>`,
// },
// });
// noteIds.push(id);
// }
// await deviceA.sync(true);
// await deviceB.sync(true);
// const unsyncedNoteIds = [];
// for (let id of noteIds) {
// const noteId = await deviceA.notes.add({
// id,
// content: {
// type: "tiptap",
// data: `<p>deviceA=true+changed=true</p>`,
// },
// });
// unsyncedNoteIds.push(noteId);
// }
// deviceA.eventManager.subscribe(
// EVENTS.syncProgress,
// async ({ type, total, current }) => {
// const half = total / 2 + 1;
// if (type === "upload" && current === half) {
// await deviceA.syncer.stop();
// }
// }
// );
// await expect(deviceA.sync(true)).rejects.toThrow();
// await delay(10 * 1000);
// for (let id of unsyncedNoteIds) {
// await deviceB.notes.add({
// id,
// content: {
// type: "tiptap",
// data: "<p>changes from device B</p>",
// },
// });
// }
// const error = await withError(async () => {
// await deviceB.sync(true);
// await deviceA.sync(true);
// });
// expect(error).not.toBeInstanceOf(NoErrorThrownError);
// expect(error.message.includes("Merge")).toBeTruthy();
// await cleanup(deviceA, deviceB);
// },
//
// );
test("issue: running force sync from device A makes device B always download everything", async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
await syncAndWait(deviceA, deviceB, true);
const handler = jest.fn();
deviceB.eventManager.subscribe(EVENTS.syncProgress, handler);
await deviceB.sync(true);
expect(handler).not.toHaveBeenCalled();
await cleanup(deviceB);
});
test("issue: colors are not properly created if multiple notes are synced together", async () => {
const deviceA = await initializeDevice("deviceA", [CHECK_IDS.noteColor]);
const deviceB = await initializeDevice("deviceB", [CHECK_IDS.noteColor]);
const noteIds = [];
for (let i = 0; i < 3; ++i) {
const id = await deviceA.notes.add({
content: {
type: "tiptap",
data: `<p>deviceA=true</p>`,
},
});
noteIds.push(id);
}
await syncAndWait(deviceA, deviceB);
for (let noteId of noteIds) {
await deviceA.notes.note(noteId).color("purple");
expect(deviceB.notes.note(noteId)).toBeTruthy();
expect(deviceB.notes.note(noteId).data.color).toBeUndefined();
}
await syncAndWait(deviceA, deviceB);
await delay(2000);
const purpleColor = deviceB.colors.tag("purple");
console.log(noteIds, purpleColor.noteIds);
expect(noteIds.every((id) => purpleColor.noteIds.indexOf(id) > -1)).toBe(
true
);
await cleanup(deviceA, deviceB);
});
test("issue: new topic on device A gets replaced by the new topic on device B", async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const id = await deviceA.notebooks.add({ title: "Notebook 1" });
await syncAndWait(deviceA, deviceB, false);
expect(deviceB.notebooks.notebook(id)).toBeDefined();
await deviceA.notebooks.notebook(id).topics.add("Topic 1");
// to create a conflict
await delay(1500);
await deviceB.notebooks.notebook(id).topics.add("Topic 2");
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
await syncAndWait(deviceA, deviceB, false);
await delay(1000);
await syncAndWait(deviceB, deviceB, false);
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
expect(deviceA.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
await cleanup(deviceA, deviceB);
});
test("issue: remove notebook reference from notes that are removed from topic during merge", async () => {
const deviceA = await initializeDevice("deviceA");
const deviceB = await initializeDevice("deviceB");
const id = await deviceA.notebooks.add({
title: "Notebook 1",
topics: ["Topic 1"],
});
await syncAndWait(deviceA, deviceB, false);
expect(deviceB.notebooks.notebook(id)).toBeDefined();
const noteA = await deviceA.notes.add({ title: "Note 1" });
await deviceA.notes.move({ id, topic: "Topic 1" }, noteA);
expect(
deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
await delay(2000);
const noteB = await deviceB.notes.add({ title: "Note 2" });
await deviceB.notes.move({ id, topic: "Topic 1" }, noteB);
expect(
deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
await syncAndWait(deviceB, deviceA, false);
expect(
deviceA.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
expect(
deviceB.notebooks.notebook(id).topics.topic("Topic 1").totalNotes
).toBe(1);
expect(deviceA.notes.note(noteA).data.notebooks).toHaveLength(0);
await cleanup(deviceA, deviceB);
});
/**
*
* @param {string} id
* @returns {Promise<Database>}
*/
async function initializeDevice(id, capabilities = []) {
console.time("Init device");
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
return {
type,
result: capabilities.indexOf(type) > -1,
};
});
const device = new Database(new NodeStorageInterface(), EventSource, FS);
// device.host({
// API_HOST: "http://192.168.10.29:5264",
// AUTH_HOST: "http://192.168.10.29:8264",
// SSE_HOST: "http://192.168.10.29:7264",
// ISSUES_HOST: "http://192.168.10.29:2624",
// SUBSCRIPTIONS_HOST: "http://192.168.10.29:9264",
// });
await device.init(id);
await device.user.login(
process.env.EMAIL,
process.env.PASSWORD,
process.env.HASHED_PASSWORD
);
await device.user.resetUser(false);
device.eventManager.subscribe(
EVENTS.databaseSyncRequested,
async (full, force) => {
await device.sync(full, force);
}
);
console.timeEnd("Init device");
return device;
}
async function cleanup(...devices) {
for (let device of devices) {
await device.user.logout();
device.eventManager.unsubscribeAll();
}
EV.unsubscribeAll();
}
/**
*
* @param {Database} deviceA
* @param {Database} deviceB
* @returns
*/
function syncAndWait(deviceA, deviceB, force = false) {
return new Promise((resolve) => {
const ref = deviceB.eventManager.subscribe(EVENTS.syncCompleted, () => {
ref.unsubscribe();
resolve();
});
deviceA.sync(true, force);
});
}

View File

@@ -100,19 +100,5 @@ class Collector {
...(await this._serialize(item)),
};
}
filter(data, predicate) {
const arrays = ["notes", "notebooks", "content", "attachments", "settings"];
const newData = {};
for (let array of arrays) {
if (!data[array]) continue;
this.logger.info(`Filtering from ${data[array].length} ${array}`);
newData[array] = data[array].filter(predicate);
this.logger.info(
`Length after filtering ${array}: ${newData[array].length}`
);
}
return newData;
}
}
export default Collector;