mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 14:39:34 +01:00
test: improve overall test coverage
This commit is contained in:
3
packages/core/__e2e__/__snapshots__/offers.test.js.snap
Normal file
3
packages/core/__e2e__/__snapshots__/offers.test.js.snap
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`get offer code: offer-code 1`] = `"123"`;
|
||||||
77
packages/core/__e2e__/monographs.test.js
Normal file
77
packages/core/__e2e__/monographs.test.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { databaseTest, noteTest, StorageInterface } from "../__tests__/utils";
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email: process.env.EMAIL,
|
||||||
|
password: process.env.PASSWORD,
|
||||||
|
hashedPassword: process.env.HASHED_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.setTimeout(15 * 1000);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = await databaseTest();
|
||||||
|
await db.monographs.init();
|
||||||
|
|
||||||
|
for (const id of db.monographs.monographs) {
|
||||||
|
await db.monographs.unpublish(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
// test("get monographs", () =>
|
||||||
|
// databaseTest().then(async (db) => {
|
||||||
|
// await db.user.login(user.email, user.password, user.hashedPassword);
|
||||||
|
|
||||||
|
// await db.monographs.init();
|
||||||
|
|
||||||
|
// expect(db.monographs.all).toBeGreaterThanOrEqual(0);
|
||||||
|
// }));
|
||||||
|
|
||||||
|
test("publish a monograph", () =>
|
||||||
|
noteTest().then(async ({ db, id }) => {
|
||||||
|
await db.user.login(user.email, user.password, user.hashedPassword);
|
||||||
|
await db.monographs.init();
|
||||||
|
|
||||||
|
const monographId = await db.monographs.publish(id);
|
||||||
|
|
||||||
|
expect(db.monographs.all.find((m) => m.id === id)).toBeDefined();
|
||||||
|
|
||||||
|
const monograph = await db.monographs.get(monographId);
|
||||||
|
const note = db.notes.note(id);
|
||||||
|
expect(monograph.id).toBe(monographId);
|
||||||
|
expect(monograph.title).toBe(note.title);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("update a published monograph", () =>
|
||||||
|
noteTest().then(async ({ db, id }) => {
|
||||||
|
await db.user.login(user.email, user.password, user.hashedPassword);
|
||||||
|
await db.monographs.init();
|
||||||
|
|
||||||
|
const monographId = await db.monographs.publish(id);
|
||||||
|
let monograph = await db.monographs.get(monographId);
|
||||||
|
const note = db.notes.note(id);
|
||||||
|
expect(monograph.title).toBe(note.title);
|
||||||
|
|
||||||
|
const editedTitle = "EDITED TITLE OF MY NOTE!";
|
||||||
|
await db.notes.add({ id, title: editedTitle });
|
||||||
|
await db.monographs.publish(id);
|
||||||
|
monograph = await db.monographs.get(monographId);
|
||||||
|
expect(monograph.title).toBe(editedTitle);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("unpublish a monograph", () =>
|
||||||
|
noteTest().then(async ({ db, id }) => {
|
||||||
|
await db.user.login(user.email, user.password, user.hashedPassword);
|
||||||
|
await db.monographs.init();
|
||||||
|
|
||||||
|
await db.monographs.publish(id);
|
||||||
|
expect(db.monographs.all.find((m) => m.id === id)).toBeDefined();
|
||||||
|
|
||||||
|
await db.monographs.unpublish(id);
|
||||||
|
expect(db.monographs.all.find((m) => m.id === id)).toBeUndefined();
|
||||||
|
}));
|
||||||
18
packages/core/__e2e__/offers.test.js
Normal file
18
packages/core/__e2e__/offers.test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import hosts from "../utils/constants";
|
||||||
|
import Offers from "../api/offers";
|
||||||
|
|
||||||
|
test("get offer code", async () => {
|
||||||
|
const offers = new Offers();
|
||||||
|
hosts.SUBSCRIPTIONS_HOST = "https://subscriptions.streetwriters.co";
|
||||||
|
expect(await offers.getCode("TESTOFFER", "android")).toMatchSnapshot(
|
||||||
|
"offer-code"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get invalid offer code", async () => {
|
||||||
|
const offers = new Offers();
|
||||||
|
hosts.SUBSCRIPTIONS_HOST = "https://subscriptions.streetwriters.co";
|
||||||
|
await expect(() => offers.getCode("INVALIDOFFER", "android")).rejects.toThrow(
|
||||||
|
/Not found/i
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -7,18 +7,10 @@ const user = {
|
|||||||
hashedPassword: process.env.HASHED_PASSWORD,
|
hashedPassword: process.env.HASHED_PASSWORD,
|
||||||
};
|
};
|
||||||
|
|
||||||
test.skip("refresh token concurrently", async () => {
|
test("refresh token concurrently", async () => {
|
||||||
const db = new DB(StorageInterface);
|
const db = new DB(StorageInterface);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
db.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",
|
|
||||||
SUBSCRIPTIONS_HOST: "http://192.168.10.29:9264",
|
|
||||||
ISSUES_HOST: "http://192.168.10.29:2624",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
db.user.login(user.email, user.password, user.hashedPassword)
|
db.user.login(user.email, user.password, user.hashedPassword)
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
@@ -36,18 +28,10 @@ test.skip("refresh token concurrently", async () => {
|
|||||||
).toHaveLength(4);
|
).toHaveLength(4);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
test.skip("refresh token using the same refresh_token multiple time", async () => {
|
test("refresh token using the same refresh_token multiple time", async () => {
|
||||||
const db = new DB(StorageInterface);
|
const db = new DB(StorageInterface);
|
||||||
await db.init();
|
await db.init();
|
||||||
|
|
||||||
db.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",
|
|
||||||
SUBSCRIPTIONS_HOST: "http://192.168.10.29:9264",
|
|
||||||
ISSUES_HOST: "http://192.168.10.29:2624",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
db.user.login(user.email, user.password, user.hashedPassword)
|
db.user.login(user.email, user.password, user.hashedPassword)
|
||||||
).resolves.not.toThrow();
|
).resolves.not.toThrow();
|
||||||
|
|||||||
72
packages/core/__e2e__/user-manager.test.js
Normal file
72
packages/core/__e2e__/user-manager.test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { databaseTest } from "../__tests__/utils";
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email: process.env.EMAIL,
|
||||||
|
password: process.env.PASSWORD,
|
||||||
|
hashed: process.env.HASHED_PASSWORD,
|
||||||
|
};
|
||||||
|
|
||||||
|
// test("signup user and check for token", async () => {
|
||||||
|
// const db = new DB(StorageInterface);
|
||||||
|
// const usermanager = new UserManager(db);
|
||||||
|
|
||||||
|
// await expect(
|
||||||
|
// usermanager.signup(user.email, user.password)
|
||||||
|
// ).resolves.not.toThrow();
|
||||||
|
|
||||||
|
// await expect(usermanager.tokenManager.getToken()).resolves.toBeDefined();
|
||||||
|
// }, 30000);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"login user and check for token",
|
||||||
|
() =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
await expect(
|
||||||
|
db.user.login(user.email, user.password, user.hashed)
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
await expect(db.user.tokenManager.getToken()).resolves.toBeDefined();
|
||||||
|
}),
|
||||||
|
30000
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"login user and get user data",
|
||||||
|
() =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
await db.user.login(user.email, user.password, user.hashed);
|
||||||
|
|
||||||
|
const userData = await db.user.getUser();
|
||||||
|
expect(userData.email).toBe(user.email);
|
||||||
|
}),
|
||||||
|
30000
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"login user and logout user",
|
||||||
|
() =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
await db.user.login(user.email, user.password, user.hashed);
|
||||||
|
|
||||||
|
await expect(db.user.logout()).resolves.not.toThrow();
|
||||||
|
}),
|
||||||
|
30000
|
||||||
|
);
|
||||||
|
|
||||||
|
// test("login user and delete user", async () => {
|
||||||
|
// const db = new DB(StorageInterface);
|
||||||
|
// const usermanager = new UserManager(db);
|
||||||
|
|
||||||
|
// await usermanager.login(user.email, user.password, user.hashed);
|
||||||
|
|
||||||
|
// await expect(usermanager.deleteUser(user.password)).resolves.toBe(true);
|
||||||
|
// }, 30000);
|
||||||
|
|
||||||
|
// test("login user and get user sessions", async () => {
|
||||||
|
// const db = new DB(StorageInterface);
|
||||||
|
// const usermanager = new UserManager(db);
|
||||||
|
|
||||||
|
// await usermanager.login(user.email, user.password);
|
||||||
|
|
||||||
|
// await usermanager.getSessions();
|
||||||
|
// }, 30000);
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import UserManager from "../api/user-manager";
|
|
||||||
import DB from "../api";
|
|
||||||
import StorageInterface from "../__mocks__/storage.mock";
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
email: process.env.EMAIL,
|
|
||||||
password: process.env.PASSWORD,
|
|
||||||
};
|
|
||||||
|
|
||||||
test.skip("signup user and check for token", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usermanager.signup(user.email, user.password)
|
|
||||||
).resolves.not.toThrow();
|
|
||||||
|
|
||||||
await expect(usermanager.tokenManager.getToken()).resolves.toBeDefined();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
test.skip("login user and check for token", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
usermanager.login(user.email, user.password)
|
|
||||||
).resolves.not.toThrow();
|
|
||||||
|
|
||||||
await expect(usermanager.tokenManager.getToken()).resolves.toBeDefined();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
test.skip("login user and get user data", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await usermanager.login(user.email, user.password);
|
|
||||||
|
|
||||||
const userData = await usermanager.getUser();
|
|
||||||
expect(userData.email).toBe(process.env.EMAIL);
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
test.skip("login user and logout user", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await usermanager.login(user.email, user.password);
|
|
||||||
|
|
||||||
await expect(usermanager.logout()).resolves.not.toThrow();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
test.skip("login user and delete user", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await usermanager.login(user.email, user.password);
|
|
||||||
|
|
||||||
await expect(usermanager.deleteUser(user.password)).resolves.toBe(true);
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
test("login user and get user sessions", async () => {
|
|
||||||
const db = new DB(StorageInterface);
|
|
||||||
const usermanager = new UserManager(db);
|
|
||||||
|
|
||||||
await usermanager.login(user.email, user.password);
|
|
||||||
|
|
||||||
await usermanager.getSessions();
|
|
||||||
}, 30000);
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { databaseTest } from "./utils";
|
import { databaseTest, notebookTest, StorageInterface } from "./utils";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
test("settings' dateModified should not update on init", () =>
|
test("settings' dateModified should not update on init", () =>
|
||||||
databaseTest().then(async (db) => {
|
databaseTest().then(async (db) => {
|
||||||
@@ -29,3 +33,75 @@ test("tag alias should update if aliases in settings update", () =>
|
|||||||
});
|
});
|
||||||
expect(db.tags.tag(tag.id).alias).toBe("hello232");
|
expect(db.tags.tag(tag.id).alias).toBe("hello232");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test("save group options", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const groupOptions = {
|
||||||
|
groupBy: "abc",
|
||||||
|
sortBy: "dateCreated",
|
||||||
|
sortDirection: "asc",
|
||||||
|
};
|
||||||
|
await db.settings.setGroupOptions("home", groupOptions);
|
||||||
|
expect(db.settings.getGroupOptions("home")).toMatchObject(groupOptions);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("save toolbar config", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const toolbarConfig = {
|
||||||
|
preset: "custom",
|
||||||
|
config: ["bold", "italic"],
|
||||||
|
};
|
||||||
|
await db.settings.setToolbarConfig("mobile", toolbarConfig);
|
||||||
|
expect(db.settings.getToolbarConfig("mobile")).toMatchObject(toolbarConfig);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("pinning an invalid item should throw", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
await expect(() => db.settings.pin("lolo", {})).rejects.toThrow(
|
||||||
|
/item cannot be pinned/i
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("pin a notebook", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
await db.settings.pin("notebook", { id });
|
||||||
|
expect(db.settings.pins).toHaveLength(1);
|
||||||
|
expect(db.settings.pins[0].id).toBe(id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("pin an already pinned notebook", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
await db.settings.pin("notebook", { id });
|
||||||
|
await db.settings.pin("notebook", { id });
|
||||||
|
|
||||||
|
expect(db.settings.pins).toHaveLength(1);
|
||||||
|
expect(db.settings.pins[0].id).toBe(id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("pin a topic", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
const notebook = db.notebooks.notebook(id)._notebook;
|
||||||
|
const topic = notebook.topics[0];
|
||||||
|
await db.settings.pin("topic", { id: topic.id, notebookId: id });
|
||||||
|
expect(db.settings.pins).toHaveLength(1);
|
||||||
|
expect(db.settings.pins[0].id).toBe(topic.id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("pin a tag", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const tag = await db.tags.add("HELLO!");
|
||||||
|
await db.settings.pin("tag", { id: tag.id });
|
||||||
|
expect(db.settings.pins).toHaveLength(1);
|
||||||
|
expect(db.settings.pins[0].id).toBe(tag.id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("unpin a pinned item", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const tag = await db.tags.add("HELLO!");
|
||||||
|
await db.settings.pin("tag", { id: tag.id });
|
||||||
|
expect(db.settings.pins).toHaveLength(1);
|
||||||
|
expect(db.settings.pins[0].id).toBe(tag.id);
|
||||||
|
|
||||||
|
await db.settings.unpin(tag.id);
|
||||||
|
expect(db.settings.pins).toHaveLength(0);
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
import { enableFetchMocks } from "jest-fetch-mock";
|
|
||||||
import { StorageInterface, databaseTest } from "./utils";
|
|
||||||
|
|
||||||
const SUCCESS_LOGIN_RESPONSE = {
|
|
||||||
access_token: "access_token",
|
|
||||||
refresh_token: "refresh_token",
|
|
||||||
|
|
||||||
scope: "sync",
|
|
||||||
expires_in: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SUCCESS_USER_RESPONSE = {
|
|
||||||
id: "0",
|
|
||||||
email: process.env.EMAIL,
|
|
||||||
salt: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
function mock(expiry = 3600) {
|
|
||||||
fetch
|
|
||||||
.mockResponseOnce(
|
|
||||||
JSON.stringify({ ...SUCCESS_LOGIN_RESPONSE, expires_in: expiry }),
|
|
||||||
{
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.mockResponseOnce(JSON.stringify(SUCCESS_USER_RESPONSE), {
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
enableFetchMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fetch.resetMocks();
|
|
||||||
StorageInterface.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("no user should be returned when not logged in", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
expect(await db.user.getUser()).toBeUndefined();
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("undefined user should not be set", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
await db.user.setUser();
|
|
||||||
expect(await db.user.getUser()).toBeUndefined();
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("login user", async () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
mock();
|
|
||||||
await db.user.login("myuser", "mylogin", true, "mylogin");
|
|
||||||
const dbuser = await db.user.getUser();
|
|
||||||
expect(dbuser.email).toBe(SUCCESS_USER_RESPONSE.email);
|
|
||||||
expect(dbuser.id).toBe(SUCCESS_USER_RESPONSE.id);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("login user with wrong password", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
fetch.mockResponseOnce(
|
|
||||||
JSON.stringify({
|
|
||||||
error_description: "Username or password is incorrect.",
|
|
||||||
}),
|
|
||||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
await expect(
|
|
||||||
db.user.login("myuser", "wrongpassword", true, "wrongpassword")
|
|
||||||
).rejects.toThrow(/Username or password is incorrect./);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("failed login with unknown error", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
fetch.mockResponseOnce(JSON.stringify({}), { status: 400 });
|
|
||||||
await expect(
|
|
||||||
db.user.login("myuser", "wrongpassword", true, "wrongpassword")
|
|
||||||
).rejects.toThrow(/Request failed with status code: /);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("signup user", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
fetch.mockResponseOnce(undefined, { status: 200 });
|
|
||||||
mock();
|
|
||||||
await db.user.signup(SUCCESS_USER_RESPONSE.email, "password");
|
|
||||||
const dbuser = await db.user.getUser();
|
|
||||||
expect(dbuser.email).toBe(SUCCESS_USER_RESPONSE.email);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("logout user", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
mock();
|
|
||||||
await db.user.login("myuser", "mylogin", true, "mylogin");
|
|
||||||
const dbuser = await db.user.getUser();
|
|
||||||
expect(dbuser.email).toBe(SUCCESS_USER_RESPONSE.email);
|
|
||||||
await db.user.logout();
|
|
||||||
expect(await db.user.getUser()).toBeUndefined();
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("refresh user's token", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
mock();
|
|
||||||
await db.user.login(
|
|
||||||
SUCCESS_USER_RESPONSE.email,
|
|
||||||
"mylogin",
|
|
||||||
true,
|
|
||||||
"mylogin"
|
|
||||||
);
|
|
||||||
const token = await db.user.tokenManager.getToken();
|
|
||||||
await db.user.tokenManager.saveToken({ ...token, expires_in: -2000 });
|
|
||||||
fetch.mockResponseOnce(
|
|
||||||
JSON.stringify({
|
|
||||||
...SUCCESS_LOGIN_RESPONSE,
|
|
||||||
access_token: "new_token",
|
|
||||||
refresh_token: "new_refresh_token",
|
|
||||||
expires_in: 3600,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const { access_token, refresh_token } =
|
|
||||||
await db.user.tokenManager.getToken();
|
|
||||||
expect(refresh_token).toBe("new_refresh_token");
|
|
||||||
expect(access_token).toBe("new_token");
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("refresh user's token when its not expired", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
mock();
|
|
||||||
await db.user.login(
|
|
||||||
SUCCESS_USER_RESPONSE.email,
|
|
||||||
"mylogin",
|
|
||||||
true,
|
|
||||||
"mylogin"
|
|
||||||
);
|
|
||||||
expect(await db.user.tokenManager.getAccessToken()).toBe("access_token");
|
|
||||||
const dbuser = await db.user.getUser();
|
|
||||||
expect(dbuser.email).toBe(SUCCESS_USER_RESPONSE.email);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test("refresh token for non existent user should do nothing", () =>
|
|
||||||
databaseTest().then(async (db) => {
|
|
||||||
fetch.mockResponseOnce(JSON.stringify(SUCCESS_LOGIN_RESPONSE));
|
|
||||||
expect(await db.user.tokenManager.getAccessToken()).toBeUndefined();
|
|
||||||
const dbuser = await db.user.getUser();
|
|
||||||
expect(dbuser).toBeUndefined();
|
|
||||||
}));
|
|
||||||
13
packages/core/api/__tests__/__snapshots__/debug.test.js.snap
Normal file
13
packages/core/api/__tests__/__snapshots__/debug.test.js.snap
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`strip note with content: stripped-note-with-content 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"note\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123,\\"additionalData\\":{\\"content\\":\\"{\\\\\\"title\\\\\\":false,\\\\\\"description\\\\\\":false,\\\\\\"headline\\\\\\":false,\\\\\\"colored\\\\\\":false,\\\\\\"type\\\\\\":\\\\\\"tiptap\\\\\\",\\\\\\"id\\\\\\":\\\\\\"hello\\\\\\",\\\\\\"dateModified\\\\\\":123,\\\\\\"dateEdited\\\\\\":123,\\\\\\"dateCreated\\\\\\":123}\\"}}"`;
|
||||||
|
|
||||||
|
exports[`strip note: stripped-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"note\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
|
||||||
|
|
||||||
|
exports[`strip notebook: stripped-notebook 1`] = `"{\\"title\\":true,\\"description\\":true,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"notebook\\",\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123,\\"additionalData\\":[{\\"type\\":\\"topic\\",\\"id\\":\\"hello\\",\\"notebookId\\":\\"hello23\\",\\"title\\":\\"hello\\",\\"dateCreated\\":123,\\"dateEdited\\":123,\\"notes\\":[],\\"dateModified\\":123}]}"`;
|
||||||
|
|
||||||
|
exports[`strip tag: stripped-tag 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"tag\\",\\"noteIds\\":[],\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
|
||||||
|
|
||||||
|
exports[`strip topic: stripped-topic 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":false,\\"colored\\":false,\\"type\\":\\"topic\\",\\"notes\\":[],\\"id\\":\\"hello\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateCreated\\":123}"`;
|
||||||
|
|
||||||
|
exports[`strip trashed note: stripped-trashed-note 1`] = `"{\\"title\\":true,\\"description\\":false,\\"headline\\":true,\\"colored\\":false,\\"type\\":\\"trash\\",\\"tags\\":[],\\"id\\":\\"hello\\",\\"contentId\\":\\"hello2\\",\\"dateModified\\":123,\\"dateEdited\\":123,\\"dateDeleted\\":123,\\"dateCreated\\":123}"`;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`get android pricing tier get monthly android tier: monthly-android-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 78,
|
||||||
|
"sku": "com.streetwriters.notesnook.sub.mo.tier3",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get android pricing tier get yearly android tier: yearly-android-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 80,
|
||||||
|
"sku": "com.streetwriters.notesnook.sub.yr.tier3",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get ios pricing tier get monthly ios tier: monthly-ios-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 78,
|
||||||
|
"sku": "com.streetwriters.notesnook.sub.mo.tier3",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get ios pricing tier get yearly ios tier: yearly-ios-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 80,
|
||||||
|
"sku": "com.streetwriters.notesnook.sub.yr.tier3",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get monthly price: monthly-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 78,
|
||||||
|
"price": 0.99,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get undefined price: monthly-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 78,
|
||||||
|
"price": 0.99,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get web pricing tier get monthly web tier: monthly-web-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 78,
|
||||||
|
"sku": "763943",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get web pricing tier get yearly web tier: yearly-web-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 80,
|
||||||
|
"sku": "763945",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`get yearly price: yearly-pricing 1`] = `
|
||||||
|
Object {
|
||||||
|
"country": "Pakistan",
|
||||||
|
"countryCode": "PK",
|
||||||
|
"discount": 80,
|
||||||
|
"price": 9.99,
|
||||||
|
}
|
||||||
|
`;
|
||||||
125
packages/core/api/__tests__/debug.test.js
Normal file
125
packages/core/api/__tests__/debug.test.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import Debug from "../debug";
|
||||||
|
import { noteTest, notebookTest, databaseTest } from "../../__tests__/utils";
|
||||||
|
import { enableFetchMocks, disableFetchMocks } from "jest-fetch-mock";
|
||||||
|
|
||||||
|
test("strip empty item shouldn't throw", () => {
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(debug.strip()).toBe("{}");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strip note", () =>
|
||||||
|
noteTest().then(({ db, id }) => {
|
||||||
|
const note = db.notes.note(id)._note;
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(debug.strip(normalizeItem(note))).toMatchSnapshot("stripped-note");
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("strip trashed note", () =>
|
||||||
|
noteTest().then(async ({ db, id }) => {
|
||||||
|
await db.notes.delete(id);
|
||||||
|
const note = db.trash.all[0];
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(debug.strip(normalizeItem(note))).toMatchSnapshot(
|
||||||
|
"stripped-trashed-note"
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("strip note with content", () =>
|
||||||
|
noteTest().then(async ({ db, id }) => {
|
||||||
|
const note = db.notes.note(id)._note;
|
||||||
|
const debug = new Debug();
|
||||||
|
|
||||||
|
const content = await db.content.raw(note.contentId);
|
||||||
|
note.additionalData = {
|
||||||
|
content: db.debug.strip(normalizeItem(content)),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(debug.strip(normalizeItem(note))).toMatchSnapshot(
|
||||||
|
"stripped-note-with-content"
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("strip notebook", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
const notebook = db.notebooks.notebook(id)._notebook;
|
||||||
|
const debug = new Debug();
|
||||||
|
notebook.additionalData = notebook.topics.map((topic) =>
|
||||||
|
normalizeItem(topic)
|
||||||
|
);
|
||||||
|
expect(debug.strip(normalizeItem(notebook))).toMatchSnapshot(
|
||||||
|
"stripped-notebook"
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("strip topic", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
const notebook = db.notebooks.notebook(id)._notebook;
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(debug.strip(normalizeItem(notebook.topics[0]))).toMatchSnapshot(
|
||||||
|
"stripped-topic"
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("strip tag", () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const tag = await db.tags.add("Hello tag");
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(debug.strip(normalizeItem(tag))).toMatchSnapshot("stripped-tag");
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("reporting empty issue should return undefined", async () => {
|
||||||
|
const debug = new Debug();
|
||||||
|
expect(await debug.report()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUCCESS_REPORT_RESPONSE = {
|
||||||
|
url: "https://reported/",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("reporting issue should return issue url", async () => {
|
||||||
|
enableFetchMocks();
|
||||||
|
|
||||||
|
const debug = new Debug();
|
||||||
|
|
||||||
|
fetch.mockResponseOnce(JSON.stringify(SUCCESS_REPORT_RESPONSE), {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await debug.report({
|
||||||
|
title: "I am title",
|
||||||
|
body: "I am body",
|
||||||
|
userId: "anything",
|
||||||
|
})
|
||||||
|
).toBe(SUCCESS_REPORT_RESPONSE.url);
|
||||||
|
|
||||||
|
disableFetchMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reporting invalid issue should return undefined", async () => {
|
||||||
|
enableFetchMocks();
|
||||||
|
|
||||||
|
const debug = new Debug();
|
||||||
|
|
||||||
|
fetch.mockResponseOnce(
|
||||||
|
JSON.stringify({
|
||||||
|
error_description: "Invalid issue.",
|
||||||
|
}),
|
||||||
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await debug.report({})).toBeUndefined();
|
||||||
|
|
||||||
|
disableFetchMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizeItem(item) {
|
||||||
|
item.id = "hello";
|
||||||
|
item.notebookId = "hello23";
|
||||||
|
item.dateModified = 123;
|
||||||
|
item.dateEdited = 123;
|
||||||
|
item.dateCreated = 123;
|
||||||
|
if (item.dateDeleted) item.dateDeleted = 123;
|
||||||
|
if (item.contentId) item.contentId = "hello2";
|
||||||
|
return item;
|
||||||
|
}
|
||||||
1
packages/core/api/__tests__/mfa-manager.test.js
Normal file
1
packages/core/api/__tests__/mfa-manager.test.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test.todo("skipped test");
|
||||||
1
packages/core/api/__tests__/migrations.test.js
Normal file
1
packages/core/api/__tests__/migrations.test.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test.todo("skipped test");
|
||||||
1
packages/core/api/__tests__/outbox.test.js
Normal file
1
packages/core/api/__tests__/outbox.test.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test.todo("skipped test");
|
||||||
18
packages/core/api/__tests__/pricing.test.js
Normal file
18
packages/core/api/__tests__/pricing.test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Pricing from "../pricing";
|
||||||
|
|
||||||
|
test.each(["monthly", "yearly", undefined])(`get %s price`, async (period) => {
|
||||||
|
const pricing = new Pricing();
|
||||||
|
const price = await pricing.price(period);
|
||||||
|
expect(price).toMatchSnapshot(`${period || "monthly"}-pricing`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each(["android", "ios", "web"])(`get %s pricing tier`, (platform) => {
|
||||||
|
test.each(["monthly", "yearly"])(
|
||||||
|
`get %s ${platform} tier`,
|
||||||
|
async (period) => {
|
||||||
|
const pricing = new Pricing();
|
||||||
|
const price = await pricing.sku(platform, period);
|
||||||
|
expect(price).toMatchSnapshot(`${period}-${platform}-pricing`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import hosts from "../utils/constants";
|
|||||||
|
|
||||||
export default class Debug {
|
export default class Debug {
|
||||||
strip(item) {
|
strip(item) {
|
||||||
|
if (!item) return "{}";
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
title: !!item.title,
|
title: !!item.title,
|
||||||
description: !!item.description,
|
description: !!item.description,
|
||||||
@@ -9,6 +10,8 @@ export default class Debug {
|
|||||||
colored: !!item.color,
|
colored: !!item.color,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
notebooks: item.notebooks,
|
notebooks: item.notebooks,
|
||||||
|
notes: item.notes,
|
||||||
|
noteIds: item.noteIds,
|
||||||
tags: item.tags,
|
tags: item.tags,
|
||||||
id: item.id,
|
id: item.id,
|
||||||
contentId: item.contentId,
|
contentId: item.contentId,
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ class Monographs {
|
|||||||
*/
|
*/
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
this._db = db;
|
this._db = db;
|
||||||
this.monographs = [];
|
this.monographs = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deinit() {
|
async deinit() {
|
||||||
this.monographs = [];
|
this.monographs = undefined;
|
||||||
await this._db.storage.write("monographs", this.monographs);
|
await this._db.storage.write("monographs", this.monographs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class Monographs {
|
|||||||
* @returns {boolean} Whether note is published or not.
|
* @returns {boolean} Whether note is published or not.
|
||||||
*/
|
*/
|
||||||
isPublished(noteId) {
|
isPublished(noteId) {
|
||||||
return this.monographs.indexOf(noteId) > -1;
|
return this.monographs && this.monographs.indexOf(noteId) > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +46,8 @@ class Monographs {
|
|||||||
* @returns Monograph Id
|
* @returns Monograph Id
|
||||||
*/
|
*/
|
||||||
monograph(noteId) {
|
monograph(noteId) {
|
||||||
|
if (!this.monographs) return;
|
||||||
|
|
||||||
return this.monographs[this.monographs.indexOf(noteId)];
|
return this.monographs[this.monographs.indexOf(noteId)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ class Monographs {
|
|||||||
* @param {{password: string, selfDestruct: boolean}} opts Publish options
|
* @param {{password: string, selfDestruct: boolean}} opts Publish options
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async publish(noteId, opts) {
|
async publish(noteId, opts = { password: undefined, selfDestruct: false }) {
|
||||||
if (!this.monographs) await this.init();
|
if (!this.monographs) await this.init();
|
||||||
|
|
||||||
let update = !!this.isPublished(noteId);
|
let update = !!this.isPublished(noteId);
|
||||||
@@ -116,8 +118,8 @@ class Monographs {
|
|||||||
const token = await this._db.user.tokenManager.getAccessToken();
|
const token = await this._db.user.tokenManager.getAccessToken();
|
||||||
if (!user || !token) throw new Error("Please login to publish a note.");
|
if (!user || !token) throw new Error("Please login to publish a note.");
|
||||||
|
|
||||||
const note = this._db.notes.note(noteId);
|
// const note = this._db.notes.note(noteId);
|
||||||
if (!note) throw new Error("No such note found.");
|
// if (!note) throw new Error("No such note found.");
|
||||||
|
|
||||||
if (!this.isPublished(noteId))
|
if (!this.isPublished(noteId))
|
||||||
throw new Error("This note is not published.");
|
throw new Error("This note is not published.");
|
||||||
@@ -128,9 +130,15 @@ class Monographs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get all() {
|
get all() {
|
||||||
|
if (!this.monographs) return [];
|
||||||
|
|
||||||
return this._db.notes.all.filter(
|
return this._db.notes.all.filter(
|
||||||
(note) => this.monographs.indexOf(note.id) > -1
|
(note) => this.monographs.indexOf(note.id) > -1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get(monographId) {
|
||||||
|
return await http.get(`${Constants.API_HOST}/monographs/${monographId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default Monographs;
|
export default Monographs;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Pricing {
|
|||||||
* discount: number
|
* discount: number
|
||||||
* }>}
|
* }>}
|
||||||
*/
|
*/
|
||||||
price(period) {
|
price(period = "monthly") {
|
||||||
return http.get(`${BASE_URL}/${period}`);
|
return http.get(`${BASE_URL}/${period}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPinned(id) {
|
isPinned(id) {
|
||||||
return this._settings.pins.findIndex((v) => v.data.id === id) > -1;
|
return !!this._settings.pins.find((v) => v.data.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pins() {
|
get pins() {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
375
packages/core/api/sync/__tests__/sync.test.skip.js
Normal file
375
packages/core/api/sync/__tests__/sync.test.skip.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -100,19 +100,5 @@ class Collector {
|
|||||||
...(await this._serialize(item)),
|
...(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;
|
export default Collector;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "../types";
|
|||||||
import http from "../utils/http";
|
import http from "../utils/http";
|
||||||
import constants from "../utils/constants";
|
import constants from "../utils/constants";
|
||||||
import TokenManager from "./token-manager";
|
import TokenManager from "./token-manager";
|
||||||
import { EV, EVENTS, setUserPersonalizationBytes } from "../common";
|
import { EV, EVENTS } from "../common";
|
||||||
|
|
||||||
const ENDPOINTS = {
|
const ENDPOINTS = {
|
||||||
signup: "/users",
|
signup: "/users",
|
||||||
@@ -44,7 +44,6 @@ class UserManager {
|
|||||||
async init() {
|
async init() {
|
||||||
const user = await this.getUser();
|
const user = await this.getUser();
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
setUserPersonalizationBytes(user.salt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async signup(email, password) {
|
async signup(email, password) {
|
||||||
@@ -58,8 +57,8 @@ class UserManager {
|
|||||||
return await this._login({ email, password, hashedPassword });
|
return await this._login({ email, password, hashedPassword });
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(email, password) {
|
async login(email, password, hashedPassword = undefined) {
|
||||||
return this._login({ email, password });
|
return this._login({ email, password, hashedPassword });
|
||||||
}
|
}
|
||||||
|
|
||||||
async mfaLogin(email, password, { code, method }) {
|
async mfaLogin(email, password, { code, method }) {
|
||||||
@@ -87,7 +86,6 @@ class UserManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const user = await this.fetchUser();
|
const user = await this.fetchUser();
|
||||||
setUserPersonalizationBytes(user.salt);
|
|
||||||
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
|
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
|
||||||
password,
|
password,
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
|
|||||||
@@ -65,15 +65,3 @@ export const EVENTS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CURRENT_DATABASE_VERSION = 5.6;
|
export const CURRENT_DATABASE_VERSION = 5.6;
|
||||||
|
|
||||||
export function setUserPersonalizationBytes(userSalt) {
|
|
||||||
USER_PERSONALIZATION_HASH = new Uint8Array(
|
|
||||||
Buffer.from(userSalt, "base64")
|
|
||||||
).slice(0, 8);
|
|
||||||
if (
|
|
||||||
!USER_PERSONALIZATION_HASH.length ||
|
|
||||||
!USER_PERSONALIZATION_HASH.byteLength
|
|
||||||
)
|
|
||||||
USER_PERSONALIZATION_HASH = undefined;
|
|
||||||
}
|
|
||||||
export var USER_PERSONALIZATION_HASH = null;
|
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import { extractHostname } from "./hostname";
|
import { extractHostname } from "./hostname";
|
||||||
|
|
||||||
|
function isProduction() {
|
||||||
|
return (
|
||||||
|
process.env.NODE_ENV === "production" || process.env.NODE_ENV === "test"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const hosts = {
|
const hosts = {
|
||||||
API_HOST:
|
API_HOST: isProduction()
|
||||||
process.env.NODE_ENV === "production"
|
? "https://api.notesnook.com"
|
||||||
? "https://api.notesnook.com"
|
: "http://localhost:5264",
|
||||||
: "http://localhost:5264",
|
AUTH_HOST: isProduction()
|
||||||
AUTH_HOST:
|
? "https://auth.streetwriters.co"
|
||||||
process.env.NODE_ENV === "production"
|
: "http://localhost:8264",
|
||||||
? "https://auth.streetwriters.co"
|
SSE_HOST: isProduction()
|
||||||
: "http://localhost:8264",
|
? "https://events.streetwriters.co"
|
||||||
SSE_HOST:
|
: "http://localhost:7264",
|
||||||
process.env.NODE_ENV === "production"
|
SUBSCRIPTIONS_HOST: isProduction()
|
||||||
? "https://events.streetwriters.co"
|
? "https://subscriptions.streetwriters.co"
|
||||||
: "http://localhost:7264",
|
: "http://localhost:9264",
|
||||||
SUBSCRIPTIONS_HOST:
|
ISSUES_HOST: isProduction()
|
||||||
process.env.NODE_ENV === "production"
|
? "https://issues.streetwriters.co"
|
||||||
? "https://subscriptions.streetwriters.co"
|
: "http://localhost:2624",
|
||||||
: "http://localhost:9264",
|
|
||||||
ISSUES_HOST:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? "https://issues.streetwriters.co"
|
|
||||||
: "http://localhost:2624",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hosts;
|
export default hosts;
|
||||||
|
|||||||
Reference in New Issue
Block a user