← back to Directory Core
test/_ident.test.ts
171 lines
// test/_ident.test.ts
// Tests for src/_ident.ts — assertSafeIdent + assertSafeIdents.
//
// Every path exercised: valid idents pass, invalid idents throw with a
// descriptive message, custom `what` labels appear in the error, and
// assertSafeIdents validates the array contract.
process.env['DATABASE_URL'] = process.env['DATABASE_URL'] || 'postgresql://fake:fake@localhost:5432/fake';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { assertSafeIdent, assertSafeIdents } from '../src/_ident.js';
// ── assertSafeIdent — accepted values ────────────────────────────────────────
describe('assertSafeIdent — accepted identifiers', () => {
const valid = [
'app_users',
'id',
'_t1',
'public.app_users',
'schema.col_name',
'A',
'Col123',
'_',
'a1b2c3',
'my_schema.my_table',
];
for (const s of valid) {
it(`accepts "${s}"`, () => {
assert.doesNotThrow(() => assertSafeIdent(s));
});
}
});
// ── assertSafeIdent — rejected values ────────────────────────────────────────
describe('assertSafeIdent — rejected identifiers', () => {
const cases: Array<{ label: string; value: unknown }> = [
{ label: 'SQL injection via semicolon', value: "users; DROP TABLE x" },
{ label: 'SQL comment --', value: "users -- comment" },
{ label: 'embedded double-quote', value: 'users"x"' },
{ label: 'empty string', value: '' },
{ label: 'number (non-string)', value: 42 },
{ label: 'null', value: null },
{ label: 'undefined', value: undefined },
{ label: 'plain object', value: {} },
{ label: 'array', value: ['users'] },
{ label: 'string with spaces', value: 'my table' },
{ label: 'backtick in string', value: '`users`' },
{ label: 'starts with digit', value: '1table' },
{ label: 'unicode letter', value: 'tëst' },
{ label: 'single quote', value: "user's" },
{ label: 'asterisk wildcard', value: 'table.*' },
{ label: 'double-dot qualified', value: 'a.b.c' },
];
for (const { label, value } of cases) {
it(`rejects ${label}`, () => {
assert.throws(
() => assertSafeIdent(value),
(err: unknown) => {
assert.ok(err instanceof Error, 'error should be an Error instance');
assert.ok(
err.message.includes('directory-core'),
`error message should mention directory-core, got: ${err.message}`,
);
return true;
},
);
});
}
});
// ── assertSafeIdent — custom `what` label ────────────────────────────────────
describe('assertSafeIdent — custom what label in error message', () => {
it('includes the what label in the thrown error message', () => {
assert.throws(
() => assertSafeIdent('bad value!', 'userTable'),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.ok(
err.message.includes('userTable'),
`Expected "userTable" in error: ${(err as Error).message}`,
);
return true;
},
);
});
it('uses default label "identifier" when what is omitted', () => {
assert.throws(
() => assertSafeIdent('bad!'),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.ok(
err.message.includes('identifier'),
`Expected "identifier" in error: ${(err as Error).message}`,
);
return true;
},
);
});
});
// ── assertSafeIdents — array contract ────────────────────────────────────────
describe('assertSafeIdents — array validation', () => {
it('accepts an array of valid identifiers', () => {
assert.doesNotThrow(() => assertSafeIdents(['id', 'name', 'created_at']));
});
it('accepts an empty array', () => {
assert.doesNotThrow(() => assertSafeIdents([]));
});
it('rejects non-array input (string)', () => {
assert.throws(
() => assertSafeIdents('users' as unknown),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.ok(err.message.includes('array'), `Expected "array" in error: ${(err as Error).message}`);
return true;
},
);
});
it('rejects non-array input (number)', () => {
assert.throws(() => assertSafeIdents(42 as unknown));
});
it('rejects non-array input (null)', () => {
assert.throws(() => assertSafeIdents(null as unknown));
});
it('rejects an array containing one bad element', () => {
assert.throws(
() => assertSafeIdents(['id', 'users; DROP TABLE x', 'name']),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.ok(
err.message.includes('directory-core'),
`Expected directory-core in error: ${(err as Error).message}`,
);
return true;
},
);
});
it('rejects an array containing a non-string element', () => {
assert.throws(() => assertSafeIdents(['id', 42 as unknown as string, 'name']));
});
it('uses the custom what label when provided', () => {
assert.throws(
() => assertSafeIdents(['bad ident'], 'columnList'),
(err: unknown) => {
assert.ok(err instanceof Error);
// The what label is propagated into the per-item error
assert.ok(
err.message.includes('columnList'),
`Expected "columnList" in error: ${(err as Error).message}`,
);
return true;
},
);
});
});