Skip to content
This repository was archived by the owner on Apr 13, 2025. It is now read-only.

Commit 2f4fff6

Browse files
committed
Update and fix PersistenceManager tests
1 parent 02f0214 commit 2f4fff6

File tree

1 file changed

+63
-38
lines changed

1 file changed

+63
-38
lines changed

nodecg-io-core/extension/__tests__/persistenceManager.ts

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import * as crypto from "crypto-js";
22
import { BundleManager } from "../bundleManager";
33
import { InstanceManager } from "../instanceManager";
4-
import { decryptData, EncryptedData, PersistenceManager, PersistentData } from "../persistenceManager";
4+
import {
5+
decryptData,
6+
deriveEncryptionKey,
7+
EncryptedData,
8+
PersistenceManager,
9+
PersistentData,
10+
} from "../persistenceManager";
511
import { ServiceManager } from "../serviceManager";
612
import { ServiceProvider } from "../serviceProvider";
713
import { emptySuccess, error } from "../utils/result";
814
import { MockNodeCG, testBundle, testInstance, testService, testServiceInstance } from "./mocks";
915

1016
describe("PersistenceManager", () => {
1117
const validPassword = "myPassword";
12-
const invalidPassword = "someOtherPassword";
18+
const invalidPassword = "myInvalidPassword";
19+
const salt = crypto.lib.WordArray.random(128 / 8).toString();
20+
const validEncryptionKey = deriveEncryptionKey(validPassword, salt).toString();
21+
const invalidEncryptionKey = deriveEncryptionKey(invalidPassword, salt).toString();
1322

1423
const nodecg = new MockNodeCG();
1524
const serviceManager = new ServiceManager(nodecg);
@@ -39,7 +48,7 @@ describe("PersistenceManager", () => {
3948
* Creates a basic config and encrypts it. Used to check whether load decrypts and more importantly
4049
* restores the same configuration again.
4150
*/
42-
function generateEncryptedConfig(data?: PersistentData) {
51+
function generateEncryptedConfig(data?: PersistentData): EncryptedData {
4352
const d: PersistentData = data
4453
? data
4554
: {
@@ -56,22 +65,28 @@ describe("PersistenceManager", () => {
5665
[testInstance]: testServiceInstance,
5766
},
5867
};
59-
return crypto.AES.encrypt(JSON.stringify(d), validPassword).toString();
68+
69+
const iv = crypto.lib.WordArray.random(16);
70+
const encryptionKeyArray = crypto.enc.Hex.parse(validEncryptionKey);
71+
return {
72+
cipherText: crypto.AES.encrypt(JSON.stringify(d), encryptionKeyArray, { iv }).toString(),
73+
iv: iv.toString(),
74+
};
6075
}
6176

62-
describe("checkPassword", () => {
77+
describe("checkEncryptionKey", () => {
6378
test("should return false if not loaded", () => {
64-
expect(persistenceManager.checkPassword(validPassword)).toBe(false);
79+
expect(persistenceManager.checkEncryptionKey(validEncryptionKey)).toBe(false);
6580
});
6681

6782
test("should return false if loaded but password is wrong", async () => {
68-
await persistenceManager.load(validPassword);
69-
expect(persistenceManager.checkPassword(invalidPassword)).toBe(false);
83+
await persistenceManager.load(validEncryptionKey);
84+
expect(persistenceManager.checkEncryptionKey(invalidEncryptionKey)).toBe(false);
7085
});
7186

7287
test("should return true if loaded and password is correct", async () => {
73-
await persistenceManager.load(validPassword);
74-
expect(persistenceManager.checkPassword(validPassword)).toBe(true);
88+
await persistenceManager.load(validEncryptionKey);
89+
expect(persistenceManager.checkEncryptionKey(validEncryptionKey)).toBe(true);
7590
});
7691
});
7792

@@ -81,15 +96,15 @@ describe("PersistenceManager", () => {
8196
});
8297

8398
test("should return false if load was called but failed", async () => {
84-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig();
85-
const res = await persistenceManager.load(invalidPassword); // Will fail because the password is invalid
99+
encryptedDataReplicant.value = generateEncryptedConfig();
100+
const res = await persistenceManager.load(invalidEncryptionKey); // Will fail because the password is invalid
86101
expect(res.failed).toBe(true);
87102
expect(persistenceManager.isLoaded()).toBe(false);
88103
});
89104

90105
test("should return true if load was called and succeeded", async () => {
91-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig();
92-
const res = await persistenceManager.load(validPassword); // password is correct, should work
106+
encryptedDataReplicant.value = generateEncryptedConfig();
107+
const res = await persistenceManager.load(validEncryptionKey); // password is correct, should work
93108
expect(res.failed).toBe(false);
94109
expect(persistenceManager.isLoaded()).toBe(true);
95110
});
@@ -102,49 +117,49 @@ describe("PersistenceManager", () => {
102117
});
103118

104119
test("should return false if an encrypted config exists", () => {
105-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig(); // config = not a first startup
120+
encryptedDataReplicant.value = generateEncryptedConfig(); // config = not a first startup
106121
expect(persistenceManager.isFirstStartup()).toBe(false);
107122
});
108123
});
109124

110125
describe("load", () => {
111-
beforeEach(() => (encryptedDataReplicant.value.cipherText = generateEncryptedConfig()));
126+
beforeEach(() => (encryptedDataReplicant.value = generateEncryptedConfig()));
112127

113128
// General
114129

115130
test("should error if called after configuration already has been loaded", async () => {
116-
const res1 = await persistenceManager.load(validPassword);
131+
const res1 = await persistenceManager.load(validEncryptionKey);
117132
expect(res1.failed).toBe(false);
118-
const res2 = await persistenceManager.load(validPassword);
133+
const res2 = await persistenceManager.load(validEncryptionKey);
119134
expect(res2.failed).toBe(true);
120135
if (res2.failed) {
121136
expect(res2.errorMessage).toContain("already been decrypted and loaded");
122137
}
123138
});
124139

125140
test("should save current state if no encrypted config was found", async () => {
126-
const res = await persistenceManager.load(validPassword);
141+
const res = await persistenceManager.load(validEncryptionKey);
127142
expect(res.failed).toBe(false);
128143
expect(encryptedDataReplicant.value.cipherText).toBeDefined();
129144
});
130145

131146
test("should error if password is wrong", async () => {
132-
const res = await persistenceManager.load(invalidPassword);
147+
const res = await persistenceManager.load(invalidEncryptionKey);
133148
expect(res.failed).toBe(true);
134149
if (res.failed) {
135150
expect(res.errorMessage).toContain("Password isn't correct");
136151
}
137152
});
138153

139154
test("should succeed if password is correct", async () => {
140-
const res = await persistenceManager.load(validPassword);
155+
const res = await persistenceManager.load(validEncryptionKey);
141156
expect(res.failed).toBe(false);
142157
});
143158

144159
// Service instances
145160

146161
test("should load service instances including configuration", async () => {
147-
await persistenceManager.load(validPassword);
162+
await persistenceManager.load(validEncryptionKey);
148163
const inst = instanceManager.getServiceInstance(testInstance);
149164
expect(inst).toBeDefined();
150165
if (!inst) return;
@@ -153,21 +168,21 @@ describe("PersistenceManager", () => {
153168
});
154169

155170
test("should log failures when creating service instances", async () => {
156-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig({
171+
encryptedDataReplicant.value = generateEncryptedConfig({
157172
instances: {
158173
"": testServiceInstance, // This is invalid because the instance name is empty
159174
},
160175
bundleDependencies: {},
161176
});
162-
await persistenceManager.load(validPassword);
177+
await persistenceManager.load(validEncryptionKey);
163178
expect(nodecg.log.warn).toHaveBeenCalledTimes(1);
164179
expect(nodecg.log.warn.mock.calls[0][0]).toContain("Couldn't load instance");
165180
expect(nodecg.log.warn.mock.calls[0][0]).toContain("name must not be empty");
166181
});
167182

168183
test("should not set instance config when no config is required", async () => {
169184
testService.requiresNoConfig = true;
170-
await persistenceManager.load(validPassword);
185+
await persistenceManager.load(validEncryptionKey);
171186

172187
const inst = instanceManager.getServiceInstance(testInstance);
173188
if (!inst) throw new Error("instance was not re-created");
@@ -182,7 +197,7 @@ describe("PersistenceManager", () => {
182197
test("should log failures when setting service instance configs", async () => {
183198
const errorMsg = "client error message";
184199
testService.createClient.mockImplementationOnce(() => error(errorMsg));
185-
await persistenceManager.load(validPassword);
200+
await persistenceManager.load(validEncryptionKey);
186201

187202
// Wait for all previous promises created by loading to settle.
188203
await new Promise((res) => setImmediate(res));
@@ -194,7 +209,7 @@ describe("PersistenceManager", () => {
194209
// Service dependency assignments
195210

196211
test("should load service dependency assignments", async () => {
197-
await persistenceManager.load(validPassword);
212+
await persistenceManager.load(validEncryptionKey);
198213
const deps = bundleManager.getBundleDependencies()[testBundle];
199214
expect(deps).toBeDefined();
200215
if (!deps) return;
@@ -204,7 +219,7 @@ describe("PersistenceManager", () => {
204219
});
205220

206221
test("should unset service dependencies when the underlying instance was deleted", async () => {
207-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig({
222+
encryptedDataReplicant.value = generateEncryptedConfig({
208223
instances: {},
209224
bundleDependencies: {
210225
[testBundle]: [
@@ -216,15 +231,15 @@ describe("PersistenceManager", () => {
216231
],
217232
},
218233
});
219-
await persistenceManager.load(validPassword);
234+
await persistenceManager.load(validEncryptionKey);
220235

221236
const deps = bundleManager.getBundleDependencies()[testBundle];
222237
expect(deps?.[0]).toBeDefined();
223238
expect(deps?.[0]?.serviceInstance).toBeUndefined();
224239
});
225240

226241
test("should support unassigned service dependencies", async () => {
227-
encryptedDataReplicant.value.cipherText = generateEncryptedConfig({
242+
encryptedDataReplicant.value = generateEncryptedConfig({
228243
instances: {},
229244
bundleDependencies: {
230245
[testBundle]: [
@@ -236,7 +251,7 @@ describe("PersistenceManager", () => {
236251
],
237252
},
238253
});
239-
await persistenceManager.load(validPassword);
254+
await persistenceManager.load(validEncryptionKey);
240255

241256
const deps = bundleManager.getBundleDependencies()[testBundle];
242257
expect(deps?.[0]).toBeDefined();
@@ -251,7 +266,7 @@ describe("PersistenceManager", () => {
251266
});
252267

253268
test("should encrypt and save configuration if framework is loaded", async () => {
254-
const res = await persistenceManager.load(validPassword);
269+
const res = await persistenceManager.load(validEncryptionKey);
255270
expect(res.failed).toBe(false);
256271

257272
instanceManager.createServiceInstance(testService.serviceType, testInstance);
@@ -267,8 +282,12 @@ describe("PersistenceManager", () => {
267282
if (!encryptedDataReplicant.value.cipherText) return;
268283

269284
// Decrypt and check that the information that was saved is correct
270-
const data = decryptData(encryptedDataReplicant.value.cipherText, validPassword);
271-
if (data.failed) throw new Error("could not decrypt newly encrypted data");
285+
const data = decryptData(
286+
encryptedDataReplicant.value.cipherText,
287+
crypto.enc.Hex.parse(validEncryptionKey),
288+
encryptedDataReplicant.value.iv,
289+
);
290+
if (data.failed) throw new Error("could not decrypt newly encrypted data: " + data.errorMessage);
272291

273292
expect(data.result.instances[testInstance]?.serviceType).toBe(testService.serviceType);
274293
expect(data.result.instances[testInstance]?.config).toBe(testService.defaultConfig);
@@ -286,13 +305,19 @@ describe("PersistenceManager", () => {
286305
nodecg.log.error.mockReset();
287306

288307
persistenceManager = new PersistenceManager(nodecg, serviceManager, instanceManager, bundleManager);
289-
persistenceManager.load = jest.fn().mockImplementation(async (password: string) => {
290-
if (password === validPassword) return emptySuccess();
291-
else return error("password invalid");
308+
persistenceManager.load = jest.fn().mockImplementation(async (encryptionKey: string) => {
309+
if (encryptionKey === validEncryptionKey) return emptySuccess();
310+
else return error("encryption key invalid");
292311
});
293312
nodecgBundleReplicant.value = bundleRepValue ?? [nodecg.bundleName];
294313
}
295314

315+
beforeEach(() => {
316+
encryptedDataReplicant.value = {
317+
salt,
318+
};
319+
});
320+
296321
afterEach(() => {
297322
nodecg.bundleConfig = {};
298323
nodecgBundleReplicant.removeAllListeners();
@@ -377,7 +402,7 @@ describe("PersistenceManager", () => {
377402
});
378403

379404
test("should automatically save if BundleManager or InstanceManager emit a change event", async () => {
380-
await persistenceManager.load(validPassword); // Set password so that we can save stuff
405+
await persistenceManager.load(validEncryptionKey); // Set password so that we can save stuff
381406

382407
encryptedDataReplicant.value.cipherText = undefined;
383408
bundleManager.emit("change");

0 commit comments

Comments
 (0)