UNPKG

etebase

Version:

Etebase TypeScript API for the web and node

1,254 lines 52.4 kB
var _a; import request from "./Request"; import * as Etebase from "./Etebase"; import { USER, USER2, sessionStorageKey } from "./TestConstants"; import { Authenticator, PrefetchOption } from "./OnlineManagers"; import { fromBase64, fromString, msgpackEncode, msgpackDecode, randomBytesDeterministic, toBase64 } from "./Helpers"; const testApiBase = (_a = process.env.ETEBASE_TEST_API_URL) !== null && _a !== void 0 ? _a : "http://localhost:8033"; let etebase; const colType = "some.coltype"; async function verifyCollection(col, meta, content) { col.verify(); const decryptedMeta = col.getMeta(); expect(decryptedMeta).toEqual(meta); const decryptedContent = await col.getContent(); expect(toBase64(decryptedContent)).toEqual(toBase64(content)); } async function verifyItem(item, meta, content) { item.verify(); const decryptedMeta = item.getMeta(); expect(decryptedMeta).toEqual(meta); const decryptedContent = await item.getContent(); expect(toBase64(decryptedContent)).toEqual(toBase64(content)); } async function prepareUserForTest(user) { const response = await request(testApiBase + "/api/v1/test/authentication/reset/", { method: "post", headers: { "Accept": "application/msgpack", "Content-Type": "application/msgpack", }, body: msgpackEncode({ user: { username: user.username, email: user.email, }, salt: fromBase64(user.salt), loginPubkey: fromBase64(user.loginPubkey), encryptedContent: fromBase64(user.encryptedContent), pubkey: fromBase64(user.pubkey), }), }); if (!response.ok) { throw new Error(response.statusText); } const etebase = await Etebase.Account.restore(user.storedSession, fromBase64(sessionStorageKey)); etebase.serverUrl = testApiBase; await etebase.fetchToken(); return etebase; } beforeAll(async () => { await Etebase.ready; for (const user of [USER, USER2]) { try { const authenticator = new Authenticator(testApiBase); await authenticator.signup(user, fromBase64(user.salt), fromBase64(user.loginPubkey), fromBase64(user.pubkey), fromBase64(user.encryptedContent)); } catch (e) { // } } }); beforeEach(async () => { await Etebase.ready; etebase = await prepareUserForTest(USER); }); afterEach(async () => { await etebase.logout(); }); it("Check server is etebase", async () => { expect(await Etebase.Account.isEtebaseServer(testApiBase)).toBeTruthy(); expect(await Etebase.Account.isEtebaseServer(testApiBase + "/api/")).toBeFalsy(); await expect(Etebase.Account.isEtebaseServer("http://doesnotexist")).rejects.toBeInstanceOf(Error); await expect(Etebase.Account.login(USER2.username, USER2.password, testApiBase + "/api/")).rejects.toBeInstanceOf(Etebase.NotFoundError); }); it("Getting dashboard url", async () => { var _a; let url; try { url = await etebase.getDashboardUrl(); } catch (e) { expect(e).toBeInstanceOf(Etebase.HttpError); expect((_a = e.content) === null || _a === void 0 ? void 0 : _a.code).toEqual("not_supported"); } if (url !== undefined) { expect(url).toBeTruthy(); } }); it("Simple collection handling", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const content = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, meta, content); expect(col.getCollectionType()).toEqual(colType); await verifyCollection(col, meta, content); const meta2 = { name: "Calendar2", description: "Someone", color: "#000000", }; col.setMeta(meta2); await verifyCollection(col, meta2, content); expect(meta).not.toEqual(col.getMeta()); expect(col.isDeleted).toBeFalsy(); col.delete(true); expect(col.isDeleted).toBeTruthy(); await verifyCollection(col, meta2, content); }); it("Simple item handling", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await verifyItem(item, meta, content); const meta2 = { type: "ITEMTYPE", someval: "someval", }; item.setMeta(meta2); await verifyItem(item, meta2, content); expect(meta).not.toEqual(col.getMeta()); expect(item.isDeleted).toBeFalsy(); item.delete(true); expect(item.isDeleted).toBeTruthy(); await verifyItem(item, meta2, content); }); it("Content formats", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const content = "Hello"; const col = await collectionManager.create(colType, meta, content); { const decryptedContent = await col.getContent(Etebase.OutputFormat.String); expect(decryptedContent).toEqual(content); const decryptedContentUint = await col.getContent(); expect(decryptedContentUint).toEqual(fromString(content)); } const itemManager = collectionManager.getItemManager(col); const metaItem = { type: "ITEMTYPE", }; const content2 = "Hello2"; const item = await itemManager.create(metaItem, content2); { const decryptedContent = await item.getContent(Etebase.OutputFormat.String); expect(decryptedContent).toEqual(content2); const decryptedContentUint = await item.getContent(); expect(decryptedContentUint).toEqual(fromString(content2)); } }); it("Simple collection sync", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const content = Uint8Array.from([1, 2, 3, 5]); let col = await collectionManager.create(colType, meta, content); await verifyCollection(col, meta, content); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(0); } await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], meta, content); } { col = await collectionManager.fetch(col.uid); const collections = await collectionManager.list(colType, { stoken: col.stoken }); expect(collections.data.length).toBe(0); } const colOld = await collectionManager.fetch(col.uid); const meta2 = { name: "Calendar2", description: "Someone", color: "#000000", }; col.setMeta(meta2); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], meta2, content); } { const collections = await collectionManager.list(colType, { stoken: col.stoken }); expect(collections.data.length).toBe(1); } // Fail uploading because of an old stoken/etag { const content2 = Uint8Array.from([7, 2, 3, 5]); await colOld.setContent(content2); await expect(collectionManager.transaction(colOld)).rejects.toBeInstanceOf(Etebase.ConflictError); await expect(collectionManager.upload(colOld, { stoken: colOld.stoken })).rejects.toBeInstanceOf(Etebase.ConflictError); } const content2 = Uint8Array.from([7, 2, 3, 5]); await col.setContent(content2); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], meta2, content2); } // Try uploadign the same collection twice as new col = await collectionManager.create(colType, meta, content); const cachedCollection = collectionManager.cacheSave(col); const colCopy = collectionManager.cacheLoad(cachedCollection); await colCopy.setContent("Something else"); // Just so it has a different revision uid await collectionManager.upload(col); await expect(collectionManager.upload(colCopy)).rejects.toBeInstanceOf(Etebase.ConflictError); }); it("Collection types", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const content = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, meta, content); await verifyCollection(col, meta, content); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(0); } await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], meta, content); } { const collections = await collectionManager.list("bad.coltype"); expect(collections.data.length).toBe(0); } { const collections = await collectionManager.list(["bad.coltype", colType, "anotherbad"]); expect(collections.data.length).toBe(1); } { const collections = await collectionManager.list(["bad.coltype", "anotherbad"]); expect(collections.data.length).toBe(0); } }); it("Simple item sync", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await verifyItem(item, meta, content); await itemManager.batch([item]); { const items = await itemManager.list(); expect(items.data.length).toBe(1); await verifyItem(items.data[0], meta, content); } const meta2 = { type: "ITEMTYPE", someval: "someval", }; item.setMeta(meta2); await itemManager.batch([item]); { const items = await itemManager.list(); expect(items.data.length).toBe(1); await verifyItem(items.data[0], meta2, content); } const content2 = Uint8Array.from([7, 2, 3, 5]); await item.setContent(content2); await itemManager.batch([item]); { const items = await itemManager.list(); expect(items.data.length).toBe(1); await verifyItem(items.data[0], meta2, content2); } }); it("Item re-uploaded revisions", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const col = await collectionManager.create(colType, colMeta, ""); await collectionManager.upload(col); const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const item = await itemManager.create(meta, ""); await itemManager.batch([item]); // Adding the same item twice should work await itemManager.batch([item]); const itemOld = item._clone(); await item.setContent("Test"); await itemManager.batch([item]); await expect(itemManager.batch([itemOld])).rejects.toBeInstanceOf(Etebase.HttpError); }); it("Collection as item", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); // Verify withCollection works { let items = await itemManager.list(); expect(items.data.length).toBe(0); items = await itemManager.list({ withCollection: true }); expect(items.data.length).toBe(1); await verifyItem(items.data[0], colMeta, colContent); } const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await verifyItem(item, meta, content); await itemManager.batch([item]); { let items = await itemManager.list(); expect(items.data.length).toBe(1); items = await itemManager.list({ withCollection: true }); expect(items.data.length).toBe(2); await verifyItem(items.data[0], colMeta, colContent); } const colItemOld = await itemManager.fetch(col.uid); // Manipulate the collection with batch/transaction await col.setContent("test"); await itemManager.batch([col.item]); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], colMeta, fromString("test")); } await col.setContent("test2"); await itemManager.transaction([col.item]); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); await verifyCollection(collections.data[0], colMeta, fromString("test2")); } { const updates = await itemManager.fetchUpdates([colItemOld, item]); expect(updates.data.length).toBe(1); await verifyItem(updates.data[0], colMeta, fromString("test2")); } }); // Verify we prevent users from trying to use the wrong items in the API it("Item multiple collections", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); const colMeta2 = { name: "Calendar 2", description: "Mine", color: "#ffffff", }; const colContent2 = Uint8Array.from([]); const col2 = await collectionManager.create(colType, colMeta2, colContent2); await collectionManager.upload(col2); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(2); } const itemManager = collectionManager.getItemManager(col); const itemManager2 = collectionManager.getItemManager(col2); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await verifyItem(item, meta, content); // With the bad item as the main await itemManager.batch([item]); await expect(itemManager2.batch([item])).rejects.toBeInstanceOf(Etebase.ProgrammingError); await expect(itemManager2.transaction([item])).rejects.toBeInstanceOf(Etebase.ProgrammingError); // With the bad item as a dep const item2 = await itemManager2.create(meta, "col2"); await expect(itemManager2.batch([item2], [item])).rejects.toBeInstanceOf(Etebase.ProgrammingError); await expect(itemManager2.transaction([item2], [item])).rejects.toBeInstanceOf(Etebase.ProgrammingError); await itemManager2.batch([item2]); await itemManager.fetchUpdates([item]); await expect(itemManager.fetchUpdates([item, item2])).rejects.toBeInstanceOf(Etebase.ProgrammingError); // Verify we also set it correctly when fetched { const items = await itemManager.list(); const itemFetched = items.data[0]; expect(item.collectionUid).toEqual(itemFetched.collectionUid); } { const itemFetched = await itemManager.fetch(item.uid); expect(item.collectionUid).toEqual(itemFetched.collectionUid); } }); it("Collection and item deletion", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await verifyCollection(col, colMeta, colContent); await collectionManager.upload(col); const collections = await collectionManager.list(colType); const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await verifyItem(item, meta, content); await itemManager.batch([item]); const items = await itemManager.list(); expect(items.data.length).toBe(1); item.delete(true); await itemManager.batch([item]); { const items2 = await itemManager.list({ stoken: items.stoken }); expect(items2.data.length).toBe(1); const item2 = items2.data[0]; await verifyItem(item2, meta, content); expect(item2.isDeleted).toBeTruthy(); } col.delete(true); await collectionManager.upload(col); { const collections2 = await collectionManager.list(colType, { stoken: collections.stoken }); expect(collections2.data.length).toBe(1); const col2 = collections2.data[0]; await verifyCollection(col2, colMeta, colContent); expect(col2.isDeleted).toBeTruthy(); } }); it("Empty content", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const content = Uint8Array.from([]); let col = await collectionManager.create(colType, meta, content); await verifyCollection(col, meta, content); await collectionManager.upload(col); { col = await collectionManager.fetch(col.uid); await verifyCollection(col, meta, content); } const itemManager = collectionManager.getItemManager(col); const itemMeta = { type: "ITEMTYPE", }; const item = await itemManager.create(itemMeta, content); await itemManager.transaction([item]); { const items = await itemManager.list(); const itemFetched = items.data[0]; verifyItem(itemFetched, itemMeta, content); } }); it("List response correctness", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); const itemManager = collectionManager.getItemManager(col); const items = []; for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.batch(items); { let items = await itemManager.list(); expect(items.data.length).toBe(5); expect(items.done).toBeTruthy(); items = await itemManager.list({ limit: 5 }); expect(items.done).toBeTruthy(); } let stoken = null; for (let i = 0; i < 3; i++) { const items = await itemManager.list({ limit: 2, stoken }); expect(items.done).toBe(i === 2); stoken = items.stoken; } // Also check collections { for (let i = 0; i < 4; i++) { const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const col2 = await collectionManager.create(colType, colMeta, content2); await collectionManager.upload(col2); } } { let collections = await collectionManager.list(colType); expect(collections.data.length).toBe(5); expect(collections.done).toBeTruthy(); collections = await collectionManager.list(colType, { limit: 5 }); expect(collections.done).toBeTruthy(); } stoken = null; for (let i = 0; i < 3; i++) { const collections = await collectionManager.list(colType, { limit: 2, stoken }); expect(collections.done).toBe(i === 2); stoken = collections.stoken; } }); it("Item transactions", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); const deps = [item]; await itemManager.transaction(deps); const itemOld = await itemManager.fetch(item.uid); const items = []; { const items = await itemManager.list(); expect(items.data.length).toBe(1); } for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.transaction(items, deps); { const items = await itemManager.list(); expect(items.data.length).toBe(6); } { const meta3 = { ...meta, someval: "some" }; item.setMeta(meta3); } await itemManager.transaction([item], items); { const items = await itemManager.list(); expect(items.data.length).toBe(6); } { const meta3 = { ...meta, someval: "some2" }; item.setMeta(meta3); // Old in the deps await expect(itemManager.transaction([item], [...items, itemOld])).rejects.toBeInstanceOf(Etebase.ConflictError); const itemOld2 = itemOld._clone(); await itemManager.transaction([item]); itemOld2.setMeta(meta3); // Old stoken in the item itself await expect(itemManager.transaction([itemOld2])).rejects.toBeInstanceOf(Etebase.ConflictError); } { const meta3 = { ...meta, someval: "some2" }; const item2 = await itemManager.fetch(items[0].uid); item2.setMeta(meta3); const itemOld2 = itemOld._clone(); itemOld2.setMeta(meta3); // Part of the transaction is bad, and part is good await expect(itemManager.transaction([item2, itemOld2])).rejects.toBeInstanceOf(Etebase.ConflictError); // Verify it hasn't changed after the transaction above failed const item2Fetch = await itemManager.fetch(item2.uid); expect(item2Fetch.getMeta()).not.toEqual(item2.getMeta()); } { // Global stoken test const meta3 = { ...meta, someval: "some2" }; item.setMeta(meta3); const newCol = await collectionManager.fetch(col.uid); const stoken = newCol.stoken; const badEtag = col.etag; await expect(itemManager.transaction([item], undefined, { stoken: badEtag })).rejects.toBeInstanceOf(Etebase.ConflictError); await itemManager.transaction([item], undefined, { stoken }); } }); it("Item batch stoken", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await itemManager.batch([item]); const items = []; { const items = await itemManager.list(); expect(items.data.length).toBe(1); } for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.batch(items); { const meta3 = { ...meta, someval: "some2" }; const item2 = item._clone(); item2.setMeta(meta3); await itemManager.batch([item2]); meta3.someval = "some3"; item.setMeta(meta3); // Old stoken in the item itself should work for batch and fail for transaction or batch with deps await expect(itemManager.transaction([item])).rejects.toBeInstanceOf(Etebase.ConflictError); await expect(itemManager.batch([item], [item])).rejects.toBeInstanceOf(Etebase.ConflictError); await itemManager.batch([item]); } { // Global stoken test const meta3 = { ...meta, someval: "some2" }; item.setMeta(meta3); const newCol = await collectionManager.fetch(col.uid); const stoken = newCol.stoken; const badEtag = col.etag; await expect(itemManager.batch([item], null, { stoken: badEtag })).rejects.toBeInstanceOf(Etebase.ConflictError); await itemManager.batch([item], null, { stoken }); } }); it("Item fetch updates", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await itemManager.batch([item]); const items = []; { const items = await itemManager.list(); expect(items.data.length).toBe(1); } for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.batch(items); { const items = await itemManager.list(); expect(items.data.length).toBe(6); } // Fetch multi { let items2 = await itemManager.fetchMulti(items.map((x) => x.uid)); expect(items2.data.length).toBe(5); items2 = await itemManager.fetchMulti(["L4QQdlkCDJ9ySmrGD5fM0DsFo08MnWel", items[0].uid]); // Only 1 because only one of the items exists expect(items2.data.length).toBe(1); } let stoken = null; { const newCol = await collectionManager.fetch(col.uid); stoken = newCol.stoken; } { let updates = await itemManager.fetchUpdates(items); expect(updates.data.length).toBe(0); updates = await itemManager.fetchUpdates(items, { stoken }); expect(updates.data.length).toBe(0); } { const meta3 = { ...meta, someval: "some2" }; const item2 = items[0]._clone(); item2.setMeta(meta3); await itemManager.batch([item2]); } { let updates = await itemManager.fetchUpdates(items); expect(updates.data.length).toBe(1); updates = await itemManager.fetchUpdates(items, { stoken }); expect(updates.data.length).toBe(1); } { const item2 = await itemManager.fetch(items[0].uid); let updates = await itemManager.fetchUpdates([item2]); expect(updates.data.length).toBe(0); updates = await itemManager.fetchUpdates([item2], { stoken }); expect(updates.data.length).toBe(1); } { const newCol = await collectionManager.fetch(col.uid); stoken = newCol.stoken; } { const updates = await itemManager.fetchUpdates(items, { stoken }); expect(updates.data.length).toBe(0); } }); it("Item revisions", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const col = await collectionManager.create(colType, colMeta, ""); await collectionManager.upload(col); const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const item = await itemManager.create(meta, ""); for (let i = 0; i < 5; i++) { const content = Uint8Array.from([1, 2, i]); await item.setContent(content); await itemManager.batch([item]); } await item.setContent("Latest"); await itemManager.batch([item]); { let revisions = await itemManager.itemRevisions(item, { iterator: item.etag }); expect(revisions.data.length).toBe(5); expect(revisions.done).toBeTruthy(); revisions = await itemManager.itemRevisions(item, { iterator: item.etag, limit: 5 }); expect(revisions.done).toBeTruthy(); for (let i = 0; i < 5; i++) { const content = Uint8Array.from([1, 2, i]); // The revisions are ordered newest to oldest const rev = revisions.data[4 - i]; expect(await rev.getContent()).toEqual(content); } } // Iterate through revisions { let iterator = item.etag; for (let i = 0; i < 2; i++) { const revisions = await itemManager.itemRevisions(item, { limit: 2, iterator }); expect(revisions.done).toBe(i === 2); iterator = revisions.iterator; } } }); it("Collection invitations", async () => { var _a, _b, _c; const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const items = []; for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.batch(items); const collectionInvitationManager = etebase.getInvitationManager(); const etebase2 = await prepareUserForTest(USER2); const collectionManager2 = etebase2.getCollectionManager(); const collectionInvitationManager2 = etebase2.getInvitationManager(); const user2Profile = await collectionInvitationManager.fetchUserProfile(USER2.username); { // Also make sure we can invite by email const user2Profile2 = await collectionInvitationManager.fetchUserProfile(USER2.email); expect(user2Profile2.pubkey).toEqual(user2Profile.pubkey); } // Should be verified by user1 off-band const user2pubkey = collectionInvitationManager2.pubkey; expect(user2Profile.pubkey).toEqual(user2pubkey); // Off-band verification: expect(Etebase.getPrettyFingerprint(user2Profile.pubkey)).toEqual(Etebase.getPrettyFingerprint(user2pubkey)); await collectionInvitationManager.invite(col, USER2.username, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); let invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(1); expect(invitations.data[0].fromUsername).toEqual(USER.username); await collectionInvitationManager2.reject(invitations.data[0]); { const collections = await collectionManager2.list(colType); expect(collections.data.length).toBe(0); } { const invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(0); } // Invite and then disinvite await collectionInvitationManager.invite(col, USER2.username, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(1); await collectionInvitationManager.disinvite(invitations.data[0]); { const collections = await collectionManager2.list(colType); expect(collections.data.length).toBe(0); } { const invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(0); } // Invite again, this time use email, and this time accept await collectionInvitationManager.invite(col, USER2.email, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(1); let stoken; { const newCol = await collectionManager.fetch(col.uid); stoken = newCol.stoken; } // Should be verified by user2 off-band const user1pubkey = collectionInvitationManager.pubkey; expect(invitations.data[0].fromPubkey).toEqual(user1pubkey); await collectionInvitationManager2.accept(invitations.data[0]); { const collections = await collectionManager2.list(colType); expect(collections.data.length).toBe(1); collections.data[0].getMeta(); expect(collections.data[0].getCollectionType()).toEqual(colType); } { const invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(0); } const col2 = await collectionManager2.fetch(col.uid); const collectionMemberManager2 = collectionManager2.getMemberManager(col2); await collectionMemberManager2.leave(); { const collections = await collectionManager2.list(colType, { stoken }); expect(collections.data.length).toBe(0); expect((_a = collections.removedMemberships) === null || _a === void 0 ? void 0 : _a.length).toBe(1); } // Add again await collectionInvitationManager.invite(col, USER2.username, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(1); await collectionInvitationManager2.accept(invitations.data[0]); { const newCol = await collectionManager.fetch(col.uid); expect(stoken).not.toEqual(newCol.stoken); const collections = await collectionManager2.list(colType, { stoken }); expect(collections.data.length).toBe(1); expect(collections.data[0].uid).toEqual(col.uid); expect(collections.removedMemberships).not.toBeDefined(); } // Remove { const newCol = await collectionManager.fetch(col.uid); expect(stoken).not.toEqual(newCol.stoken); const collectionMemberManager = collectionManager.getMemberManager(col); await collectionMemberManager.remove(USER2.username); const collections = await collectionManager2.list(colType, { stoken }); expect(collections.data.length).toBe(0); expect((_b = collections.removedMemberships) === null || _b === void 0 ? void 0 : _b.length).toBe(1); stoken = newCol.stoken; } { const collections = await collectionManager2.list(colType, { stoken }); expect(collections.data.length).toBe(0); expect((_c = collections.removedMemberships) === null || _c === void 0 ? void 0 : _c.length).toBe(1); } await etebase2.logout(); }, 10000); it("Iterating invitations", async () => { const etebase2 = await prepareUserForTest(USER2); const collectionManager = etebase.getCollectionManager(); const collectionInvitationManager = etebase.getInvitationManager(); const user2Profile = await collectionInvitationManager.fetchUserProfile(USER2.username); const collections = []; for (let i = 0; i < 3; i++) { const colMeta = { name: `Calendar ${i}`, }; const col = await collectionManager.create(colType, colMeta, ""); await collectionManager.upload(col); await collectionInvitationManager.invite(col, USER2.username, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); collections.push(col); } const collectionInvitationManager2 = etebase2.getInvitationManager(); // Check incoming { const invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(3); } { let iterator = null; for (let i = 0; i < 2; i++) { const invitations = await collectionInvitationManager2.listIncoming({ limit: 2, iterator }); expect(invitations.done).toBe(i === 1); iterator = invitations.iterator; } } // Check outgoing { const invitations = await collectionInvitationManager.listOutgoing(); expect(invitations.data.length).toBe(3); } { let iterator = null; for (let i = 0; i < 2; i++) { const invitations = await collectionInvitationManager.listOutgoing({ limit: 2, iterator }); expect(invitations.done).toBe(i === 1); iterator = invitations.iterator; } } await etebase2.logout(); }); it("Collection access level", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", description: "Mine", color: "#ffffff", }; const colContent = Uint8Array.from([1, 2, 3, 5]); const col = await collectionManager.create(colType, colMeta, colContent); await collectionManager.upload(col); { const collections = await collectionManager.list(colType); expect(collections.data.length).toBe(1); } const itemManager = collectionManager.getItemManager(col); const items = []; for (let i = 0; i < 5; i++) { const meta2 = { type: "ITEMTYPE", someval: "someval", i, }; const content2 = Uint8Array.from([i, 7, 2, 3, 5]); const item2 = await itemManager.create(meta2, content2); items.push(item2); } await itemManager.batch(items); const collectionMemberManager = collectionManager.getMemberManager(col); const collectionInvitationManager = etebase.getInvitationManager(); const etebase2 = await prepareUserForTest(USER2); const collectionManager2 = etebase2.getCollectionManager(); const user2Profile = await collectionInvitationManager.fetchUserProfile(USER2.username); await collectionInvitationManager.invite(col, USER2.username, user2Profile.pubkey, Etebase.CollectionAccessLevel.ReadWrite); const collectionInvitationManager2 = etebase2.getInvitationManager(); const invitations = await collectionInvitationManager2.listIncoming(); expect(invitations.data.length).toBe(1); await collectionInvitationManager2.accept(invitations.data[0]); const col2 = await collectionManager2.fetch(col.uid); const itemManager2 = collectionManager2.getItemManager(col2); // Item creation: success { const members = await collectionMemberManager.list(); expect(members.data.length).toBe(2); for (const member of members.data) { if (member.username === USER2.username) { expect(member.accessLevel).toBe(Etebase.CollectionAccessLevel.ReadWrite); } } const meta = { type: "ITEMTYPE2", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager2.create(meta, content); await itemManager2.batch([item]); } await collectionMemberManager.modifyAccessLevel(USER2.username, Etebase.CollectionAccessLevel.ReadOnly); // Item creation: fail { const members = await collectionMemberManager.list(); expect(members.data.length).toBe(2); for (const member of members.data) { if (member.username === USER2.username) { expect(member.accessLevel).toBe(Etebase.CollectionAccessLevel.ReadOnly); } } const meta = { type: "ITEMTYPE3", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager2.create(meta, content); await expect(itemManager2.batch([item])).rejects.toBeInstanceOf(Etebase.PermissionDeniedError); } await collectionMemberManager.modifyAccessLevel(USER2.username, Etebase.CollectionAccessLevel.Admin); // Item creation: success { const members = await collectionMemberManager.list(); expect(members.data.length).toBe(2); for (const member of members.data) { if (member.username === USER2.username) { expect(member.accessLevel).toBe(Etebase.CollectionAccessLevel.Admin); } } const meta = { type: "ITEMTYPE3", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager2.create(meta, content); await itemManager2.batch([item]); } // Iterate members { const members = await collectionMemberManager.list({ limit: 1 }); expect(members.data.length).toBe(1); const members2 = await collectionMemberManager.list({ limit: 1, iterator: members.iterator }); expect(members2.data.length).toBe(1); // Verify we got two different usersnames expect(members.data[0].username).not.toEqual(members2.data[0].username); const membersDone = await collectionMemberManager.list(); expect(membersDone.done).toBeTruthy(); } await etebase2.logout(); }); it("Session store and restore", async () => { const collectionManager = etebase.getCollectionManager(); const meta = { name: "Calendar", }; const col = await collectionManager.create(colType, meta, "test"); await collectionManager.upload(col); // Verify we can store and restore without an encryption key { const saved = await etebase.save(); const etebase2 = await Etebase.Account.restore(saved); // Verify we can access the data const collectionManager2 = etebase2.getCollectionManager(); const collections = await collectionManager2.list(colType); expect(collections.data.length).toBe(1); expect(await collections.data[0].getContent(Etebase.OutputFormat.String)).toEqual("test"); } // Verify we can store and restore with an encryption key { const encryptionKey = Etebase.randomBytes(32); const saved = await etebase.save(encryptionKey); // Fail without an encryption key // FIXME: Test for the correct error value await expect(Etebase.Account.restore(saved)).rejects.toBeTruthy(); const etebase2 = await Etebase.Account.restore(saved, encryptionKey); // Verify we can access the data const collectionManager2 = etebase2.getCollectionManager(); const collections = await collectionManager2.list(colType); expect(collections.data.length).toBe(1); expect(await collections.data[0].getContent(Etebase.OutputFormat.String)).toEqual("test"); } }); it("Cache collections and items", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", }; const col = await collectionManager.create(colType, colMeta, "test"); await collectionManager.upload(col); const itemManager = collectionManager.getItemManager(col); const meta = { type: "ITEMTYPE", }; const content = Uint8Array.from([1, 2, 3, 6]); const item = await itemManager.create(meta, content); await itemManager.batch([item]); const optionsArray = [ { saveContent: true }, { saveContent: false }, ]; for (const options of optionsArray) { const savedCachedCollection = collectionManager.cacheSave(col, options); const cachedCollection = collectionManager.cacheLoad(savedCachedCollection); expect(col.uid).toEqual(cachedCollection.uid); expect(col.etag).toEqual(cachedCollection.etag); expect(col.getMeta()).toEqual(cachedCollection.getMeta()); const savedCachedItem = itemManager.cacheSave(item, options); const cachedItem = itemManager.cacheLoad(savedCachedItem); expect(item.uid).toEqual(cachedItem.uid); expect(item.etag).toEqual(cachedItem.etag); expect(item.getMeta()).toEqual(cachedItem.getMeta()); } // Verify content { const options = { saveContent: true }; const savedCachedItem = itemManager.cacheSave(item, options); const cachedItem = itemManager.cacheLoad(savedCachedItem); expect(await item.getContent()).toEqual(await cachedItem.getContent()); } }); it("Chunk pre-upload and download-missing", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", }; const col = await collectionManager.create(colType, colMeta, ""); await collectionManager.upload(col); const itemManager = collectionManager.getItemManager(col); const meta = { type: "itemtype", }; const content = "Something"; const item = await itemManager.create(meta, content); expect(item.isMissingContent).not.toBeTruthy(); await itemManager.uploadContent(item); // Verify we don't fail even when already uploaded await itemManager.uploadContent(item); await itemManager.batch([item]); { const item2 = await itemManager.fetch(item.uid, { prefetch: PrefetchOption.Medium }); const meta2 = item2.getMeta(); expect(meta2).toEqual(meta); // We can't get the content of partial item await expect(item2.getContent()).rejects.toBeInstanceOf(Etebase.MissingContentError); expect(item2.isMissingContent).toBeTruthy(); // Fetch the content and then try to get it await itemManager.downloadContent(item2); expect(item2.isMissingContent).not.toBeTruthy(); expect(await item2.getContent(Etebase.OutputFormat.String)).toEqual(content); } }); it("Chunking large data", async () => { const collectionManager = etebase.getCollectionManager(); const colMeta = { name: "Calendar", }; const buf = randomBytesDeterministic(120 * 1024, new Uint8Array(32)); // 120kb of psuedorandom data const col = await collectionManager.create(colType, colMeta, ""); const itemManager = collectionManager.getItemManager(col); const item = await itemManager.create({}, buf); await verifyItem(item, {}, buf); async function itemGetChunkUids(it) { // XXX: hack - get the chunk uids from the cached saving const cachedItem = msgpackDecode(itemManager.cacheSave(it, { saveContent: false })); const cachedRevision = msgpackDecode(cachedItem[cachedItem.length - 1]); const cachedChunks = cachedRevision[cachedRevision.length - 1]; return cachedChunks.map((x) => toBase64(x[0])); } const uidSet = new Set(); // Get the first chunks and init uidSet { const chunkUids = await itemGetChunkUids(item); expect(chunkUids.length).toEqual(8); chunkUids.forEach(