Skip to content

Commit b499069

Browse files
authored
BE-7008 Added unit tests for owned records created, updated, and deleted via sync down (#95)
* Added unit test cases for owned records added/edited/deleted through sync down routine * Added the test:syncDown command and registered it in the CI/CD pipeline * use the node platform instead of browser platform * upgraded the node version to 20 in Github Action * Removed the testUtils module - unneccessary * updated the test case messages * updated a test case of the Owned Records
1 parent ce0e49c commit b499069

5 files changed

Lines changed: 222 additions & 5 deletions

File tree

.github/workflows/main.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: Setup Node
1515
uses: actions/setup-node@v3
1616
with:
17-
node-version: '16'
17+
node-version: '20'
1818

1919
- name: Lib - Install
2020
run: npm i
@@ -34,9 +34,9 @@ jobs:
3434
env:
3535
NPM_TOKEN: ""
3636

37-
# - name: Lib - Run Tests
38-
# run: npm run test:ci
39-
# working-directory: ./keeperapi
37+
- name: Lib - Run Sync Down Test
38+
run: npm run test:syncDown
39+
working-directory: ./keeperapi
4040

4141
- name: Examples (node) - Installation
4242
run: npm run link-local

keeperapi/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"update-proto:es6": "pbjs -t static-module -w es6 -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto && pbts -o src/proto.d.ts src/proto.js",
1414
"update-proto:cjs": "pbjs -t json-module -w commonjs -o src/proto.js ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto && pbjs -t static-module -w commonjs ../../keeperapp-protobuf/APIRequest.proto ../../keeperapp-protobuf/AccountSummary.proto ../../keeperapp-protobuf/automator.proto ../../keeperapp-protobuf/breachwatch.proto ../../keeperapp-protobuf/client.proto ../../keeperapp-protobuf/externalservice.proto ../../keeperapp-protobuf/folder.proto ../../keeperapp-protobuf/push.proto ../../keeperapp-protobuf/record.proto ../../keeperapp-protobuf/servicelogger.proto ../../keeperapp-protobuf/ssocloud.proto ../../keeperapp-protobuf/token.proto ../../keeperapp-protobuf/upsell.proto ../../keeperapp-protobuf/SyncDown.proto ../../keeperapp-protobuf/BI.proto | pbts -o src/proto.d.ts -",
1515
"test": "jest",
16+
"test:syncDown": "npm run test -- --testPathPattern=vault.test.ts",
1617
"types": "tsc --watch",
1718
"types:ci": "tsc",
1819
"prepublishOnly": "rollup -c && cp src/proto.d.ts dist",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {Vault} from "../proto";
2+
3+
export class SyncDownResponseBuilder {
4+
private readonly data: Vault.ISyncDownResponse;
5+
6+
constructor() {
7+
this.data = {
8+
continuationToken: new Uint8Array([]),
9+
users: [],
10+
teams: [],
11+
userFolders: [],
12+
userFolderRecords: [],
13+
sharedFolders: [],
14+
sharedFolderUsers: [],
15+
sharedFolderTeams: [],
16+
sharedFolderRecords: [],
17+
sharedFolderFolderRecords: [],
18+
userFolderSharedFolders: [],
19+
sharedFolderFolders: [],
20+
recordMetaData: [],
21+
recordLinks: [],
22+
records: [],
23+
nonSharedData: [],
24+
breachWatchRecords: [],
25+
breachWatchSecurityData: [],
26+
securityScoreData: [],
27+
removedTeams: [],
28+
removedRecords: [],
29+
removedRecordLinks: [],
30+
removedUserFolders: [],
31+
removedUserFolderRecords: [],
32+
removedSharedFolderFolders: [],
33+
removedSharedFolderTeams: [],
34+
removedSharedFolderUsers: [],
35+
removedSharedFolderRecords: [],
36+
removedSharedFolderFolderRecords: [],
37+
removedSharedFolders: [],
38+
removedUsers: [],
39+
}
40+
}
41+
42+
addUserFolderRecord(userFolderRecord: Vault.IUserFolderRecord) {
43+
this.data.userFolderRecords?.push(userFolderRecord)
44+
return this
45+
}
46+
47+
addRecordMetadata(recordMetadata: Vault.IRecordMetaData) {
48+
this.data.recordMetaData?.push(recordMetadata)
49+
return this
50+
}
51+
52+
addRecord(record: Vault.IRecord) {
53+
this.data.records?.push(record)
54+
return this
55+
}
56+
57+
addRemovedRecord(recordUid: Uint8Array) {
58+
this.data.removedRecords?.push(recordUid)
59+
return this
60+
}
61+
62+
build() {
63+
return this.data
64+
}
65+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {syncDown, VaultStorage} from "../vault"
2+
import {Auth} from '../auth'
3+
import {nodePlatform} from "../node/platform"
4+
import {connectPlatform, platform} from "../platform";
5+
import {SyncDownResponseBuilder} from './SyncDownResponseBuilder'
6+
import {Records, Vault} from "../proto";
7+
import {webSafe64FromBytes} from "../utils";
8+
9+
describe('Sync Down', () => {
10+
let dataKey: Uint8Array
11+
let auth: Auth;
12+
let eccKeyPair: {privateKey: Uint8Array, publicKey: Uint8Array};
13+
let storage: VaultStorage;
14+
let mockSyncDownCommand: jest.MockedFunction<() => any>;
15+
let syncDownResponseBuilder: SyncDownResponseBuilder;
16+
beforeAll(async () => {
17+
connectPlatform(nodePlatform)
18+
dataKey = platform.getRandomBytes(32)
19+
eccKeyPair = await platform.generateECKeyPair()
20+
})
21+
describe('Owned Records', () => {
22+
beforeEach(() => {
23+
storage = {
24+
get: jest.fn(),
25+
addDependencies: jest.fn(),
26+
delete: jest.fn(),
27+
removeDependencies: jest.fn(),
28+
put: jest.fn(),
29+
saveObject: jest.fn(),
30+
saveKeyBytes: jest.fn(),
31+
} as unknown as VaultStorage;
32+
mockSyncDownCommand = jest.fn()
33+
auth = {
34+
dataKey,
35+
eccPrivateKey: eccKeyPair.privateKey,
36+
eccPublicKey: eccKeyPair.publicKey,
37+
executeRest: mockSyncDownCommand,
38+
} as unknown as Auth;
39+
syncDownResponseBuilder = new SyncDownResponseBuilder();
40+
})
41+
it('saves the record data when a new record is created by the user', async () => {
42+
const decryptedRecordKey = platform.getRandomBytes(32)
43+
const recordKey = await platform.aesGcmEncrypt(decryptedRecordKey, auth.dataKey!)
44+
const recordUid = platform.getRandomBytes(16)
45+
const recordUidStr = webSafe64FromBytes(recordUid)
46+
const decryptedRecordData = {
47+
title: 'test record',
48+
}
49+
const decodedRecordData = platform.stringToBytes(JSON.stringify(decryptedRecordData))
50+
const recordData = await platform.aesGcmEncrypt(decodedRecordData, decryptedRecordKey)
51+
const userFolderRecord: Vault.IUserFolderRecord = {
52+
recordUid,
53+
revision: 1,
54+
}
55+
const recordMetadata: Vault.IRecordMetaData = {
56+
recordUid,
57+
recordKey,
58+
owner: true,
59+
canEdit: true,
60+
canShare: true,
61+
recordKeyType: Records.RecordKeyType.ENCRYPTED_BY_DATA_KEY_GCM,
62+
ownerUsername: 'hlee+dev@keepersecurity.com',
63+
ownerAccountUid: platform.getRandomBytes(16),
64+
}
65+
const record: Vault.IRecord = {
66+
recordUid,
67+
version: 3,
68+
data: recordData,
69+
extra: new Uint8Array([]),
70+
}
71+
syncDownResponseBuilder
72+
.addUserFolderRecord(userFolderRecord)
73+
.addRecordMetadata(recordMetadata)
74+
.addRecord(record)
75+
mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build())
76+
await syncDown({
77+
auth,
78+
storage,
79+
})
80+
expect(storage.put).toHaveBeenCalledWith(
81+
expect.objectContaining({
82+
kind: 'metadata',
83+
uid: recordUidStr,
84+
owner: recordMetadata.owner,
85+
ownerUsername: recordMetadata.ownerUsername,
86+
})
87+
)
88+
expect(storage.put).toHaveBeenCalledWith(
89+
expect.objectContaining({
90+
kind: 'record',
91+
uid: recordUidStr,
92+
data: decryptedRecordData,
93+
})
94+
)
95+
expect(storage.addDependencies).toHaveBeenCalledWith({
96+
"": new Set([{
97+
kind: "record",
98+
"parentUid": "",
99+
uid: recordUidStr,
100+
}])
101+
})
102+
})
103+
104+
it('saves the new record data when an existing record is updated by the user', async () => {
105+
const decryptedRecordKey = platform.getRandomBytes(32)
106+
const recordKey = await platform.aesGcmEncrypt(decryptedRecordKey, auth.dataKey!)
107+
const recordUid = platform.getRandomBytes(16)
108+
const recordUidStr = webSafe64FromBytes(recordUid)
109+
await platform.unwrapKey(recordKey, recordUidStr, 'data', 'gcm', 'aes')
110+
const decryptedRecordData = {
111+
title: 'test record updated',
112+
}
113+
const decodedRecordData = platform.stringToBytes(JSON.stringify(decryptedRecordData))
114+
const recordData = await platform.aesGcmEncrypt(decodedRecordData, decryptedRecordKey)
115+
116+
const record: Vault.IRecord = {
117+
recordUid,
118+
version: 3,
119+
data: recordData,
120+
extra: new Uint8Array([]),
121+
}
122+
syncDownResponseBuilder
123+
.addRecord(record)
124+
mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build())
125+
await syncDown({
126+
auth,
127+
storage,
128+
})
129+
expect(storage.put).toHaveBeenCalledWith(
130+
expect.objectContaining({
131+
kind: 'record',
132+
uid: recordUidStr,
133+
data: decryptedRecordData,
134+
})
135+
)
136+
})
137+
138+
it('deletes the record data when an existing record is removed', async () => {
139+
const recordUid = platform.getRandomBytes(16)
140+
syncDownResponseBuilder
141+
.addRemovedRecord(recordUid)
142+
mockSyncDownCommand.mockResolvedValue(syncDownResponseBuilder.build())
143+
await syncDown({
144+
auth,
145+
storage,
146+
})
147+
expect(storage.delete).toHaveBeenCalledWith('record', webSafe64FromBytes(recordUid))
148+
})
149+
})
150+
})
151+

keeperapi/src/vault.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ const processUserFolders = async (folders: IUserFolder[], storage: VaultStorage,
468468
const processUserFolderRecords = async (folderRecords: IUserFolderRecord[], dependencies: Dependencies) => {
469469
for (const folderRecord of folderRecords as NN<IUserFolderRecord>[]) {
470470
const recUid = webSafe64FromBytes(folderRecord.recordUid)
471-
if (folderRecord.folderUid.length > 0) {
471+
if (folderRecord.folderUid?.length > 0) {
472472
addDependencies(dependencies, webSafe64FromBytes(folderRecord.folderUid), recUid, 'record')
473473
} else {
474474
// Root user folder

0 commit comments

Comments
 (0)