File size: 5,274 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | // v2.0.56 — runtime-config credentials persistence + scrypt verify.
// Covers the new helpers in src/runtime-config.js:
// - hashPassword / verifyPassword (scrypt)
// - setRuntimeApiKey / setRuntimeDashboardPassword
// - getEffectiveApiKey / getEffectiveDashboardPasswordStored
//
// We don't write real files — instead we exercise the in-memory state
// after each setter and snap the round-trip via verifyPassword on the
// returned hash format.
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import {
hashPassword,
verifyPassword,
setRuntimeApiKey,
setRuntimeDashboardPassword,
getCredentials,
getEffectiveApiKey,
getEffectiveDashboardPasswordStored,
} from '../src/runtime-config.js';
import { config } from '../src/config.js';
const original = {
apiKey: config.apiKey,
dashboardPassword: config.dashboardPassword,
};
beforeEach(() => {
setRuntimeApiKey('');
setRuntimeDashboardPassword('');
config.apiKey = original.apiKey;
config.dashboardPassword = original.dashboardPassword;
});
afterEach(() => {
setRuntimeApiKey('');
setRuntimeDashboardPassword('');
config.apiKey = original.apiKey;
config.dashboardPassword = original.dashboardPassword;
});
describe('hashPassword + verifyPassword (scrypt)', () => {
it('hash format is `scrypt$N$r$p$salt$hash`', () => {
const h = hashPassword('correct-horse-battery-staple');
const parts = h.split('$');
assert.equal(parts[0], 'scrypt');
assert.equal(parts.length, 6);
assert.ok(parseInt(parts[1], 10) > 0, 'N must be positive int');
assert.ok(parts[4].length > 0, 'salt must be non-empty');
assert.ok(parts[5].length > 0, 'hash must be non-empty');
});
it('round-trips: verify(plain, hash(plain)) === true', () => {
const h = hashPassword('mypassword');
assert.equal(verifyPassword('mypassword', h), true);
});
it('verify rejects wrong password', () => {
const h = hashPassword('mypassword');
assert.equal(verifyPassword('wrongpassword', h), false);
assert.equal(verifyPassword('', h), false);
assert.equal(verifyPassword('mypassword2', h), false);
});
it('verify falls back to plaintext compare when stored has no scrypt prefix', () => {
// env-supplied `DASHBOARD_PASSWORD=plain` takes this branch.
assert.equal(verifyPassword('hello', 'hello'), true);
assert.equal(verifyPassword('hello', 'world'), false);
assert.equal(verifyPassword('', ''), false);
assert.equal(verifyPassword('hello', ''), false);
});
it('verify rejects malformed hash strings', () => {
assert.equal(verifyPassword('x', 'scrypt$bad'), false);
assert.equal(verifyPassword('x', 'scrypt$1$2$3$4'), false); // only 5 parts
assert.equal(verifyPassword('x', null), false);
assert.equal(verifyPassword('x', undefined), false);
assert.equal(verifyPassword('x', 123), false);
});
it('different salts produce different hashes for the same password', () => {
const a = hashPassword('same');
const b = hashPassword('same');
assert.notEqual(a, b, 'salt randomness must produce distinct outputs');
assert.equal(verifyPassword('same', a), true);
assert.equal(verifyPassword('same', b), true);
});
});
describe('setRuntimeApiKey / getEffectiveApiKey', () => {
it('runtime override wins over config.apiKey', () => {
config.apiKey = 'env-key';
setRuntimeApiKey('runtime-key');
assert.equal(getEffectiveApiKey(), 'runtime-key');
});
it('empty runtime falls back to env', () => {
config.apiKey = 'env-key';
setRuntimeApiKey('');
assert.equal(getEffectiveApiKey(), 'env-key');
});
it('both empty → empty string', () => {
config.apiKey = '';
setRuntimeApiKey('');
assert.equal(getEffectiveApiKey(), '');
});
it('whitespace-only input is normalised to empty', () => {
setRuntimeApiKey(' ');
assert.equal(getCredentials().apiKey, '');
});
});
describe('setRuntimeDashboardPassword / getEffectiveDashboardPasswordStored', () => {
it('runtime password is stored as scrypt hash', () => {
setRuntimeDashboardPassword('newpassword');
const stored = getEffectiveDashboardPasswordStored();
assert.ok(stored.startsWith('scrypt$'), `expected scrypt$ prefix, got ${stored.slice(0, 20)}`);
assert.equal(verifyPassword('newpassword', stored), true);
assert.equal(verifyPassword('wrong', stored), false);
});
it('runtime override wins over env plaintext password', () => {
config.dashboardPassword = 'env-password';
setRuntimeDashboardPassword('runtime-password');
const stored = getEffectiveDashboardPasswordStored();
// verifyPassword on the runtime hash must accept runtime-password
assert.equal(verifyPassword('runtime-password', stored), true);
// but reject the env one
assert.equal(verifyPassword('env-password', stored), false);
});
it('clearing runtime falls back to env plaintext', () => {
config.dashboardPassword = 'env-password';
setRuntimeDashboardPassword('runtime-password');
setRuntimeDashboardPassword('');
const stored = getEffectiveDashboardPasswordStored();
assert.equal(stored, 'env-password');
assert.equal(verifyPassword('env-password', stored), true);
});
});
|