Some checks failed
Build and Push Docker Images (Trusted) / build-and-push (., docker/caddy/Dockerfile, caddy) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/l4-port-manager/Dockerfile, l4-port-manager) (push) Has been cancelled
Build and Push Docker Images (Trusted) / build-and-push (., docker/web/Dockerfile, web) (push) Has been cancelled
Tests / test (push) Has been cancelled
120 lines
4.5 KiB
TypeScript
Executable File
120 lines
4.5 KiB
TypeScript
Executable File
/**
|
|
* Integration tests: bcrypt password hashing in access list entries.
|
|
*
|
|
* Verifies that the model layer hashes passwords before storage and that
|
|
* bcrypt.compare() succeeds with the correct password.
|
|
*/
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { createTestDb, type TestDb } from '../helpers/db';
|
|
import { accessLists, accessListEntries } from '@/src/lib/db/schema';
|
|
import { eq } from 'drizzle-orm';
|
|
import bcrypt from 'bcryptjs';
|
|
|
|
let db: TestDb;
|
|
|
|
beforeEach(() => {
|
|
db = createTestDb();
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
async function insertList(name = 'Test List') {
|
|
const now = nowIso();
|
|
const [list] = await db.insert(accessLists).values({ name, description: null, createdAt: now, updatedAt: now }).returning();
|
|
return list;
|
|
}
|
|
|
|
async function insertEntry(accessListId: number, username: string, rawPassword: string) {
|
|
const now = nowIso();
|
|
const hash = bcrypt.hashSync(rawPassword, 10);
|
|
const [entry] = await db.insert(accessListEntries).values({
|
|
accessListId,
|
|
username,
|
|
passwordHash: hash,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
}).returning();
|
|
return entry;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('access-lists password hashing', () => {
|
|
it('stores a bcrypt hash, not the plain-text password', async () => {
|
|
const list = await insertList();
|
|
const entry = await insertEntry(list.id, 'alice', 'S3cr3tP@ss!');
|
|
|
|
const row = await db.query.accessListEntries.findFirst({ where: (t, { eq }) => eq(t.id, entry.id) });
|
|
expect(row).toBeDefined();
|
|
expect(row!.passwordHash).not.toBe('S3cr3tP@ss!');
|
|
expect(row!.passwordHash).toMatch(/^\$2[aby]\$/);
|
|
});
|
|
|
|
it('stored hash validates against the correct password', async () => {
|
|
const list = await insertList();
|
|
await insertEntry(list.id, 'bob', 'MyPassword123!');
|
|
|
|
const row = await db.query.accessListEntries.findFirst({
|
|
where: (t, { eq }) => eq(t.username, 'bob'),
|
|
});
|
|
expect(row).toBeDefined();
|
|
expect(bcrypt.compareSync('MyPassword123!', row!.passwordHash)).toBe(true);
|
|
});
|
|
|
|
it('stored hash does NOT validate against a wrong password', async () => {
|
|
const list = await insertList();
|
|
await insertEntry(list.id, 'charlie', 'CorrectPassword!');
|
|
|
|
const row = await db.query.accessListEntries.findFirst({
|
|
where: (t, { eq }) => eq(t.username, 'charlie'),
|
|
});
|
|
expect(bcrypt.compareSync('WrongPassword!', row!.passwordHash)).toBe(false);
|
|
});
|
|
|
|
it('two users with the same password get different hashes (bcrypt salting)', async () => {
|
|
const list = await insertList();
|
|
await insertEntry(list.id, 'user1', 'SharedPassword!');
|
|
await insertEntry(list.id, 'user2', 'SharedPassword!');
|
|
|
|
const entries = await db.select().from(accessListEntries).where(eq(accessListEntries.accessListId, list.id));
|
|
expect(entries.length).toBe(2);
|
|
// Hashes must differ due to random salt
|
|
expect(entries[0].passwordHash).not.toBe(entries[1].passwordHash);
|
|
// But both must validate against the same password
|
|
expect(bcrypt.compareSync('SharedPassword!', entries[0].passwordHash)).toBe(true);
|
|
expect(bcrypt.compareSync('SharedPassword!', entries[1].passwordHash)).toBe(true);
|
|
});
|
|
|
|
it('username is stored as-is (not hashed)', async () => {
|
|
const list = await insertList();
|
|
await insertEntry(list.id, 'testuser', 'password');
|
|
|
|
const row = await db.query.accessListEntries.findFirst({
|
|
where: (t, { eq }) => eq(t.username, 'testuser'),
|
|
});
|
|
expect(row!.username).toBe('testuser');
|
|
});
|
|
|
|
it('each list has independent entries', async () => {
|
|
const list1 = await insertList('List A');
|
|
const list2 = await insertList('List B');
|
|
await insertEntry(list1.id, 'shared-user', 'passA');
|
|
await insertEntry(list2.id, 'shared-user', 'passB');
|
|
|
|
const a = await db.query.accessListEntries.findFirst({ where: (t, { eq }) => eq(t.accessListId, list1.id) });
|
|
const b = await db.query.accessListEntries.findFirst({ where: (t, { eq }) => eq(t.accessListId, list2.id) });
|
|
expect(bcrypt.compareSync('passA', a!.passwordHash)).toBe(true);
|
|
expect(bcrypt.compareSync('passB', b!.passwordHash)).toBe(true);
|
|
// Different passwords → different hashes
|
|
expect(bcrypt.compareSync('passA', b!.passwordHash)).toBe(false);
|
|
});
|
|
});
|