From 7ece96688db76a5f33c3279641b0dfa8af2ed0db Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 17 Jun 2026 16:12:14 -0400 Subject: [PATCH] test(19-01): add failing tests for hashPassword/verifyPassword scrypt primitives --- apps/api/tests/auth/localCredentials.test.ts | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 apps/api/tests/auth/localCredentials.test.ts diff --git a/apps/api/tests/auth/localCredentials.test.ts b/apps/api/tests/auth/localCredentials.test.ts new file mode 100644 index 0000000..5bc5433 --- /dev/null +++ b/apps/api/tests/auth/localCredentials.test.ts @@ -0,0 +1,57 @@ +/** + * localCredentials.ts — unit tests for hashPassword / verifyPassword. + * + * Uses node:crypto scrypt under the hood; no external dependencies. + * All tests run without MariaDB or any external service. + * + * Test suite (TDD RED → GREEN — Plan 19-01 Task 1): + * Test 1: correct password verifies true + * Test 2: wrong password verifies false + * Test 3: two hashes of the same input produce different encoded strings (unique salt) + * Test 4: verifyPassword never throws on a malformed hash (returns false) + * Test 5: encoded string has the PHC shape: scrypt$N$r$p$$ (6 segments) + */ + +import { describe, it, expect } from 'vitest'; +import { hashPassword, verifyPassword } from '../../src/auth/localCredentials.js'; + +describe('hashPassword / verifyPassword', () => { + it('Test 1: verifyPassword(hashPassword(pw), pw) === true (round-trip)', () => { + const encoded = hashPassword('hunter2'); + const result = verifyPassword(encoded, 'hunter2'); + expect(result).toBe(true); + }); + + it('Test 2: verifyPassword(hashPassword(pw), wrong) === false', () => { + const encoded = hashPassword('hunter2'); + const result = verifyPassword(encoded, 'wrong-password'); + expect(result).toBe(false); + }); + + it('Test 3: two hashPassword calls on same input produce different encoded strings (unique salt)', () => { + const encoded1 = hashPassword('x'); + const encoded2 = hashPassword('x'); + expect(encoded1).not.toBe(encoded2); + }); + + it('Test 4: verifyPassword returns false (never throws) on a malformed stored hash', () => { + expect(() => verifyPassword('not-a-valid-hash', 'x')).not.toThrow(); + expect(verifyPassword('not-a-valid-hash', 'x')).toBe(false); + expect(verifyPassword('', 'x')).toBe(false); + expect(verifyPassword('scrypt$bad$data', 'x')).toBe(false); + }); + + it('Test 5: encoded string has scrypt$N$r$p$salt$hash shape (6 $-delimited segments)', () => { + const encoded = hashPassword('testpassword'); + const segments = encoded.split('$'); + expect(segments).toHaveLength(6); + expect(segments[0]).toBe('scrypt'); + // N, r, p are numeric + expect(Number(segments[1])).toBeGreaterThan(0); // N + expect(Number(segments[2])).toBeGreaterThan(0); // r + expect(Number(segments[3])).toBeGreaterThan(0); // p + // salt and hash are non-empty base64url strings + expect(segments[4].length).toBeGreaterThan(0); + expect(segments[5].length).toBeGreaterThan(0); + }); +});