From 408ed6f0ec802bab820f5b095e8dd8a9e81df44e Mon Sep 17 00:00:00 2001 From: Hoseong Lee Date: Thu, 11 Dec 2025 15:05:46 -0600 Subject: [PATCH 1/5] unit test cases for shared folder sync down routine --- .../src/__tests__/SyncDownResponseBuilder.ts | 56 +- keeperapi/src/__tests__/vault.test.ts | 802 +++++++++++++++++- 2 files changed, 852 insertions(+), 6 deletions(-) diff --git a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts index b7310d8..03f0019 100644 --- a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts +++ b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts @@ -70,9 +70,9 @@ export class SyncDownResponseBuilder { this.data.recordMetaData?.push(recordMetadata) } - async addRecord(decryptedRecordData: DecryptedRecordData) { + async addRecord(decryptedRecordData: DecryptedRecordData, encryptionKey?: Uint8Array) { const decryptedRecordKey = this.platform.getRandomBytes(32) - const recordKey = await this.platform.aesGcmEncrypt(decryptedRecordKey, this.auth.dataKey!) + const recordKey = await this.platform.aesGcmEncrypt(decryptedRecordKey, encryptionKey ? encryptionKey : this.auth.dataKey!) const recordUid = this.platform.getRandomBytes(16) const decodedRecordData = this.platform.stringToBytes(JSON.stringify(decryptedRecordData)) const recordData = await this.platform.aesGcmEncrypt(decodedRecordData, decryptedRecordKey) @@ -132,6 +132,58 @@ export class SyncDownResponseBuilder { this.data.removedUserFolderRecords?.push({recordUid, folderUid}) } + addSharedFolder(sharedFolder: Vault.ISharedFolder) { + this.data.sharedFolders?.push(sharedFolder) + } + + addSharedFolderUser(sharedFolderUser: Vault.ISharedFolderUser) { + this.data.sharedFolderUsers?.push(sharedFolderUser) + } + + addRemovedSharedFolder(sharedFolderUid: Uint8Array) { + this.data.removedSharedFolders?.push(sharedFolderUid) + } + + addRemovedSharedFolderTeam(sharedFolderTeam: Vault.ISharedFolderTeam) { + this.data.removedSharedFolderTeams?.push(sharedFolderTeam) + } + + addSharedFolderTeam(sharedFolderTeam: Vault.ISharedFolderTeam) { + this.data.sharedFolderTeams?.push(sharedFolderTeam) + } + + addUserFolderSharedFolder(userFolderSharedFolder: Vault.IUserFolderSharedFolder) { + this.data.userFolderSharedFolders?.push(userFolderSharedFolder) + } + + addTeam(team: Vault.ITeam) { + this.data.teams?.push(team) + } + + addSharedFolderRecord(sharedFolderRecord: Vault.ISharedFolderRecord) { + this.data.sharedFolderRecords?.push(sharedFolderRecord) + } + + addRemovedSharedFolderRecord(sharedFolderRecord: Vault.ISharedFolderRecord) { + this.data.removedSharedFolderRecords?.push(sharedFolderRecord) + } + + addSharedFolderFolder(sharedFolderFolder: Vault.ISharedFolderFolder) { + this.data.sharedFolderFolders?.push(sharedFolderFolder) + } + + addRemovedSharedFolderFolder(removedSharedFolderFolder: Vault.ISharedFolderFolder) { + this.data.removedSharedFolderFolders?.push(removedSharedFolderFolder) + } + + addSharedFolderFolderRecord(sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord) { + this.data.sharedFolderFolderRecords?.push(sharedFolderFolderRecord) + } + + addRemovedSharedFolderFolderRecord(sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord) { + this.data.removedSharedFolderFolderRecords?.push(sharedFolderFolderRecord) + } + build() { return this.data } diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index e88bd9d..1004e06 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -10,8 +10,9 @@ describe('Sync Down', () => { let dataKey: Uint8Array let auth: Auth; let eccKeyPair: {privateKey: Uint8Array, publicKey: Uint8Array}; + let rsaKeyPair: {privateKey: Uint8Array, publicKey: Uint8Array}; let storage: VaultStorage; - let mockSyncDownCommand: jest.MockedFunction<() => any>; + let mockSyncDownCommand: jest.MockedFunction<() => Promise>; let syncDownResponseBuilder: SyncDownResponseBuilder; let syncDownUser: { username: string, @@ -25,6 +26,7 @@ describe('Sync Down', () => { connectPlatform(nodePlatform) dataKey = platform.getRandomBytes(32) eccKeyPair = await platform.generateECKeyPair() + rsaKeyPair = await platform.generateRSAKeyPair() syncDownUser = { username: 'keeper@keepersecurity.com', accountUid: platform.getRandomBytes(16) @@ -39,6 +41,7 @@ describe('Sync Down', () => { get: jest.fn(), addDependencies: jest.fn(), delete: jest.fn(), + getDependencies: jest.fn(), removeDependencies: jest.fn(), put: jest.fn(), saveObject: jest.fn(), @@ -49,6 +52,7 @@ describe('Sync Down', () => { dataKey, eccPrivateKey: eccKeyPair.privateKey, eccPublicKey: eccKeyPair.publicKey, + privateKey: rsaKeyPair.privateKey, executeRest: mockSyncDownCommand, } as unknown as Auth; syncDownResponseBuilder = new SyncDownResponseBuilder(platform, auth); @@ -330,14 +334,17 @@ describe('Sync Down', () => { }) it('deletes the corresponding folder data when a user deletes an existing folder - empty folder', async () => { const folderUid = platform.getRandomBytes(16) - syncDownResponseBuilder - .addRemovedUserFolder(folderUid) + const folderUidStr = webSafe64FromBytes(folderUid) + syncDownResponseBuilder.addRemovedUserFolder(folderUid) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, storage, }) - expect(storage.delete).toHaveBeenCalledWith("user_folder", webSafe64FromBytes(folderUid)) + expect(storage.delete).toHaveBeenCalledWith("user_folder", folderUidStr) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [folderUidStr]: "*", + }) }) it('deletes the corresponding folder data when a user deletes an existing folder - folder with child records and child folders', async () => { /* @@ -509,4 +516,791 @@ describe('Sync Down', () => { expect(storage.delete).toHaveBeenCalledWith("record", webSafe64FromBytes(recordUid)) }) }) + describe('Shared Folders', () => { + it('saves the shared folder data when a new shared folder is created by the user', async () => { + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey= await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: syncDownUser.username, + ownerAccountUid: syncDownUser.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + syncDownResponseBuilder.addSharedFolder(sharedFolder) + const sharedFolderUser: Vault.ISharedFolderUser = { + // if the data is the current sync user, the username and accountUid are empty + username: '', + accountUid: new Uint8Array([]), + sharedFolderUid, + manageRecords: true, + manageUsers: true, + } + syncDownResponseBuilder.addSharedFolderUser(sharedFolderUser) + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + revision: sharedFolder.revision, + sharedFolderUid, + folderUid: new Uint8Array([]), // root folder + } + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + uid: webSafe64FromBytes(sharedFolderUid), + data: sharedFolderData, + name: sharedFolderNameStr, + revision: sharedFolder.revision, + ownerUsername: sharedFolder.owner, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_user', + sharedFolderUid: webSafe64FromBytes(sharedFolderUid), + accountUid: webSafe64FromBytes(sharedFolderUser.accountUid!), + accountUsername: sharedFolderUser.username, + manageRecords: sharedFolderUser.manageRecords, + manageUsers: sharedFolderUser.manageUsers, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({}) + }) + it('saves the shared folder data when the user is added to the folder', async () => { + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: anotherUserA.username, + ownerAccountUid: anotherUserA.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + syncDownResponseBuilder.addSharedFolder(sharedFolder) + const sharedFolderUserA: Vault.ISharedFolderUser = { + // if the data is the current sync user, the username and accountUid are empty + username: '', + accountUid: new Uint8Array([]), + sharedFolderUid, + manageRecords: false, + manageUsers: false, + } + const sharedFolderUserB: Vault.ISharedFolderUser = { + username: 'other user who owns the shared folder', + accountUid: anotherUserA.accountUid, + sharedFolderUid, + manageRecords: true, + manageUsers: true, + } + syncDownResponseBuilder.addSharedFolderUser(sharedFolderUserA) + syncDownResponseBuilder.addSharedFolderUser(sharedFolderUserB) + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + revision: sharedFolder.revision, + sharedFolderUid, + folderUid: new Uint8Array([]), // root folder + } + + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + uid: webSafe64FromBytes(sharedFolderUid), + data: sharedFolderData, + name: sharedFolderNameStr, + revision: sharedFolder.revision, + ownerUsername: sharedFolder.owner, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_user', + sharedFolderUid: webSafe64FromBytes(sharedFolderUid), + accountUid: webSafe64FromBytes(sharedFolderUserA.accountUid!), + accountUsername: sharedFolderUserA.username, + manageRecords: sharedFolderUserA.manageRecords, + manageUsers: sharedFolderUserA.manageUsers, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_user', + sharedFolderUid: webSafe64FromBytes(sharedFolderUid), + accountUid: webSafe64FromBytes(sharedFolderUserB.accountUid!), + accountUsername: sharedFolderUserB.username, + manageRecords: sharedFolderUserB.manageRecords, + manageUsers: sharedFolderUserB.manageUsers, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({}) + }) + it("saves the shared folder data when the user's team is added to the folder", async () => { + const sharedFolderNameStr = 'a shared folder through team access' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const teamUid = platform.getRandomBytes(16) + const teamUidStr = webSafe64FromBytes(teamUid) + const decryptedTeamKey = platform.getRandomBytes(32) + const encryptedTeamKey = platform.publicEncrypt(decryptedTeamKey, platform.bytesToBase64(auth.privateKey!)) + const decryptedTeamPrivateKeyPair = await platform.generateRSAKeyPair() + const encryptedTeamPrivateKey= await platform.aesCbcEncrypt(decryptedTeamPrivateKeyPair.privateKey, decryptedTeamKey, true) + const sharedFolderKey = platform.publicEncrypt(decryptedSharedFolderKey, platform.bytesToBase64(decryptedTeamPrivateKeyPair.privateKey)) + const team: Vault.ITeam = { + teamUid, + name: 'team name', + removedSharedFolders: [], + sharedFolderKeys: [ + { + keyType: Records.RecordKeyType.ENCRYPTED_BY_PUBLIC_KEY, + sharedFolderUid, + sharedFolderKey, + }, + ], + teamKey: encryptedTeamKey, + teamPrivateKey: encryptedTeamPrivateKey, + teamKeyType: Records.RecordKeyType.ENCRYPTED_BY_PUBLIC_KEY, + restrictEdit: false, + restrictShare: false, + restrictView: false, + } + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + owner: anotherUserA.username, + ownerAccountUid: anotherUserA.accountUid, + keyType: Records.RecordKeyType.NO_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + revision: sharedFolder.revision, + sharedFolderUid, + folderUid: new Uint8Array([]), // root folder + } + const sharedFolderUser: Vault.ISharedFolderUser = { + username: 'other user who owns the shared folder', + accountUid: anotherUserA.accountUid, + sharedFolderUid, + manageRecords: true, + manageUsers: true, + } + const sharedFolderTeam: Vault.ISharedFolderTeam = { + name: sharedFolderNameStr, + manageUsers: false, + manageRecords: false, + teamUid, + sharedFolderUid, + } + syncDownResponseBuilder.addTeam(team) + syncDownResponseBuilder.addSharedFolder(sharedFolder) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + syncDownResponseBuilder.addSharedFolderUser(sharedFolderUser) + syncDownResponseBuilder.addSharedFolderTeam(sharedFolderTeam) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'team', + name: team.name, + uid: webSafe64FromBytes(teamUid), + restrictEdit: team.restrictEdit, + restrictShare: team.restrictShare, + restrictView: team.restrictView, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + uid: webSafe64FromBytes(sharedFolderUid), + data: sharedFolderData, + name: sharedFolderNameStr, + revision: sharedFolder.revision, + ownerUsername: sharedFolder.owner, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_user', + sharedFolderUid: webSafe64FromBytes(sharedFolderUid), + accountUid: webSafe64FromBytes(sharedFolderUser.accountUid!), + accountUsername: sharedFolderUser.username, + manageRecords: sharedFolderUser.manageRecords, + manageUsers: sharedFolderUser.manageUsers, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_team', + teamUid: teamUidStr, + name: sharedFolderNameStr, + sharedFolderUid: sharedFolderUidStr, + manageRecords: sharedFolderTeam.manageRecords, + manageUsers: sharedFolderTeam.manageUsers, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [teamUidStr]: new Set([{ + kind: 'shared_folder', + parentUid: teamUidStr, + uid: sharedFolderUidStr, + }]) + }) + }) + it('saves the shared folder data when the folder data is updated', async () => { + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'an existing shared folder data updated' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: anotherUserA.username, + ownerAccountUid: anotherUserA.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + revision: sharedFolder.revision, + sharedFolderUid, + folderUid: new Uint8Array([]), // root folder + } + syncDownResponseBuilder.addSharedFolder(sharedFolder) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + uid: webSafe64FromBytes(sharedFolderUid), + data: sharedFolderData, + name: sharedFolderNameStr, + revision: sharedFolder.revision, + ownerUsername: sharedFolder.owner, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({}) + }) + // TODO(@hleekeeper): a bug found where the shared folder folder data is not cleaned up properly when its parent shared folder is deleted/unshared. + // A Jira ticket (BE-7056) has been filed. And the business logic and the test code around this part may change as part of the BE-7056 + it.each([ + "deletes the shared folder data and its child resources when it's deleted", + "deletes the shared folder data and its child resources when the user's direct access to the folder was removed", + ])('%s', async () => { + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + syncDownResponseBuilder.addRemovedSharedFolder(sharedFolderUid) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.delete).toHaveBeenCalledWith('shared_folder', sharedFolderUidStr) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: "*" + }) + }) + it("deletes the shared folder data and its child resources when the user's team access to the folder was removed", async () => { + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const teamUid = platform.getRandomBytes(16) + const teamUidStr = webSafe64FromBytes(teamUid) + const removedSharedFolderTeam: Vault.ISharedFolderTeam = { + sharedFolderUid, + teamUid, + } + syncDownResponseBuilder.addRemovedSharedFolderTeam(removedSharedFolderTeam) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([teamUidStr]) + }) + }) + it('saves the record data when a new record is created in a shared folder', async () => { + const decryptedRecordData = { + title: 'an existing record moved to a shared folder' + } + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: syncDownUser.username, + ownerAccountUid: syncDownUser.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + const {recordKey, recordUid} = await syncDownResponseBuilder.addRecord(decryptedRecordData, decryptedSharedFolderKey) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderRecord: Vault.ISharedFolderRecord = { + owner: true, + recordKey, + recordUid, + sharedFolderUid, + ownerAccountUid: new Uint8Array([]), + } + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + folderUid: new Uint8Array([]), + sharedFolderUid, + recordUid, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + sharedFolderUid, + folderUid: new Uint8Array([]), + revision: Date.now() + } + syncDownResponseBuilder.addSharedFolder(sharedFolder) + syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + name: sharedFolderNameStr, + data: sharedFolderData, + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + ownerUsername: sharedFolder.owner, + revision: sharedFolder.revision, + uid: sharedFolderUidStr, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: "shared_folder_record", + canEdit: true, + canShare: true, + owner: sharedFolderRecord.owner, + ownerUid: "", + recordUid: recordUidStr, + sharedFolderUid: sharedFolderUidStr, + }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + kind: "record", + uid: recordUidStr, + data: decryptedRecordData, + }) + ) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([{ + kind: "record", + parentUid: sharedFolderUidStr, + uid: recordUidStr, + }]) + }) + }) + it('saves the record data when an existing record is added to a shared folder', async () => { + const decryptedRecordData = { + title: 'an existing record moved to a shared folder' + } + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: syncDownUser.username, + ownerAccountUid: syncDownUser.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + } + const {recordKey, recordUid} = await syncDownResponseBuilder.addRecord(decryptedRecordData, decryptedSharedFolderKey) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderRecord: Vault.ISharedFolderRecord = { + owner: true, + recordKey, + recordUid, + sharedFolderUid, + ownerAccountUid: new Uint8Array([]), + } + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + folderUid: new Uint8Array([]), + sharedFolderUid, + recordUid, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + sharedFolderUid, + folderUid: new Uint8Array([]), + revision: Date.now() + } + syncDownResponseBuilder.addRemovedRecord(recordUid) + syncDownResponseBuilder.addSharedFolder(sharedFolder) + syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder', + name: sharedFolderNameStr, + data: sharedFolderData, + defaultCanEdit: sharedFolder.defaultCanEdit, + defaultCanShare: sharedFolder.defaultCanReshare, + defaultManageRecords: sharedFolder.defaultManageRecords, + defaultManageUsers: sharedFolder.defaultManageUsers, + ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), + ownerUsername: sharedFolder.owner, + revision: sharedFolder.revision, + uid: sharedFolderUidStr, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: "shared_folder_record", + canEdit: true, + canShare: true, + owner: sharedFolderRecord.owner, + ownerUid: "", + recordUid: recordUidStr, + sharedFolderUid: sharedFolderUidStr, + }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + kind: "record", + uid: recordUidStr, + data: decryptedRecordData, + }) + ) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([{ + kind: "record", + parentUid: sharedFolderUidStr, + uid: recordUidStr, + }]) + }) + expect(storage.delete).toHaveBeenCalledWith('record', recordUidStr) + }) + it('deletes the record data when a child record is deleted from a shared folder', async () => { + const recordUid = platform.getRandomBytes(16) + const recordUidStr = webSafe64FromBytes(recordUid) + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: syncDownUser.username, + ownerAccountUid: syncDownUser.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + } + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid, + folderUid: new Uint8Array([]) + } + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + sharedFolderUid, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + sharedFolderUid, + folderUid: new Uint8Array([]), + revision: Date.now() + } + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + syncDownResponseBuilder.addSharedFolder(sharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + "": new Set([recordUidStr]), + [sharedFolderUidStr]: new Set([recordUidStr]) + }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + uid: sharedFolderUidStr, + kind: "shared_folder", + revision: sharedFolder.revision, + }) + ) + }) + it('updates the record data when it is moved out of a shared folder (moved to the root vault)', async () => { + const decryptedRecordData = { + title: "a record removed from a shared folder and moved to a root vault" + } + const {recordUid, recordKey, record} = await syncDownResponseBuilder.addRecord(decryptedRecordData) + const recordUidStr = webSafe64FromBytes(recordUid) + const recordMetadata: Vault.IRecordMetaData = { + recordUid, + recordKey, + recordKeyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM, + } + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) + const sharedFolderNameStr = 'a new shared folder' + const sharedFolderData = { + name: sharedFolderNameStr, + } + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: syncDownUser.username, + ownerAccountUid: syncDownUser.accountUid, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + } + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid, + folderUid: new Uint8Array([]) + } + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + sharedFolderUid, + } + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + sharedFolderUid, + folderUid: new Uint8Array([]), + revision: Date.now() + } + syncDownResponseBuilder.addRecordMetadata(recordMetadata) + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + syncDownResponseBuilder.addSharedFolder(sharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "record", + uid: recordUidStr, + revision: record.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "metadata", + uid: recordUidStr, + })) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + "": new Set([recordUidStr]), + [sharedFolderUidStr]: new Set([recordUidStr]) + }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + uid: sharedFolderUidStr, + kind: "shared_folder", + revision: sharedFolder.revision, + }) + ) + }) + }) + describe('Shared-Folder Folders', () => { + it.each([ + "saves the folder data when a new shared-folder folder is created in a shared folder", + "saves the folder data when an exisiting shared folder folder is edited in the same shared folder", + ])('%s', async () => { + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) + const folderUid = platform.getRandomBytes(16) + const folderUidStr = webSafe64FromBytes(folderUid) + const decryptedSharedFolderFolderKey = platform.getRandomBytes(32) + const sharedFolderFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderFolderKey, decryptedSharedFolderKey, true) + const folderName = 'an existing user folder' + const decryptedFolderData = { name: folderName } + const sharedFolderFolder: Vault.ISharedFolderFolder = { + sharedFolderUid, + folderUid, + sharedFolderFolderKey, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedFolderData)), decryptedSharedFolderFolderKey, true), + // either empty or the parent shared folder's uid if the folder is the direct child of the shared folder (level 0) + parentUid: new Uint8Array([]), + } + syncDownResponseBuilder.addRemovedUserFolder(folderUid) + syncDownResponseBuilder.addSharedFolderFolder(sharedFolderFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_folder', + data: decryptedFolderData, + revision: sharedFolderFolder.revision, + sharedFolderUid: sharedFolderUidStr, + uid: folderUidStr, + }) + }) + it('saves the folder data when an existing user folder is moved to a shared folder (the user folder gets converted to a shared-folder folder)', async () => { + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) + const folderUid = platform.getRandomBytes(16) + const folderUidStr = webSafe64FromBytes(folderUid) + const decryptedSharedFolderFolderKey = platform.getRandomBytes(32) + const sharedFolderFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderFolderKey, decryptedSharedFolderKey, true) + const folderName = 'an existing user folder' + const decryptedFolderData = { name: folderName } + const sharedFolderFolder: Vault.ISharedFolderFolder = { + sharedFolderUid, + folderUid, + sharedFolderFolderKey, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedFolderData)), decryptedSharedFolderFolderKey, true), + parentUid: sharedFolderUid, + } + syncDownResponseBuilder.addRemovedUserFolder(folderUid) + syncDownResponseBuilder.addSharedFolderFolder(sharedFolderFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_folder', + data: decryptedFolderData, + revision: sharedFolderFolder.revision, + sharedFolderUid: sharedFolderUidStr, + uid: folderUidStr, + }) + expect(storage.delete).toHaveBeenCalledWith('user_folder', folderUidStr) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([{ + kind: "shared_folder_folder", + parentUid: sharedFolderUidStr, + uid: folderUidStr + }]) + }) + }) + it('deletes the folder data when a shared-folder folder is deleted from a shared folder - empty folder', async () => { + const folderUid = platform.getRandomBytes(16) + const folderUidStr = webSafe64FromBytes(folderUid) + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolderFolder: Vault.ISharedFolderFolder = { + folderUid, + sharedFolderUid, + parentUid: new Uint8Array([]) + } + syncDownResponseBuilder.addRemovedSharedFolderFolder(sharedFolderFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.delete).toHaveBeenCalledWith("user_folder", folderUidStr) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [folderUidStr]: "*", + [sharedFolderUidStr]: new Set([folderUidStr]) + }) + }) + it('deletes the folder data when a shared-folder folder is deleted from a shared folder - folder with child records and child shared-folder folders', async () => { + /* + shared-folder folder A/ <-- contains a record C + └── shared-folder folder B/ <-- contains a record D + */ + }) + it('does not allow to take the shared-folder folder out from the shared folder', () => {}) + }) }) From 735b11d078d9f756428867b0d256680d861d95ac Mon Sep 17 00:00:00 2001 From: Hoseong Lee Date: Wed, 24 Dec 2025 16:31:36 -0600 Subject: [PATCH 2/5] refactored the addSharedFolder and addSharedFolderFolder methods to streamline the code --- .../src/__tests__/SyncDownResponseBuilder.ts | 75 +++- keeperapi/src/__tests__/vault.test.ts | 370 ++++++++---------- 2 files changed, 241 insertions(+), 204 deletions(-) diff --git a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts index 03f0019..4711794 100644 --- a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts +++ b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts @@ -1,4 +1,4 @@ -import {Vault} from "../proto"; +import {Records, Vault} from "../proto"; import {platform, Platform} from "../platform"; import {Auth} from "../auth"; @@ -19,6 +19,24 @@ type DecryptedSecurityScoreDataData = { version: number, } +type DecryptedSharedFolderFolderData = { + name: string // folder name +} + +type DecryptedSharedFolderData = { + name: string +} + +type UserInfo = { + username: string + accountUid: Uint8Array +} + +type SharedFolderPermissionData = Pick< + Vault.ISharedFolder, + "defaultCanEdit" | "defaultCanReshare" | "defaultManageUsers" | "defaultManageRecords" +> + export class SyncDownResponseBuilder { private readonly data: Vault.ISyncDownResponse; private readonly platform: Platform @@ -132,8 +150,35 @@ export class SyncDownResponseBuilder { this.data.removedUserFolderRecords?.push({recordUid, folderUid}) } - addSharedFolder(sharedFolder: Vault.ISharedFolder) { + async addSharedFolder( + decryptedSharedFolderData: DecryptedSharedFolderData, + userInfo: UserInfo, + permissionData: SharedFolderPermissionData, + encryptionkey?: Uint8Array + ) { + const sharedFolderUid = platform.getRandomBytes(16) + const decryptedSharedFolderKey = platform.getRandomBytes(32) + let sharedFolderKey: Uint8Array + if (!encryptionkey) { + sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, encryptionkey ? encryptionkey : this.auth.dataKey!, true) + } else {// normally when a shared folder is shared to a team + sharedFolderKey = platform.publicEncrypt(decryptedSharedFolderKey, platform.bytesToBase64(encryptionkey)) + } + const sharedFolder: Vault.ISharedFolder = { + sharedFolderUid, + sharedFolderKey, + owner: userInfo.username, + ownerAccountUid: userInfo.accountUid, + keyType: encryptionkey ? Records.RecordKeyType.NO_KEY : Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + name: await platform.aesCbcEncrypt(platform.stringToBytes(decryptedSharedFolderData.name), decryptedSharedFolderKey, true), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedSharedFolderData)), decryptedSharedFolderKey, true), + ...permissionData, + } + this.data.sharedFolders?.push(sharedFolder) + + return {sharedFolderUid, sharedFolder, sharedFolderKey, decryptedSharedFolderKey} } addSharedFolderUser(sharedFolderUser: Vault.ISharedFolderUser) { @@ -168,8 +213,32 @@ export class SyncDownResponseBuilder { this.data.removedSharedFolderRecords?.push(sharedFolderRecord) } - addSharedFolderFolder(sharedFolderFolder: Vault.ISharedFolderFolder) { + async addSharedFolderFolder( + decryptedSharedFolderFolderData: DecryptedSharedFolderFolderData, + sharedFolderUid: Uint8Array, + decryptedSharedFolderKey: Uint8Array, + parentUid: Uint8Array = new Uint8Array([]) + ) { + const sharedFolderFolderUid = platform.getRandomBytes(16) + const decryptedSharedFolderFolderKey = platform.getRandomBytes(32) + const sharedFolderFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderFolderKey, decryptedSharedFolderKey, true) + const sharedFolderFolder: Vault.ISharedFolderFolder = { + sharedFolderUid, + folderUid: sharedFolderFolderUid, + sharedFolderFolderKey, + keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + revision: Date.now(), + data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedSharedFolderFolderData)), decryptedSharedFolderFolderKey, true), + // either empty or the parent shared folder's uid if the folder is the direct child of the shared folder (level 0) + parentUid, + } + this.data.sharedFolderFolders?.push(sharedFolderFolder) + + return { + sharedFolderFolderUid, + sharedFolderFolder, + } } addRemovedSharedFolderFolder(removedSharedFolderFolder: Vault.ISharedFolderFolder) { diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index 1004e06..192da9c 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -518,28 +518,15 @@ describe('Sync Down', () => { }) describe('Shared Folders', () => { it('saves the shared folder data when a new shared folder is created by the user', async () => { - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey= await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: "a new shared folder", } - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - sharedFolderKey, - owner: syncDownUser.username, - ownerAccountUid: syncDownUser.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, { defaultCanEdit: false, defaultCanReshare: false, defaultManageUsers: false, defaultManageRecords: false, - } - syncDownResponseBuilder.addSharedFolder(sharedFolder) + }) const sharedFolderUser: Vault.ISharedFolderUser = { // if the data is the current sync user, the username and accountUid are empty username: '', @@ -564,7 +551,7 @@ describe('Sync Down', () => { kind: 'shared_folder', uid: webSafe64FromBytes(sharedFolderUid), data: sharedFolderData, - name: sharedFolderNameStr, + name: sharedFolderData.name, revision: sharedFolder.revision, ownerUsername: sharedFolder.owner, ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), @@ -584,28 +571,15 @@ describe('Sync Down', () => { expect(storage.addDependencies).toHaveBeenCalledWith({}) }) it('saves the shared folder data when the user is added to the folder', async () => { - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: "a shared folder", } - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - sharedFolderKey, - owner: anotherUserA.username, - ownerAccountUid: anotherUserA.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, anotherUserA, { defaultCanEdit: false, defaultCanReshare: false, defaultManageUsers: false, defaultManageRecords: false, - } - syncDownResponseBuilder.addSharedFolder(sharedFolder) + }) const sharedFolderUserA: Vault.ISharedFolderUser = { // if the data is the current sync user, the username and accountUid are empty username: '', @@ -628,7 +602,6 @@ describe('Sync Down', () => { sharedFolderUid, folderUid: new Uint8Array([]), // root folder } - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ @@ -639,7 +612,7 @@ describe('Sync Down', () => { kind: 'shared_folder', uid: webSafe64FromBytes(sharedFolderUid), data: sharedFolderData, - name: sharedFolderNameStr, + name: sharedFolderData.name, revision: sharedFolder.revision, ownerUsername: sharedFolder.owner, ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), @@ -667,20 +640,22 @@ describe('Sync Down', () => { expect(storage.addDependencies).toHaveBeenCalledWith({}) }) it("saves the shared folder data when the user's team is added to the folder", async () => { - const sharedFolderNameStr = 'a shared folder through team access' const sharedFolderData = { - name: sharedFolderNameStr, + name: "a shared folder through team access", } - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const teamUid = platform.getRandomBytes(16) const teamUidStr = webSafe64FromBytes(teamUid) const decryptedTeamKey = platform.getRandomBytes(32) const encryptedTeamKey = platform.publicEncrypt(decryptedTeamKey, platform.bytesToBase64(auth.privateKey!)) const decryptedTeamPrivateKeyPair = await platform.generateRSAKeyPair() const encryptedTeamPrivateKey= await platform.aesCbcEncrypt(decryptedTeamPrivateKeyPair.privateKey, decryptedTeamKey, true) - const sharedFolderKey = platform.publicEncrypt(decryptedSharedFolderKey, platform.bytesToBase64(decryptedTeamPrivateKeyPair.privateKey)) + const {sharedFolderUid, sharedFolder, sharedFolderKey} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, anotherUserA, { + defaultCanEdit: false, + defaultCanReshare: false, + defaultManageUsers: false, + defaultManageRecords: false, + }, decryptedTeamPrivateKeyPair.privateKey) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const team: Vault.ITeam = { teamUid, name: 'team name', @@ -699,19 +674,6 @@ describe('Sync Down', () => { restrictShare: false, restrictView: false, } - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - owner: anotherUserA.username, - ownerAccountUid: anotherUserA.accountUid, - keyType: Records.RecordKeyType.NO_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), - defaultCanEdit: false, - defaultCanReshare: false, - defaultManageUsers: false, - defaultManageRecords: false, - } const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { revision: sharedFolder.revision, sharedFolderUid, @@ -725,14 +687,13 @@ describe('Sync Down', () => { manageUsers: true, } const sharedFolderTeam: Vault.ISharedFolderTeam = { - name: sharedFolderNameStr, + name: sharedFolderData.name, manageUsers: false, manageRecords: false, teamUid, sharedFolderUid, } syncDownResponseBuilder.addTeam(team) - syncDownResponseBuilder.addSharedFolder(sharedFolder) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) syncDownResponseBuilder.addSharedFolderUser(sharedFolderUser) syncDownResponseBuilder.addSharedFolderTeam(sharedFolderTeam) @@ -753,7 +714,7 @@ describe('Sync Down', () => { kind: 'shared_folder', uid: webSafe64FromBytes(sharedFolderUid), data: sharedFolderData, - name: sharedFolderNameStr, + name: sharedFolderData.name, revision: sharedFolder.revision, ownerUsername: sharedFolder.owner, ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), @@ -773,7 +734,7 @@ describe('Sync Down', () => { expect(storage.put).toHaveBeenCalledWith({ kind: 'shared_folder_team', teamUid: teamUidStr, - name: sharedFolderNameStr, + name: sharedFolderData.name, sharedFolderUid: sharedFolderUidStr, manageRecords: sharedFolderTeam.manageRecords, manageUsers: sharedFolderTeam.manageUsers, @@ -787,33 +748,20 @@ describe('Sync Down', () => { }) }) it('saves the shared folder data when the folder data is updated', async () => { - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'an existing shared folder data updated' const sharedFolderData = { - name: sharedFolderNameStr, + name: 'an existing shared folder data updated', } - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - sharedFolderKey, - owner: anotherUserA.username, - ownerAccountUid: anotherUserA.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, anotherUserA, { defaultCanEdit: false, defaultCanReshare: false, defaultManageUsers: false, defaultManageRecords: false, - } + }) const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { revision: sharedFolder.revision, sharedFolderUid, folderUid: new Uint8Array([]), // root folder } - syncDownResponseBuilder.addSharedFolder(sharedFolder) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ @@ -824,7 +772,7 @@ describe('Sync Down', () => { kind: 'shared_folder', uid: webSafe64FromBytes(sharedFolderUid), data: sharedFolderData, - name: sharedFolderNameStr, + name: sharedFolderData.name, revision: sharedFolder.revision, ownerUsername: sharedFolder.owner, ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), @@ -877,28 +825,20 @@ describe('Sync Down', () => { const decryptedRecordData = { title: 'an existing record moved to a shared folder' } - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: 'a shared folder', } - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) - const sharedFolder: Vault.ISharedFolder = { + const { sharedFolderUid, - sharedFolderKey, - owner: syncDownUser.username, - ownerAccountUid: syncDownUser.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), + sharedFolder, + decryptedSharedFolderKey, + } = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, { defaultCanEdit: false, defaultCanReshare: false, defaultManageUsers: false, defaultManageRecords: false, - } + }) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const {recordKey, recordUid} = await syncDownResponseBuilder.addRecord(decryptedRecordData, decryptedSharedFolderKey) const recordUidStr = webSafe64FromBytes(recordUid) const sharedFolderRecord: Vault.ISharedFolderRecord = { @@ -918,7 +858,6 @@ describe('Sync Down', () => { folderUid: new Uint8Array([]), revision: Date.now() } - syncDownResponseBuilder.addSharedFolder(sharedFolder) syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) @@ -927,19 +866,11 @@ describe('Sync Down', () => { auth, storage, }) - expect(storage.put).toHaveBeenCalledWith({ + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ kind: 'shared_folder', - name: sharedFolderNameStr, - data: sharedFolderData, - defaultCanEdit: sharedFolder.defaultCanEdit, - defaultCanShare: sharedFolder.defaultCanReshare, - defaultManageRecords: sharedFolder.defaultManageRecords, - defaultManageUsers: sharedFolder.defaultManageUsers, - ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), - ownerUsername: sharedFolder.owner, revision: sharedFolder.revision, uid: sharedFolderUidStr, - }) + })) expect(storage.put).toHaveBeenCalledWith({ kind: "shared_folder_record", canEdit: true, @@ -968,28 +899,15 @@ describe('Sync Down', () => { const decryptedRecordData = { title: 'an existing record moved to a shared folder' } - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: 'a shared folder', } - const sharedFolderUid = platform.getRandomBytes(16) - const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) - const sharedFolder: Vault.ISharedFolder = { + const { sharedFolderUid, - sharedFolderKey, - owner: syncDownUser.username, - ownerAccountUid: syncDownUser.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), - defaultCanEdit: false, - defaultCanReshare: false, - defaultManageUsers: false, - defaultManageRecords: false, - } + sharedFolder, + decryptedSharedFolderKey, + } = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const {recordKey, recordUid} = await syncDownResponseBuilder.addRecord(decryptedRecordData, decryptedSharedFolderKey) const recordUidStr = webSafe64FromBytes(recordUid) const sharedFolderRecord: Vault.ISharedFolderRecord = { @@ -1010,7 +928,6 @@ describe('Sync Down', () => { revision: Date.now() } syncDownResponseBuilder.addRemovedRecord(recordUid) - syncDownResponseBuilder.addSharedFolder(sharedFolder) syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) @@ -1019,19 +936,11 @@ describe('Sync Down', () => { auth, storage, }) - expect(storage.put).toHaveBeenCalledWith({ + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ kind: 'shared_folder', - name: sharedFolderNameStr, - data: sharedFolderData, - defaultCanEdit: sharedFolder.defaultCanEdit, - defaultCanShare: sharedFolder.defaultCanReshare, - defaultManageRecords: sharedFolder.defaultManageRecords, - defaultManageUsers: sharedFolder.defaultManageUsers, - ownerAccountUid: webSafe64FromBytes(sharedFolder.ownerAccountUid!), - ownerUsername: sharedFolder.owner, revision: sharedFolder.revision, uid: sharedFolderUidStr, - }) + })) expect(storage.put).toHaveBeenCalledWith({ kind: "shared_folder_record", canEdit: true, @@ -1060,24 +969,11 @@ describe('Sync Down', () => { it('deletes the record data when a child record is deleted from a shared folder', async () => { const recordUid = platform.getRandomBytes(16) const recordUidStr = webSafe64FromBytes(recordUid) - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: 'a new shared folder', } - const sharedFolderUid = platform.getRandomBytes(16) + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - sharedFolderKey, - owner: syncDownUser.username, - ownerAccountUid: syncDownUser.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), - } const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { recordUid, sharedFolderUid, @@ -1095,7 +991,6 @@ describe('Sync Down', () => { syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) - syncDownResponseBuilder.addSharedFolder(sharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -1124,24 +1019,11 @@ describe('Sync Down', () => { recordKey, recordKeyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM, } - const decryptedSharedFolderKey = platform.getRandomBytes(32) - const sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, auth.dataKey!, true) - const sharedFolderNameStr = 'a new shared folder' const sharedFolderData = { - name: sharedFolderNameStr, + name: 'a new shared folder', } - const sharedFolderUid = platform.getRandomBytes(16) + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) - const sharedFolder: Vault.ISharedFolder = { - sharedFolderUid, - sharedFolderKey, - owner: syncDownUser.username, - ownerAccountUid: syncDownUser.accountUid, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - name: await platform.aesCbcEncrypt(platform.stringToBytes(sharedFolderNameStr), decryptedSharedFolderKey, true), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(sharedFolderData)), decryptedSharedFolderKey, true), - } const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { recordUid, sharedFolderUid, @@ -1160,7 +1042,6 @@ describe('Sync Down', () => { syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) - syncDownResponseBuilder.addSharedFolder(sharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -1191,30 +1072,16 @@ describe('Sync Down', () => { describe('Shared-Folder Folders', () => { it.each([ "saves the folder data when a new shared-folder folder is created in a shared folder", - "saves the folder data when an exisiting shared folder folder is edited in the same shared folder", + "saves the folder data when an existing shared folder folder is edited in the same shared folder", ])('%s', async () => { const sharedFolderUid = platform.getRandomBytes(16) const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const decryptedSharedFolderKey = platform.getRandomBytes(32) const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) - const folderUid = platform.getRandomBytes(16) - const folderUidStr = webSafe64FromBytes(folderUid) - const decryptedSharedFolderFolderKey = platform.getRandomBytes(32) - const sharedFolderFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderFolderKey, decryptedSharedFolderKey, true) - const folderName = 'an existing user folder' - const decryptedFolderData = { name: folderName } - const sharedFolderFolder: Vault.ISharedFolderFolder = { - sharedFolderUid, - folderUid, - sharedFolderFolderKey, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedFolderData)), decryptedSharedFolderFolderKey, true), - // either empty or the parent shared folder's uid if the folder is the direct child of the shared folder (level 0) - parentUid: new Uint8Array([]), - } - syncDownResponseBuilder.addRemovedUserFolder(folderUid) - syncDownResponseBuilder.addSharedFolderFolder(sharedFolderFolder) + const decryptedFolderData = { name: "an existing user folder" } + const {sharedFolderFolderUid, sharedFolderFolder} = await syncDownResponseBuilder.addSharedFolderFolder(decryptedFolderData, sharedFolderUid, decryptedSharedFolderKey) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) + syncDownResponseBuilder.addRemovedUserFolder(sharedFolderFolderUid) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') await syncDown({ @@ -1226,7 +1093,7 @@ describe('Sync Down', () => { data: decryptedFolderData, revision: sharedFolderFolder.revision, sharedFolderUid: sharedFolderUidStr, - uid: folderUidStr, + uid: sharedFolderFolderUidStr, }) }) it('saves the folder data when an existing user folder is moved to a shared folder (the user folder gets converted to a shared-folder folder)', async () => { @@ -1234,23 +1101,11 @@ describe('Sync Down', () => { const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const decryptedSharedFolderKey = platform.getRandomBytes(32) const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) - const folderUid = platform.getRandomBytes(16) - const folderUidStr = webSafe64FromBytes(folderUid) - const decryptedSharedFolderFolderKey = platform.getRandomBytes(32) - const sharedFolderFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderFolderKey, decryptedSharedFolderKey, true) const folderName = 'an existing user folder' const decryptedFolderData = { name: folderName } - const sharedFolderFolder: Vault.ISharedFolderFolder = { - sharedFolderUid, - folderUid, - sharedFolderFolderKey, - keyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, - revision: Date.now(), - data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedFolderData)), decryptedSharedFolderFolderKey, true), - parentUid: sharedFolderUid, - } - syncDownResponseBuilder.addRemovedUserFolder(folderUid) - syncDownResponseBuilder.addSharedFolderFolder(sharedFolderFolder) + const {sharedFolderFolderUid, sharedFolderFolder} = await syncDownResponseBuilder.addSharedFolderFolder(decryptedFolderData, sharedFolderUid, decryptedSharedFolderKey, sharedFolderUid) + syncDownResponseBuilder.addRemovedUserFolder(sharedFolderFolderUid) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') await syncDown({ @@ -1262,14 +1117,14 @@ describe('Sync Down', () => { data: decryptedFolderData, revision: sharedFolderFolder.revision, sharedFolderUid: sharedFolderUidStr, - uid: folderUidStr, + uid: sharedFolderFolderUidStr, }) - expect(storage.delete).toHaveBeenCalledWith('user_folder', folderUidStr) + expect(storage.delete).toHaveBeenCalledWith('user_folder', sharedFolderFolderUidStr) expect(storage.addDependencies).toHaveBeenCalledWith({ [sharedFolderUidStr]: new Set([{ kind: "shared_folder_folder", parentUid: sharedFolderUidStr, - uid: folderUidStr + uid: sharedFolderFolderUidStr, }]) }) }) @@ -1300,7 +1155,120 @@ describe('Sync Down', () => { shared-folder folder A/ <-- contains a record C └── shared-folder folder B/ <-- contains a record D */ + const sharedFolderData = { name: "a parent shared folder" } + const sharedFolderFolderUidA = platform.getRandomBytes(16) + const sharedFolderFolderUidAStr = webSafe64FromBytes(sharedFolderFolderUidA) + const sharedFolderFolderUidB = platform.getRandomBytes(16) + const sharedFolderFolderUidBStr = webSafe64FromBytes(sharedFolderFolderUidB) + const recordUidC = platform.getRandomBytes(16) + const recordUidCStr = webSafe64FromBytes(recordUidC) + const recordUidD = platform.getRandomBytes(16) + const recordUidDStr = webSafe64FromBytes(recordUidD) + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const removedSharedFolderFolderA: Vault.ISharedFolderFolder = { + folderUid: sharedFolderFolderUidA, + sharedFolderUid, + parentUid: new Uint8Array([]), + } + const removedSharedFolderFolderB: Vault.ISharedFolderFolder = { + folderUid: sharedFolderFolderUidB, + parentUid: sharedFolderFolderUidA, + sharedFolderUid, + } + const removedSharedFolderRecordC: Vault.ISharedFolderRecord = { + recordUid: recordUidC, + sharedFolderUid, + } + const removedSharedFolderRecordD: Vault.ISharedFolderRecord = { + recordUid: recordUidD, + sharedFolderUid, + } + + const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { + revision: Date.now(), + sharedFolderUid, + folderUid: new Uint8Array([]), + } + syncDownResponseBuilder.addRemovedSharedFolderFolder(removedSharedFolderFolderA) + syncDownResponseBuilder.addRemovedSharedFolderFolder(removedSharedFolderFolderB) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecordC) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecordD) + syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([ + sharedFolderFolderUidAStr, + sharedFolderFolderUidBStr, + recordUidCStr, + recordUidDStr, + ]), + [sharedFolderFolderUidAStr]: "*", + [sharedFolderFolderUidBStr]: "*", + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder", + uid: sharedFolderUidStr, + revision: sharedFolder.revision, + })) }) it('does not allow to take the shared-folder folder out from the shared folder', () => {}) + it('updates the folder data when a shared-folder is moved into another shared-folder folder: both are still under the same shared folder', async () => { + /* + [before update] + shared-folder folder A/ + shared-folder folder B/ <-- contains a record C + [after update] + shared-folder folder A/ + └── shared-folder folder B/ <-- contains a record C + */ + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) + const decryptedFolderData = { name: "an existing user folder" } + const recordUidC = platform.getRandomBytes(16) + const recordUidCStr = webSafe64FromBytes(recordUidC) + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolderFolderAUid = platform.getRandomBytes(16) + const sharedFolderFolderAUidStr = webSafe64FromBytes(sharedFolderFolderAUid) + const {sharedFolderFolderUid: sharedFolderFolderBUid, sharedFolderFolder } = await syncDownResponseBuilder.addSharedFolderFolder(decryptedFolderData, sharedFolderUid, decryptedSharedFolderKey, sharedFolderFolderAUid) + const sharedFolderFolderBUidStr = webSafe64FromBytes(sharedFolderFolderBUid) + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid: recordUidC, + sharedFolderUid, + folderUid: sharedFolderFolderBUid, + revision: Date.now(), + } + await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderFolderBUidStr]: new Set([{ + kind: "record", + parentUid: sharedFolderFolderBUidStr, + uid: recordUidCStr, + }]), + [sharedFolderFolderAUidStr]: new Set([{ + kind: "shared_folder_folder", + parentUid: sharedFolderFolderAUidStr, + uid: sharedFolderFolderBUidStr, + }]) + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder_folder", + uid: sharedFolderFolderBUidStr, + revision: sharedFolderFolder.revision, + sharedFolderUid: sharedFolderUidStr, + })) + }) + it('saves the record data when a new record data is added to a shared-folder folder', async () => {}) }) }) From 8cc2e08f853a3ff91d948746f02a0073a9b55e49 Mon Sep 17 00:00:00 2001 From: Hoseong Lee Date: Fri, 26 Dec 2025 14:02:48 -0600 Subject: [PATCH 3/5] added sync down unit tests for shared-folder folder --- .../src/__tests__/SyncDownResponseBuilder.ts | 29 +- keeperapi/src/__tests__/vault.test.ts | 505 +++++++++++++++--- 2 files changed, 460 insertions(+), 74 deletions(-) diff --git a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts index 4711794..2ceadbb 100644 --- a/keeperapi/src/__tests__/SyncDownResponseBuilder.ts +++ b/keeperapi/src/__tests__/SyncDownResponseBuilder.ts @@ -80,8 +80,8 @@ export class SyncDownResponseBuilder { } } - addUserFolderRecord(recordUid: Uint8Array, folderUid?: Uint8Array) { - this.data.userFolderRecords?.push({recordUid, folderUid, revision: Date.now()}) + addUserFolderRecord(userFolderRecord: Vault.IUserFolderRecord) { + this.data.userFolderRecords?.push(userFolderRecord) } addRecordMetadata(recordMetadata: Vault.IRecordMetaData) { @@ -130,7 +130,8 @@ export class SyncDownResponseBuilder { recordKey, recordUid, record, - decryptedSecurityScoreDataData + decryptedSecurityScoreDataData, + decryptedRecordKey, } } @@ -154,22 +155,25 @@ export class SyncDownResponseBuilder { decryptedSharedFolderData: DecryptedSharedFolderData, userInfo: UserInfo, permissionData: SharedFolderPermissionData, - encryptionkey?: Uint8Array + options?: { + encryptionKey?: Uint8Array + parentFolderUid?: Uint8Array + }, ) { const sharedFolderUid = platform.getRandomBytes(16) const decryptedSharedFolderKey = platform.getRandomBytes(32) let sharedFolderKey: Uint8Array - if (!encryptionkey) { - sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, encryptionkey ? encryptionkey : this.auth.dataKey!, true) + if (!options?.encryptionKey) { + sharedFolderKey = await platform.aesCbcEncrypt(decryptedSharedFolderKey, options?.encryptionKey ? options?.encryptionKey : this.auth.dataKey!, true) } else {// normally when a shared folder is shared to a team - sharedFolderKey = platform.publicEncrypt(decryptedSharedFolderKey, platform.bytesToBase64(encryptionkey)) + sharedFolderKey = platform.publicEncrypt(decryptedSharedFolderKey, platform.bytesToBase64(options.encryptionKey)) } const sharedFolder: Vault.ISharedFolder = { sharedFolderUid, sharedFolderKey, owner: userInfo.username, ownerAccountUid: userInfo.accountUid, - keyType: encryptionkey ? Records.RecordKeyType.NO_KEY : Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, + keyType: options?.encryptionKey ? Records.RecordKeyType.NO_KEY : Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY, revision: Date.now(), name: await platform.aesCbcEncrypt(platform.stringToBytes(decryptedSharedFolderData.name), decryptedSharedFolderKey, true), data: await platform.aesCbcEncrypt(platform.stringToBytes(JSON.stringify(decryptedSharedFolderData)), decryptedSharedFolderKey, true), @@ -177,6 +181,11 @@ export class SyncDownResponseBuilder { } this.data.sharedFolders?.push(sharedFolder) + this.data.userFolderSharedFolders?.push({ + sharedFolderUid, + revision: sharedFolder.revision, + folderUid: options?.parentFolderUid ? options?.parentFolderUid : new Uint8Array([]), + }) return {sharedFolderUid, sharedFolder, sharedFolderKey, decryptedSharedFolderKey} } @@ -197,10 +206,6 @@ export class SyncDownResponseBuilder { this.data.sharedFolderTeams?.push(sharedFolderTeam) } - addUserFolderSharedFolder(userFolderSharedFolder: Vault.IUserFolderSharedFolder) { - this.data.userFolderSharedFolders?.push(userFolderSharedFolder) - } - addTeam(team: Vault.ITeam) { this.data.teams?.push(team) } diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index 192da9c..82337e6 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -64,7 +64,11 @@ describe('Sync Down', () => { } const { recordUid, recordKey } = await syncDownResponseBuilder.addRecord(decryptedRecordData) const recordUidStr = webSafe64FromBytes(recordUid) - syncDownResponseBuilder.addUserFolderRecord(recordUid) + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + revision: Date.now() + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) const recordMetadata: Vault.IRecordMetaData = { recordUid, recordKey, @@ -116,7 +120,11 @@ describe('Sync Down', () => { } const { recordUid, recordKey, record, decryptedSecurityScoreDataData } = await syncDownResponseBuilder.addRecord(decryptedRecordData) const recordUidStr = webSafe64FromBytes(recordUid) - syncDownResponseBuilder.addUserFolderRecord(recordUid) + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + revision: Date.now() + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) const recordMetadata: Vault.IRecordMetaData = { recordUid, recordKey, @@ -220,7 +228,11 @@ describe('Sync Down', () => { ownerUsername: anotherUserA.username, ownerAccountUid: anotherUserA.accountUid, } - syncDownResponseBuilder.addUserFolderRecord(recordUid) + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + revision: Date.now(), + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) syncDownResponseBuilder.addRecordMetadata(recordMetadata) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ @@ -430,7 +442,12 @@ describe('Sync Down', () => { ownerUsername: syncDownUser.username, ownerAccountUid: syncDownUser.accountUid, } - syncDownResponseBuilder.addUserFolderRecord(recordUid, folderUid) + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + folderUid, + revision: Date.now(), + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) syncDownResponseBuilder.addRecordMetadata(recordMetadata) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ @@ -467,7 +484,12 @@ describe('Sync Down', () => { const folderAUidStr = webSafe64FromBytes(folderAUid) const folderBUidStr = webSafe64FromBytes(folderBUid) const recordUidStr = webSafe64FromBytes(recordUid) - syncDownResponseBuilder.addUserFolderRecord(recordUid, folderBUid) + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + folderUid: folderBUid, + revision: Date.now(), + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) syncDownResponseBuilder.addRemovedUserFolderRecord(recordUid, folderAUid) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ @@ -536,12 +558,6 @@ describe('Sync Down', () => { manageUsers: true, } syncDownResponseBuilder.addSharedFolderUser(sharedFolderUser) - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - revision: sharedFolder.revision, - sharedFolderUid, - folderUid: new Uint8Array([]), // root folder - } - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -597,12 +613,6 @@ describe('Sync Down', () => { } syncDownResponseBuilder.addSharedFolderUser(sharedFolderUserA) syncDownResponseBuilder.addSharedFolderUser(sharedFolderUserB) - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - revision: sharedFolder.revision, - sharedFolderUid, - folderUid: new Uint8Array([]), // root folder - } - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -654,7 +664,7 @@ describe('Sync Down', () => { defaultCanReshare: false, defaultManageUsers: false, defaultManageRecords: false, - }, decryptedTeamPrivateKeyPair.privateKey) + }, {encryptionKey: decryptedTeamPrivateKeyPair.privateKey}) const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) const team: Vault.ITeam = { teamUid, @@ -674,11 +684,6 @@ describe('Sync Down', () => { restrictShare: false, restrictView: false, } - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - revision: sharedFolder.revision, - sharedFolderUid, - folderUid: new Uint8Array([]), // root folder - } const sharedFolderUser: Vault.ISharedFolderUser = { username: 'other user who owns the shared folder', accountUid: anotherUserA.accountUid, @@ -694,7 +699,6 @@ describe('Sync Down', () => { sharedFolderUid, } syncDownResponseBuilder.addTeam(team) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) syncDownResponseBuilder.addSharedFolderUser(sharedFolderUser) syncDownResponseBuilder.addSharedFolderTeam(sharedFolderTeam) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) @@ -757,12 +761,6 @@ describe('Sync Down', () => { defaultManageUsers: false, defaultManageRecords: false, }) - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - revision: sharedFolder.revision, - sharedFolderUid, - folderUid: new Uint8Array([]), // root folder - } - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -853,14 +851,8 @@ describe('Sync Down', () => { sharedFolderUid, recordUid, } - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - sharedFolderUid, - folderUid: new Uint8Array([]), - revision: Date.now() - } syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -922,15 +914,10 @@ describe('Sync Down', () => { sharedFolderUid, recordUid, } - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - sharedFolderUid, - folderUid: new Uint8Array([]), - revision: Date.now() - } + syncDownResponseBuilder.addRemovedRecord(recordUid) syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -983,14 +970,8 @@ describe('Sync Down', () => { recordUid, sharedFolderUid, } - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - sharedFolderUid, - folderUid: new Uint8Array([]), - revision: Date.now() - } syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -1010,7 +991,7 @@ describe('Sync Down', () => { }) it('updates the record data when it is moved out of a shared folder (moved to the root vault)', async () => { const decryptedRecordData = { - title: "a record removed from a shared folder and moved to a root vault" + title: "a record removed from a shared folder to a root vault" } const {recordUid, recordKey, record} = await syncDownResponseBuilder.addRecord(decryptedRecordData) const recordUidStr = webSafe64FromBytes(recordUid) @@ -1033,15 +1014,15 @@ describe('Sync Down', () => { recordUid, sharedFolderUid, } - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - sharedFolderUid, + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, folderUid: new Uint8Array([]), - revision: Date.now() + revision: Date.now(), } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) syncDownResponseBuilder.addRecordMetadata(recordMetadata) syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -1056,6 +1037,13 @@ describe('Sync Down', () => { kind: "metadata", uid: recordUidStr, })) + expect(storage.addDependencies).toHaveBeenCalledWith({ + "": new Set([{ + kind: "record", + parentUid: "", + uid: recordUidStr, + }]), + }) expect(storage.removeDependencies).toHaveBeenCalledWith({ "": new Set([recordUidStr]), [sharedFolderUidStr]: new Set([recordUidStr]) @@ -1068,6 +1056,90 @@ describe('Sync Down', () => { }) ) }) + it('updates the record data when it is moved out of a shared folder (moved to another shared folder)', async () => { + /* + [before update] + shared folder A/ <-- contains a record + shared folder B/ + [after update] + shared folder A/ + shared-folder folder B/ <-- the record moved from A to B + */ + const decryptedRecordData = { + title: "a record removed from a shared folder to a root vault" + } + const sharedFolderDataA = { + name: 'shared folder A', + } + const sharedFolderDataB = { + name: 'shared folder A', + } + const {sharedFolderUid: sharedFolderUidA, sharedFolder: sharedFolderA} = await syncDownResponseBuilder.addSharedFolder(sharedFolderDataA, syncDownUser, {}) + const {sharedFolderUid: sharedFolderUidB, sharedFolder: sharedFolderB, decryptedSharedFolderKey: decryptedSharedFolderKeyB} = await syncDownResponseBuilder.addSharedFolder(sharedFolderDataB, syncDownUser, {}) + const sharedFolderUidAStr = webSafe64FromBytes(sharedFolderUidA) + const sharedFolderUidBStr = webSafe64FromBytes(sharedFolderUidB) + const {recordUid, recordKey, record} = await syncDownResponseBuilder.addRecord(decryptedRecordData, decryptedSharedFolderKeyB) + const recordUidStr = webSafe64FromBytes(recordUid) + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid: sharedFolderUidA, + folderUid: new Uint8Array([]) + } + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + sharedFolderUid: sharedFolderUidA, + } + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid: sharedFolderUidB, + revision: Date.now(), + folderUid: new Uint8Array([]) + } + const sharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + recordKey, + sharedFolderUid: sharedFolderUidB, + owner: true, + ownerAccountUid: new Uint8Array([]), + } + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "record", + uid: recordUidStr, + revision: record.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'shared_folder_record', + recordUid: recordUidStr, + sharedFolderUid: sharedFolderUidBStr, + })) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + "": new Set([recordUidStr]), + [sharedFolderUidAStr]: new Set([recordUidStr]) + }) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + uid: sharedFolderUidAStr, + kind: "shared_folder", + revision: sharedFolderA.revision, + }) + ) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + uid: sharedFolderUidBStr, + kind: "shared_folder", + revision: sharedFolderB.revision, + }) + ) + }) }) describe('Shared-Folder Folders', () => { it.each([ @@ -1184,17 +1256,10 @@ describe('Sync Down', () => { recordUid: recordUidD, sharedFolderUid, } - - const userFolderSharedFolder: Vault.IUserFolderSharedFolder = { - revision: Date.now(), - sharedFolderUid, - folderUid: new Uint8Array([]), - } syncDownResponseBuilder.addRemovedSharedFolderFolder(removedSharedFolderFolderA) syncDownResponseBuilder.addRemovedSharedFolderFolder(removedSharedFolderFolderB) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecordC) syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecordD) - syncDownResponseBuilder.addUserFolderSharedFolder(userFolderSharedFolder) mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) await syncDown({ auth, @@ -1217,7 +1282,7 @@ describe('Sync Down', () => { })) }) it('does not allow to take the shared-folder folder out from the shared folder', () => {}) - it('updates the folder data when a shared-folder is moved into another shared-folder folder: both are still under the same shared folder', async () => { + it('updates the folder data when a shared-folder folder is moved into another shared-folder folder: both are still under the same shared folder', async () => { /* [before update] shared-folder folder A/ @@ -1269,6 +1334,322 @@ describe('Sync Down', () => { sharedFolderUid: sharedFolderUidStr, })) }) - it('saves the record data when a new record data is added to a shared-folder folder', async () => {}) + it('saves the record data when a new record data is added to a shared-folder folder', async () => { + const sharedFolderData = { + name: 'shared folder', + } + const sharedFolderFolderData = { + name: 'shared-folder folder', + } + const recordData = { + title: 'record' + } + const {sharedFolderUid, sharedFolder, decryptedSharedFolderKey} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const {sharedFolderFolderUid, sharedFolderFolder} = await syncDownResponseBuilder.addSharedFolderFolder(sharedFolderFolderData, sharedFolderUid, decryptedSharedFolderKey) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) + const {recordUid, record, recordKey} = await syncDownResponseBuilder.addRecord(recordData, decryptedSharedFolderKey) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + sharedFolderUid, + recordUid, + folderUid: sharedFolderFolderUid, + revision: Date.now(), + } + const sharedFolderRecord: Vault.ISharedFolderRecord = { + owner: true, + sharedFolderUid, + recordKey, + recordUid, + ownerAccountUid: new Uint8Array([]), + } + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder", + uid: sharedFolderUidStr, + revision: sharedFolder.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder_folder", + uid: sharedFolderFolderUidStr, + revision: sharedFolderFolder.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'record', + uid: recordUidStr, + revision: record.revision, + })) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_record', + recordUid: recordUidStr, + sharedFolderUid: sharedFolderUidStr, + canEdit: true, + canShare: true, + owner: sharedFolderRecord.owner, + ownerUid: webSafe64FromBytes(sharedFolderRecord.ownerAccountUid!), + ownerUsername: sharedFolderRecord.ownerUsername, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderFolderUidStr]: new Set([{ + kind: "record", + uid: recordUidStr, + parentUid: sharedFolderFolderUidStr, + }]) + }) + }) + it('saves the record data when an existing record is added to a shared-folder folder', async () => { + const sharedFolderData = { + name: 'shared folder', + } + const recordData = { + title: 'record' + } + const {sharedFolderUid, sharedFolder, decryptedSharedFolderKey} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const {recordUid, record, recordKey} = await syncDownResponseBuilder.addRecord(recordData, decryptedSharedFolderKey) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderFolderUid = platform.getRandomBytes(16) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + sharedFolderUid, + recordUid, + folderUid: sharedFolderFolderUid, + revision: Date.now(), + } + const sharedFolderRecord: Vault.ISharedFolderRecord = { + owner: true, + sharedFolderUid, + recordKey, + recordUid, + ownerAccountUid: new Uint8Array([]), + } + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + syncDownResponseBuilder.addSharedFolderRecord(sharedFolderRecord) + syncDownResponseBuilder.addRemovedRecord(recordUid) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder", + uid: sharedFolderUidStr, + revision: sharedFolder.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "record", + uid: recordUidStr, + revision: record.revision, + })) + expect(storage.put).toHaveBeenCalledWith({ + kind: 'shared_folder_record', + recordUid: recordUidStr, + sharedFolderUid: sharedFolderUidStr, + canEdit: true, + canShare: true, + owner: sharedFolderRecord.owner, + ownerUid: webSafe64FromBytes(sharedFolderRecord.ownerAccountUid!), + ownerUsername: sharedFolderRecord.ownerUsername, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderFolderUidStr]: new Set([{ + kind: "record", + uid: recordUidStr, + parentUid: sharedFolderFolderUidStr, + }]) + }) + expect(storage.delete).toHaveBeenCalledWith("record", recordUidStr) + }) + it('saves the record data when an existing child record of a shared-folder folder is updated', async () => { + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const decryptedSharedFolderKey = platform.getRandomBytes(32) + const recordData = { + title: 'a child record has been updated' + } + const {recordUid, record, recordKey} = await syncDownResponseBuilder.addRecord(recordData, decryptedSharedFolderKey) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderKey = await platform.aesGcmEncrypt(decryptedSharedFolderKey, auth.dataKey!) + await platform.unwrapKey(sharedFolderKey, sharedFolderUidStr, 'data', 'gcm', 'aes') + await platform.unwrapKey(recordKey, recordUidStr, sharedFolderUidStr, 'gcm', 'aes') + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'record', + uid: webSafe64FromBytes(recordUid), + revision: record.revision, + })) + }) + it('updates the record data when a child record of a shared-folder folder is deleted', async () => { + const recordUid = platform.getRandomBytes(16) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderData = { + name: 'shared folder', + } + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolderFolderUid = platform.getRandomBytes(16) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + sharedFolderUid, + recordUid, + } + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + folderUid: sharedFolderFolderUid, + sharedFolderUid, + recordUid, + } + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "shared_folder", + uid: sharedFolderUidStr, + revision: sharedFolder.revision, + })) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([recordUidStr]), + [sharedFolderFolderUidStr]: new Set([recordUidStr]), + }) + }) + it('updates the record data when a child record when its moved to another shared-folder folder within the same parent shared folder', async () => { + /* + [before update] + shared folder + |── shared-folder folder A/ <-- contains a record C + └── shared-folder folder B/ + [after update] + shared folder + |── shared-folder folder A/ + └── shared-folder folder B/ <-- contains a record C + */ + const recordUid = platform.getRandomBytes(16) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderUid = platform.getRandomBytes(16) + const sharedFolderFolderUidA = platform.getRandomBytes(16) + const sharedFolderFolderUidAStr = webSafe64FromBytes(sharedFolderFolderUidA) + const sharedFolderFolderUidB = platform.getRandomBytes(16) + const sharedFolderFolderUidBStr = webSafe64FromBytes(sharedFolderFolderUidB) + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + sharedFolderUid, + folderUid: sharedFolderFolderUidA, + recordUid, + } + const sharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + sharedFolderUid, + folderUid: sharedFolderFolderUidB, + recordUid, + revision: Date.now(), + } + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + syncDownResponseBuilder.addSharedFolderFolderRecord(sharedFolderFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.addDependencies).toHaveBeenCalledWith({ + [sharedFolderFolderUidBStr]: new Set([{ + kind: "record", + parentUid: sharedFolderFolderUidBStr, + uid: recordUidStr, + }]), + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderFolderUidAStr]: new Set([recordUidStr]), + }) + }) + it('updates the record data when a child record of a shared-folder folder is moved out of their parent shared folder', async () => { + /* + [before update] + root + └── shared folder + └── shared-folder folder/ <-- contains a record C + [after update] + root <-- contains a record C + └── shared folder + └── shared-folder folder/ + */ + const decryptedRecordData = { + title: "a record removed from a shared-folder folder to a root vault" + } + const {recordUid, recordKey, record} = await syncDownResponseBuilder.addRecord(decryptedRecordData) + const recordUidStr = webSafe64FromBytes(recordUid) + const recordMetadata: Vault.IRecordMetaData = { + recordUid, + recordKey, + recordKeyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM, + } + const sharedFolderData = { + name: 'a shared folder', + } + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, syncDownUser, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const sharedFolderFolderUid = platform.getRandomBytes(16) + const sharedFolderFolderUidStr = webSafe64FromBytes(sharedFolderFolderUid) + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid, + folderUid: sharedFolderFolderUid, + } + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + sharedFolderUid, + } + const userFolderRecord: Vault.IUserFolderRecord = { + recordUid, + folderUid: new Uint8Array([]), + revision: Date.now(), + } + syncDownResponseBuilder.addUserFolderRecord(userFolderRecord) + syncDownResponseBuilder.addRecordMetadata(recordMetadata) + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "record", + uid: recordUidStr, + revision: record.revision, + })) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: "metadata", + uid: recordUidStr, + })) + expect(storage.put).toHaveBeenCalledWith( + expect.objectContaining({ + uid: sharedFolderUidStr, + kind: "shared_folder", + revision: sharedFolder.revision, + }) + ) + expect(storage.addDependencies).toHaveBeenCalledWith({ + "": new Set([{ + kind: "record", + parentUid: "", + uid: recordUidStr, + }]), + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [sharedFolderUidStr]: new Set([recordUidStr]), + [sharedFolderFolderUidStr]: new Set([recordUidStr]), + }) + }) }) }) From c93b4df8e6ca5aa7a9b395728ebe12b3dbe0ff32 Mon Sep 17 00:00:00 2001 From: Hoseong Lee Date: Fri, 26 Dec 2025 17:03:22 -0600 Subject: [PATCH 4/5] added sync down unit tests for the directly-shared record + shared folder and owned record + shared folder --- keeperapi/src/__tests__/vault.test.ts | 110 ++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index 82337e6..59de21a 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -1652,4 +1652,114 @@ describe('Sync Down', () => { }) }) }) + describe('Owned Records + Shared Folders', () => { + // TODO(@hleekeeper): the test cases may differ after addressing BE-7056 + it.each([ + `deletes the shared folder and all child resources including the owned child records when a user is removed from the shared folder`, + `deletes the shared folder and all child resources including the owned child records when the shared folder is deleted (regardless or access type to the folder - user or team access)`, + ])(`%s`, async () => { + const sharedFolderUid = platform.getRandomBytes(16) + syncDownResponseBuilder.addRemovedSharedFolder(sharedFolderUid) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.delete).toHaveBeenCalledWith("shared_folder", webSafe64FromBytes(sharedFolderUid)) + }) + it(`deletes the shared folder and all child resources including the owned child records when a user's team is removed from the shared folder`, async () => { + const teamUid = platform.getRandomBytes(16) + const sharedFolderUid = platform.getRandomBytes(16) + const removedSharedFolderTeam: Vault.ISharedFolderTeam = { + teamUid, + sharedFolderUid, + } + syncDownResponseBuilder.addRemovedSharedFolderTeam(removedSharedFolderTeam) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [webSafe64FromBytes(sharedFolderUid)]: new Set([webSafe64FromBytes(teamUid)]) + }) + }) + }) + describe('Directly-Shared Records + Shared Folders', () => { + // TODO(@hleekeeper): the test cases may differ after addressing BE-7056 + it.each([ + `deletes the shared folder and all child resources except the directly-shared records when a user is removed from the shared folder`, + `deletes the shared folder and all child resources except the directly-shared records when the shared folder is deleted (regardless of folder access type - user or team access)`, + ])(`%s`, async () => { + const sharedFolderUid = platform.getRandomBytes(16) + syncDownResponseBuilder.addRemovedSharedFolder(sharedFolderUid) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.delete).toHaveBeenCalledWith("shared_folder", webSafe64FromBytes(sharedFolderUid)) + }) + it(`deletes the shared folder and all child resources except the directly-shared records when a user is removed from the shared folder`, async () => { + const teamUid = platform.getRandomBytes(16) + const sharedFolderUid = platform.getRandomBytes(16) + const removedSharedFolderTeam: Vault.ISharedFolderTeam = { + teamUid, + sharedFolderUid, + } + syncDownResponseBuilder.addRemovedSharedFolderTeam(removedSharedFolderTeam) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + [webSafe64FromBytes(sharedFolderUid)]: new Set([webSafe64FromBytes(teamUid)]) + }) + }) + it(`doesn't delete the folder data when the directly-shared record is unshared, including the record data (regardless of folder access type - user or team access)`, async () => { + const recordUid = platform.getRandomBytes(16) + syncDownResponseBuilder.addRemovedRecord(recordUid) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.delete).toHaveBeenCalledWith("record", webSafe64FromBytes(recordUid)) + }) + it(`doesn't delete the folder data when the directly-shared record is deleted, including the record data (regardless of folder access type - user or team access)`, async () => { + const recordUid = platform.getRandomBytes(16) + const recordUidStr = webSafe64FromBytes(recordUid) + const sharedFolderData = { + name: "shared folder" + } + const {sharedFolderUid, sharedFolder} = await syncDownResponseBuilder.addSharedFolder(sharedFolderData, anotherUserA, {}) + const sharedFolderUidStr = webSafe64FromBytes(sharedFolderUid) + const removedSharedFolderFolderRecord: Vault.ISharedFolderFolderRecord = { + recordUid, + sharedFolderUid, + folderUid: new Uint8Array([]), + } + const removedSharedFolderRecord: Vault.ISharedFolderRecord = { + recordUid, + sharedFolderUid, + } + syncDownResponseBuilder.addRemovedSharedFolderRecord(removedSharedFolderRecord) + syncDownResponseBuilder.addRemovedSharedFolderFolderRecord(removedSharedFolderFolderRecord) + mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build()) + await syncDown({ + auth, + storage, + }) + expect(storage.put).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'shared_folder', + uid: sharedFolderUidStr, + revision: sharedFolder.revision, + })) + expect(storage.removeDependencies).toHaveBeenCalledWith({ + "": new Set([recordUidStr]), + [sharedFolderUidStr]: new Set([recordUidStr]), + }) + }) + }) }) From 546f90e1d771f5349d22c2f08aeca11ceadee561 Mon Sep 17 00:00:00 2001 From: Hoseong Lee Date: Mon, 29 Dec 2025 13:27:32 -0600 Subject: [PATCH 5/5] minor fix --- keeperapi/src/__tests__/vault.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keeperapi/src/__tests__/vault.test.ts b/keeperapi/src/__tests__/vault.test.ts index 59de21a..4abc562 100644 --- a/keeperapi/src/__tests__/vault.test.ts +++ b/keeperapi/src/__tests__/vault.test.ts @@ -1072,7 +1072,7 @@ describe('Sync Down', () => { name: 'shared folder A', } const sharedFolderDataB = { - name: 'shared folder A', + name: 'shared folder B', } const {sharedFolderUid: sharedFolderUidA, sharedFolder: sharedFolderA} = await syncDownResponseBuilder.addSharedFolder(sharedFolderDataA, syncDownUser, {}) const {sharedFolderUid: sharedFolderUidB, sharedFolder: sharedFolderB, decryptedSharedFolderKey: decryptedSharedFolderKeyB} = await syncDownResponseBuilder.addSharedFolder(sharedFolderDataB, syncDownUser, {})