mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
202 lines
8.6 KiB
TypeScript
202 lines
8.6 KiB
TypeScript
import { Trips } from '../../../src/db/entities/Trips.entity';
|
|
import { NulSafeSqlitePlatform } from '../../../src/db/nul-safe-sqlite-platform';
|
|
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
|
import { createTrip, createUser } from '../../helpers/factories';
|
|
import { resetTestDb } from '../../helpers/test-db';
|
|
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
|
|
|
import Database from 'better-sqlite3';
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
/**
|
|
* Plan 3c Task 4 review M1 / program rule 22: MikroORM 7 inlines every bound
|
|
* value into the SQL text via `platform.quoteValue`/`escape` (see
|
|
* `NulSafeSqlitePlatform`'s own docstring for the full mechanism). These
|
|
* tests prove the override round-trips a NUL byte at every position SQLite
|
|
* itself will accept, by handing the platform's own generated SQL literal to
|
|
* a REAL `better-sqlite3` connection and reading the value back — not a
|
|
* string-shape assertion, an actual SQLite round-trip.
|
|
*/
|
|
describe('NulSafeSqlitePlatform', () => {
|
|
const platform = new NulSafeSqlitePlatform();
|
|
const db = new Database(':memory:');
|
|
|
|
function roundTrip(value: string): string {
|
|
const literal = platform.escape(value);
|
|
const row = db.prepare(`SELECT ${literal} as v`).get() as { v: string };
|
|
return row.v;
|
|
}
|
|
|
|
it('round-trips a string with no NUL unchanged (baseline: unaffected by the override)', () => {
|
|
expect(roundTrip('plain string')).toBe('plain string');
|
|
});
|
|
|
|
it('round-trips a NUL in the middle of a string', () => {
|
|
const value = 'nul\u0000name';
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('round-trips a NUL at the very start of a string', () => {
|
|
const value = '\u0000leading';
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('round-trips a NUL at the very end of a string', () => {
|
|
const value = 'trailing\u0000';
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('round-trips a string made ENTIRELY of NUL bytes', () => {
|
|
expect(roundTrip('\u0000')).toBe('\u0000');
|
|
expect(roundTrip('\u0000\u0000\u0000')).toBe('\u0000\u0000\u0000');
|
|
});
|
|
|
|
it('round-trips a NUL next to a single quote (the string-literal escape character)', () => {
|
|
const value = "o'brien\u0000's café";
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('round-trips a NUL next to a backslash (not special in a SQLite string literal)', () => {
|
|
const value = 'back\\slash\u0000end\\';
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('round-trips multiple NULs mixed with quotes and backslashes', () => {
|
|
const value = "a'\u0000b\\\u0000c''\u0000\\\\d";
|
|
expect(roundTrip(value)).toBe(value);
|
|
});
|
|
|
|
it('fuzzes every 0x00-0x1F control character: none corrupt the round-trip and none throw', () => {
|
|
for (let code = 0x00; code <= 0x1f; code++) {
|
|
const ch = String.fromCharCode(code);
|
|
const value = `before${ch}after`;
|
|
expect(() => roundTrip(value)).not.toThrow();
|
|
expect(roundTrip(value)).toBe(value);
|
|
}
|
|
});
|
|
|
|
it('leaves non-string values to the base platform (numbers, null, dates, buffers untouched)', () => {
|
|
expect(platform.escape(42)).toBe('42');
|
|
expect(platform.escape(null)).toBe('null');
|
|
expect(platform.escape(true)).toBe('true');
|
|
});
|
|
|
|
it('formatQuery (the real call path AbstractSqlConnection.execute uses: formatQuery -> quoteValue -> escape) inlines a NUL-safe literal for a bound `?`, matched via a real table', () => {
|
|
db.exec('CREATE TABLE probe (name TEXT)');
|
|
const value = 'search\u0000term';
|
|
const insertSql = platform.formatQuery('insert into probe (name) values (?)', [value]);
|
|
db.exec(insertSql);
|
|
const selectSql = platform.formatQuery('select name from probe where name = ?', [value]);
|
|
const row = db.prepare(selectSql).get() as { name: string } | undefined;
|
|
expect(row?.name).toBe(value);
|
|
});
|
|
|
|
/**
|
|
* Program rule 22's extension (Plan 3c Task 9 close-out, review A H1): a
|
|
* non-finite `number` used to render as an unquoted bareword (`NaN`/
|
|
* `Infinity`/`-Infinity`), which SQLite parses as a column reference and
|
|
* throws `no such column: …` — a 500 where the raw better-sqlite3 bind
|
|
* (`NaN` -> `NULL`, `±Infinity` -> the `REAL` value, verified empirically
|
|
* against the installed driver above the class docstring) never had the
|
|
* problem. These pin the rendered SQL literal AND the real round-trip.
|
|
*/
|
|
describe('non-finite numbers (rule 22 extension)', () => {
|
|
it('NaN renders as the literal `NULL`, not the bareword `NaN`', () => {
|
|
expect(platform.escape(NaN)).toBe('NULL');
|
|
});
|
|
|
|
it('+Infinity renders as the literal `9e999`, which SQLite parses back to the REAL value Infinity', () => {
|
|
expect(platform.escape(Infinity)).toBe('9e999');
|
|
const row = db.prepare('select typeof(9e999) as t, 9e999 as v').get() as { t: string; v: number };
|
|
expect(row).toEqual({ t: 'real', v: Infinity });
|
|
});
|
|
|
|
it('-Infinity renders as the literal `-9e999`, which SQLite parses back to the REAL value -Infinity', () => {
|
|
expect(platform.escape(-Infinity)).toBe('-9e999');
|
|
const row = db.prepare('select typeof(-9e999) as t, -9e999 as v').get() as { t: string; v: number };
|
|
expect(row).toEqual({ t: 'real', v: -Infinity });
|
|
});
|
|
|
|
it('a finite number is untouched (only the three non-finite values take the new branch)', () => {
|
|
expect(platform.escape(0)).toBe('0');
|
|
expect(platform.escape(-1)).toBe('-1');
|
|
expect(platform.escape(3.14)).toBe('3.14');
|
|
});
|
|
|
|
it("formatQuery renders `NULL` for a bound NaN, matching better-sqlite3's own binding of NaN as NULL", () => {
|
|
db.exec('CREATE TABLE nanprobe (n REAL)');
|
|
db.exec(platform.formatQuery('insert into nanprobe (n) values (?)', [NaN]));
|
|
const viaLiteral = db.prepare('select n from nanprobe').get() as { n: unknown };
|
|
const viaBind = db.prepare('select ? as n').get(NaN) as { n: unknown };
|
|
expect(viaLiteral.n).toBeNull();
|
|
expect(viaBind.n).toBeNull();
|
|
});
|
|
|
|
it("formatQuery renders `9e999`/`-9e999` for bound ±Infinity, matching better-sqlite3's own REAL binding", () => {
|
|
db.exec('CREATE TABLE infprobe (n REAL)');
|
|
db.exec(platform.formatQuery('insert into infprobe (n) values (?), (?)', [Infinity, -Infinity]));
|
|
const rows = db.prepare('select n from infprobe order by rowid').all() as { n: number }[];
|
|
expect(rows.map((r) => r.n)).toEqual([Infinity, -Infinity]);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The mechanism above through the REAL ORM call path (`findOne`,
|
|
* `nativeUpdate`, a QB `where`, and an `$in` list), against a real trip
|
|
* row — the shape `PlacesService.verifyTripAccess`/`RealtimeGateway
|
|
* .handleJoin` reach when a non-finite id slips past their own guard.
|
|
* Before this override, every one of these threw `SqliteError: no such
|
|
* column: NaN`; after, each answers a clean miss.
|
|
*/
|
|
describe('non-finite numbers through the real ORM call path', () => {
|
|
const orm = createSnapshotTestDb();
|
|
let t: TestOrm;
|
|
|
|
beforeAll(async () => {
|
|
t = await createTestOrm(orm);
|
|
});
|
|
beforeEach(() => {
|
|
resetTestDb(orm);
|
|
t.clear();
|
|
});
|
|
afterAll(async () => {
|
|
await t.close();
|
|
orm.close();
|
|
});
|
|
|
|
it('em.findOne(Trips, { id: NaN }) misses cleanly, never throws', async () => {
|
|
const { user } = createUser(orm);
|
|
createTrip(orm, user.id);
|
|
await expect(t.em.findOne(Trips, { id: NaN })).resolves.toBeNull();
|
|
});
|
|
|
|
it('em.nativeUpdate(Trips, { id: NaN }, …) updates 0 rows, never throws', async () => {
|
|
const { user } = createUser(orm);
|
|
const trip = createTrip(orm, user.id);
|
|
const affected = await t.em.nativeUpdate(Trips, { id: NaN }, { title: 'renamed' });
|
|
expect(affected).toBe(0);
|
|
const untouched = orm.prepare('SELECT title FROM trips WHERE id = ?').get(trip.id) as { title: string };
|
|
expect(untouched.title).not.toBe('renamed');
|
|
});
|
|
|
|
it('a QB `where` on a non-finite id misses cleanly, never throws (the exact `findAccessible` shape)', async () => {
|
|
const { user } = createUser(orm);
|
|
createTrip(orm, user.id);
|
|
const row = await t.em
|
|
.getRepository(Trips)
|
|
.qb('t')
|
|
.select(['t.id'])
|
|
.where('t.id = ?', [NaN])
|
|
.execute<{ id: number } | undefined>('get', false);
|
|
expect(row).toBeUndefined();
|
|
});
|
|
|
|
it('an `$in` list containing NaN/Infinity still matches the finite members, never throws', async () => {
|
|
const { user } = createUser(orm);
|
|
const trip = createTrip(orm, user.id);
|
|
const rows = await t.em.find(Trips, { id: { $in: [trip.id, NaN, Infinity, -Infinity] } });
|
|
expect(rows.map((r) => r.id)).toEqual([trip.id]);
|
|
});
|
|
});
|
|
});
|