import bcrypt from 'bcryptjs'; import { passwordMatches, TEST_ROUNDS } from '../../src/passwordHashing'; /** * `customers.password_hash` became nullable in #340, and a null one is not an * edge case to tidy away — it is a customer who signed up through Google and * has never set a password. * * The reason this is a function rather than a null check at each call site is * what the second test asserts: `bcrypt.compare` throws on a null hash instead * of returning false, so a forgotten check answers a sign-in attempt with a 500. * On the login route that is also an oracle, because it happens for exactly the * accounts that have no password. */ describe('passwordMatches', () => { it('matches a correct password against a real hash', async () => { const hash = await bcrypt.hash('supersecret123', TEST_ROUNDS); await expect(passwordMatches('supersecret123', hash)).resolves.toBe(true); }); it('refuses a wrong password against a real hash', async () => { const hash = await bcrypt.hash('supersecret123', TEST_ROUNDS); await expect(passwordMatches('nope', hash)).resolves.toBe(false); }); it.each([ ['null', null], ['undefined', undefined], ['an empty string', ''] ])('answers false rather than throwing when the stored hash is %s', async (_label, stored) => { // The whole point. bcrypt.compare throws "Illegal arguments" here. await expect(passwordMatches('anything', stored)).resolves.toBe(false); }); it('answers false for a missing password rather than throwing', async () => { const hash = await bcrypt.hash('supersecret123', TEST_ROUNDS); await expect(passwordMatches(undefined, hash)).resolves.toBe(false); await expect(passwordMatches(null, hash)).resolves.toBe(false); }); it('does not treat an empty password as matching an absent hash', async () => { // Both sides missing is the combination that would be most tempting to call // a match, and it would let anyone sign in as any social-only customer by // submitting a blank password. await expect(passwordMatches('', null)).resolves.toBe(false); }); });