import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import Database from 'better-sqlite3'; import { DyFM_Error } from '@futdevpro/fsm-dynamo'; import { DyNTS_Sqlite_Reader_Util } from './dynts-sqlite-reader.util'; import { DyNTS_SqliteColumnInfo } from '../_models/interfaces/dynts-sqlite-reader.interface'; interface NameInterface { name: string; } /** * `DyNTS_Sqlite_Reader_Util` spec (BFR-AM-009). Egy temp `.db` fájlt hozunk létre (a reader path-szal * + `fileMustExist:true` nyit, ezért valódi fájl kell — a `:memory:` path-ön nem ad fájlt), feltöltjük, * majd igazoljuk: * - `listTables` (a belső `sqlite_%` táblák kiszűrve, ABC-sorrend), * - `getSchema` (oszlop-info: name/type/notNull/primaryKey), * - `readTable` (összes sor) + nem-létező tábla → strukturált hiba, * - `query` SELECT/WITH fut + paraméteres SELECT, * - `query` NEM-SELECT (INSERT/UPDATE/DELETE/DROP/multi-statement) → READONLY-hiba (elutasít), * - hiányzó path / nem-létező fájl → strukturált hiba. */ describe('| DyNTS_Sqlite_Reader_Util (read-only SQLite reader, BFR-AM-009)', () => { /** A temp DB-fájl útvonala (minden teszt-futáshoz egyedi). */ let dbPath: string; beforeAll(() => { dbPath = path.join(os.tmpdir(), `dynts-sqlite-spec-${process.pid}-${Date.now()}.db`); const db: Database.Database = new Database(dbPath); db.exec(` CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER); CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT); `); db.prepare('INSERT INTO users (id, name, age) VALUES (?, ?, ?)').run(1, 'Ada', 36); db.prepare('INSERT INTO users (id, name, age) VALUES (?, ?, ?)').run(2, 'Grace', 40); db.prepare('INSERT INTO posts (id, title) VALUES (?, ?)').run(1, 'Hello'); db.close(); }); afterAll(() => { if (dbPath && fs.existsSync(dbPath)) { fs.unlinkSync(dbPath); } }); /** Egy hívás, ami egy adott errorCode-dal kell hogy dobjon (strukturált, NEM néma). */ const expectErrorCode = (fn: () => unknown, expectedCode: string): void => { let thrown: unknown; try { fn(); } catch (error) { thrown = error; } expect(thrown).withContext(`dobnia kellett (${expectedCode})`).toBeDefined(); const code: string | undefined = thrown instanceof DyFM_Error ? DyFM_Error.getErrorCode(thrown) : undefined; expect(code).withContext('a hibakód').toBe(expectedCode); }; describe('| listTables', () => { it('| a user-táblák ABC-sorrendben (a belső sqlite_% táblák kiszűrve)', () => { expect(DyNTS_Sqlite_Reader_Util.listTables(dbPath)).toEqual([ 'posts', 'users' ]); }); }); describe('| getSchema', () => { it('| a users tábla oszlop-info-ja (name/type/notNull/primaryKey)', () => { const schema: DyNTS_SqliteColumnInfo[] = DyNTS_Sqlite_Reader_Util.getSchema(dbPath, 'users'); expect(schema.map((column) => column.name)).toEqual([ 'id', 'name', 'age' ]); const idColumn: DyNTS_SqliteColumnInfo = schema.find((column) => column.name === 'id'); expect(idColumn.primaryKey).toBe(true); const nameColumn: DyNTS_SqliteColumnInfo = schema.find((column) => column.name === 'name'); expect(nameColumn.notNull).toBe(true); expect(nameColumn.type).toBe('TEXT'); }); it('| nem-létező tábla → DyNTS-SQLITE-READ-002 (NEM néma)', () => { expectErrorCode(() => DyNTS_Sqlite_Reader_Util.getSchema(dbPath, 'no_such_table'), 'DyNTS-SQLITE-READ-002'); }); }); describe('| readTable', () => { it('| a users tábla minden sora', () => { const rows: { id: number; name: string }[] = DyNTS_Sqlite_Reader_Util.readTable(dbPath, 'users') as { id: number; name: string }[]; expect(rows.length).toBe(2); expect(rows.map((row) => row.name).sort()).toEqual([ 'Ada', 'Grace' ]); }); it('| nem-létező tábla → DyNTS-SQLITE-READ-002 (NEM néma-[])', () => { expectErrorCode(() => DyNTS_Sqlite_Reader_Util.readTable(dbPath, 'ghost'), 'DyNTS-SQLITE-READ-002'); }); }); describe('| query (SELECT-only)', () => { it('| egy SELECT lekérdezés sorai', () => { const rows: NameInterface[] = DyNTS_Sqlite_Reader_Util.query(dbPath, 'SELECT name FROM users ORDER BY age DESC') as NameInterface[]; expect(rows.map((row) => row.name)).toEqual([ 'Grace', 'Ada' ]); }); it('| paraméteres SELECT (prepared-statement param)', () => { const rows: NameInterface[] = DyNTS_Sqlite_Reader_Util.query(dbPath, 'SELECT * FROM users WHERE age > ?', [ 37 ]) as NameInterface[]; expect(rows.map((row) => row.name)).toEqual([ 'Grace' ]); }); it('| WITH (CTE) is engedett (read-only)', () => { const rows: { n: number }[] = DyNTS_Sqlite_Reader_Util.query(dbPath, 'WITH adults AS (SELECT * FROM users WHERE age >= 36) SELECT COUNT(*) AS n FROM adults') as { n: number }[]; expect(rows[0].n).toBe(2); }); it('| INSERT → DyNTS-SQLITE-READONLY-001 (elutasít a forrás-rétegben)', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.query(dbPath, 'INSERT INTO users (id, name) VALUES (9, \'x\')'), 'DyNTS-SQLITE-READONLY-001'); }); it('| UPDATE → DyNTS-SQLITE-READONLY-001', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.query(dbPath, 'UPDATE users SET name=\'x\' WHERE id=1'), 'DyNTS-SQLITE-READONLY-001'); }); it('| DELETE → DyNTS-SQLITE-READONLY-001', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.query(dbPath, 'DELETE FROM users'), 'DyNTS-SQLITE-READONLY-001'); }); it('| DROP → DyNTS-SQLITE-READONLY-001', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.query(dbPath, 'DROP TABLE users'), 'DyNTS-SQLITE-READONLY-001'); }); it('| multi-statement (SELECT; DELETE) → DyNTS-SQLITE-READONLY-001 (nem rejthető írás SELECT mögé)', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.query(dbPath, 'SELECT 1; DELETE FROM users'), 'DyNTS-SQLITE-READONLY-001'); }); it('| üres SQL → DyNTS-SQLITE-READONLY-001', () => { expectErrorCode(() => DyNTS_Sqlite_Reader_Util.query(dbPath, ' '), 'DyNTS-SQLITE-READONLY-001'); }); }); describe('| path-validáció (fail-soft, NEM crash)', () => { it('| üres path → DyNTS-SQLITE-READ-001', () => { expectErrorCode(() => DyNTS_Sqlite_Reader_Util.listTables(''), 'DyNTS-SQLITE-READ-001'); }); it('| nem-létező fájl → DyNTS-SQLITE-READ-001', () => { expectErrorCode( () => DyNTS_Sqlite_Reader_Util.listTables(path.join(os.tmpdir(), 'no-such-db-file-xyz.db')), 'DyNTS-SQLITE-READ-001'); }); }); });