The integration suite registers around thirty-five customers and asserts nothing about any of their hashes, yet paid bcrypt cost 12 for every one. bcryptjs is a pure-JS implementation, so it pays that cost several times over compared with a native build, and hashing was most of the suite's wall clock. On a contended runner it pushed adminInventory.integration.test.ts past its twenty-second timeout, which then surfaced as a foreign key violation somewhere else entirely — the test timed out, jest moved on, beforeEach truncated, and the still-in-flight registration wrote a token for a customer that had just been deleted. Measured rather than asserted, warm run against warm run with only the constant changed: 34.5s at cost 12, 9.8s at cost 4. Three and a half times faster, about twenty-five seconds off every integration run, with all 263 tests passing either way. The first attempt at that measurement was wrong and worth recording. Comparing a cold run at cost 4 against a warm run at cost 12 made the change look like a 36% regression-shaped improvement of the wrong size; the difference was ts-jest and Postgres warming up, not the cost factor. Both numbers above are second runs, and the cost-12 figure was taken twice — 34.3s and 34.5s — before being believed. Deliberately not configurable. An environment variable here would be a way to weaken password hashing in production by misconfiguration, and nothing needs to tune it. The only route to the cheap cost is NODE_ENV=test, which a deployed container would announce anyway by refusing to serve the built frontend, since app.ts gates static serving on the same value. A setting that quietly degrades a security property should be unreachable rather than warned about, which is the reasoning that already made DEMO_MODE strict. `hashRoundsFor` is pure and separately tested because the failure it guards against is silent: only the exact string 'test' earns the cheap cost, and an unset NODE_ENV gets the strong one, so the dangerous direction has to be asked for explicitly. Both constants are pinned by assertions too — without that the branch tests pass while the numbers drift to something useless. Closes #242
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
import { hashRoundsFor, PRODUCTION_ROUNDS, TEST_ROUNDS } from '../../src/passwordHashing';
|
|
|
|
// The cost factor is a security property, and the whole point of lowering it
|
|
// under test is that it must not be lowered anywhere else. Asserted rather than
|
|
// read, so a later edit that widens the test branch fails here first.
|
|
describe('hashRoundsFor', () => {
|
|
it('uses the cheap cost under test', () => {
|
|
expect(hashRoundsFor('test')).toBe(TEST_ROUNDS);
|
|
});
|
|
|
|
it('uses the real cost in production', () => {
|
|
expect(hashRoundsFor('production')).toBe(PRODUCTION_ROUNDS);
|
|
});
|
|
|
|
// The container sets NODE_ENV=production, but a bare `node dist/server.js`
|
|
// does not set it at all. Absent must mean the strong cost, never the cheap
|
|
// one — defaulting the other way is how a weak hash reaches real passwords.
|
|
it('uses the real cost when NODE_ENV is unset', () => {
|
|
expect(hashRoundsFor(undefined)).toBe(PRODUCTION_ROUNDS);
|
|
});
|
|
|
|
it('uses the real cost for any value that is not exactly "test"', () => {
|
|
for (const value of ['development', 'staging', 'Test', 'TEST', 'testing', '']) {
|
|
expect(hashRoundsFor(value)).toBe(PRODUCTION_ROUNDS);
|
|
}
|
|
});
|
|
|
|
// Pinning the numbers themselves. Without this the tests above pass while
|
|
// both constants drift to something useless.
|
|
it('keeps the production cost at 12', () => {
|
|
expect(PRODUCTION_ROUNDS).toBe(12);
|
|
});
|
|
|
|
it('keeps the test cost cheap enough to be worth doing', () => {
|
|
expect(TEST_ROUNDS).toBeLessThanOrEqual(6);
|
|
// bcrypt refuses a cost below 4.
|
|
expect(TEST_ROUNDS).toBeGreaterThanOrEqual(4);
|
|
});
|
|
});
|