Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
182 lines
5.5 KiB
JavaScript
182 lines
5.5 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const {
|
|
createAppLockPasswordVerifier,
|
|
createAppLockSettingsStore,
|
|
canLockFromSettings,
|
|
shouldLockOnBackgroundHide,
|
|
verifyAppLockPassword,
|
|
} = require("./appLockSettingsStore.cjs");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const settingsStoreSource = fs.readFileSync(path.join(__dirname, "appLockSettingsStore.cjs"), "utf8");
|
|
|
|
test("password hashing uses the asynchronous crypto API", () => {
|
|
assert.doesNotMatch(settingsStoreSource, /pbkdf2Sync/);
|
|
assert.match(settingsStoreSource, /\bpbkdf2\b/);
|
|
});
|
|
|
|
const VALID_VERIFIER = {
|
|
version: 1,
|
|
algorithm: "PBKDF2-SHA256",
|
|
iterations: 210000,
|
|
salt: Buffer.alloc(16, 1).toString("base64"),
|
|
hash: Buffer.alloc(32, 2).toString("base64"),
|
|
};
|
|
|
|
test("settings store loads disabled config when no persisted file exists", async () => {
|
|
const store = createAppLockSettingsStore({
|
|
filePath: "/tmp/app-lock-settings.json",
|
|
readFile: async () => {
|
|
const err = new Error("ENOENT");
|
|
err.code = "ENOENT";
|
|
throw err;
|
|
},
|
|
writeFile: async () => {},
|
|
});
|
|
|
|
const settings = await store.load();
|
|
assert.deepEqual(settings, {
|
|
enabled: false,
|
|
timeoutMinutes: 15,
|
|
systemUnlockEnabled: false,
|
|
systemUnlockAutoPromptEnabled: false,
|
|
passwordVerifier: null,
|
|
});
|
|
assert.deepEqual(store.getSnapshot(), settings);
|
|
});
|
|
|
|
test("settings store normalizes malformed persisted config to disabled defaults", async () => {
|
|
const store = createAppLockSettingsStore({
|
|
filePath: "/tmp/app-lock-settings.json",
|
|
readFile: async () =>
|
|
JSON.stringify({
|
|
enabled: true,
|
|
timeoutMinutes: 999,
|
|
passwordVerifier: { hash: "invalid" },
|
|
}),
|
|
writeFile: async () => {},
|
|
});
|
|
|
|
const settings = await store.load();
|
|
assert.deepEqual(settings, {
|
|
enabled: false,
|
|
timeoutMinutes: 15,
|
|
systemUnlockEnabled: false,
|
|
systemUnlockAutoPromptEnabled: false,
|
|
passwordVerifier: null,
|
|
});
|
|
});
|
|
|
|
test("settings store saves normalized settings and updates snapshot", async () => {
|
|
let writeCall = null;
|
|
const store = createAppLockSettingsStore({
|
|
filePath: "/tmp/app-lock-settings.json",
|
|
readFile: async () => {
|
|
const err = new Error("ENOENT");
|
|
err.code = "ENOENT";
|
|
throw err;
|
|
},
|
|
writeFile: async (filePath, content, options) => {
|
|
writeCall = { filePath, content, options };
|
|
},
|
|
});
|
|
|
|
const saved = await store.save({
|
|
enabled: true,
|
|
timeoutMinutes: 0,
|
|
systemUnlockEnabled: true,
|
|
systemUnlockAutoPromptEnabled: true,
|
|
passwordVerifier: VALID_VERIFIER,
|
|
});
|
|
|
|
assert.deepEqual(saved, {
|
|
enabled: true,
|
|
timeoutMinutes: 0,
|
|
systemUnlockEnabled: true,
|
|
systemUnlockAutoPromptEnabled: true,
|
|
passwordVerifier: VALID_VERIFIER,
|
|
});
|
|
assert.deepEqual(store.getSnapshot(), saved);
|
|
assert.deepEqual(writeCall, {
|
|
filePath: "/tmp/app-lock-settings.json",
|
|
content: `${JSON.stringify(saved, null, 2)}\n`,
|
|
options: { mode: 0o600 },
|
|
});
|
|
});
|
|
|
|
test("settings store clears system unlock when no valid verifier exists", async () => {
|
|
const store = createAppLockSettingsStore({
|
|
filePath: "/tmp/app-lock-settings.json",
|
|
readFile: async () =>
|
|
JSON.stringify({
|
|
enabled: true,
|
|
timeoutMinutes: 5,
|
|
systemUnlockEnabled: true,
|
|
systemUnlockAutoPromptEnabled: true,
|
|
passwordVerifier: null,
|
|
}),
|
|
writeFile: async () => {},
|
|
});
|
|
|
|
const settings = await store.load();
|
|
assert.deepEqual(settings, {
|
|
enabled: false,
|
|
timeoutMinutes: 5,
|
|
systemUnlockEnabled: false,
|
|
systemUnlockAutoPromptEnabled: false,
|
|
passwordVerifier: null,
|
|
});
|
|
});
|
|
|
|
test("canLockFromSettings requires enabled and a verifier", async () => {
|
|
assert.equal(canLockFromSettings({ enabled: false, passwordVerifier: VALID_VERIFIER }), false);
|
|
assert.equal(canLockFromSettings({ enabled: true, passwordVerifier: null }), false);
|
|
assert.equal(canLockFromSettings({ enabled: true, passwordVerifier: { hash: "x" } }), false);
|
|
assert.equal(canLockFromSettings({ enabled: true, passwordVerifier: VALID_VERIFIER }), true);
|
|
});
|
|
|
|
test("shouldLockOnBackgroundHide requires an automatic timeout", () => {
|
|
assert.equal(shouldLockOnBackgroundHide({
|
|
enabled: true,
|
|
timeoutMinutes: 0,
|
|
passwordVerifier: VALID_VERIFIER,
|
|
}), false);
|
|
assert.equal(shouldLockOnBackgroundHide({
|
|
enabled: true,
|
|
timeoutMinutes: 5,
|
|
passwordVerifier: VALID_VERIFIER,
|
|
}), true);
|
|
assert.equal(shouldLockOnBackgroundHide({
|
|
enabled: false,
|
|
timeoutMinutes: 5,
|
|
passwordVerifier: VALID_VERIFIER,
|
|
}), false);
|
|
});
|
|
|
|
test("createAppLockPasswordVerifier stores a verifier that verifyAppLockPassword accepts", async () => {
|
|
const verifier = await createAppLockPasswordVerifier("correct horse battery staple");
|
|
|
|
assert.equal(verifier.version, 1);
|
|
assert.equal(verifier.algorithm, "PBKDF2-SHA256");
|
|
assert.ok(verifier.iterations >= 100000);
|
|
assert.notEqual(verifier.salt, "");
|
|
assert.notEqual(verifier.hash, "");
|
|
|
|
assert.equal(await verifyAppLockPassword("correct horse battery staple", verifier), true);
|
|
assert.equal(await verifyAppLockPassword("wrong password", verifier), false);
|
|
});
|
|
|
|
test("createAppLockPasswordVerifier preserves a whitespace-only password", async () => {
|
|
const verifier = await createAppLockPasswordVerifier(" ");
|
|
|
|
assert.equal(await verifyAppLockPassword(" ", verifier), true);
|
|
assert.equal(await verifyAppLockPassword("", verifier), false);
|
|
await assert.rejects(
|
|
() => createAppLockPasswordVerifier(""),
|
|
/App lock password is required/,
|
|
);
|
|
});
|