/** * lib/sqlcmd — pure-layer tests (arg contract + output parsing; no DB spawn). */ import { describe, it, expect } from 'vitest'; import { buildSqlcmdArgs, isCodePageFlagError, parsePipeRows, parseForJson } from '../sqlcmd.js'; import type { ParsedConnection } from '../appsettings.js'; function conn(over: Partial = {}): ParsedConnection { return { server: '(local)', database: 'SmartStack', useWindowsAuth: true, user: null, password: null, trustServerCertificate: false, raw: '', ...over, }; } describe('buildSqlcmdArgs', () => { it('uses trusted auth (-E) for Windows auth and a default pipe separator', () => { const args = buildSqlcmdArgs(conn(), 'SELECT 1'); expect(args).toContain('-E'); expect(args).not.toContain('-U'); expect(args.join(' ')).toContain('-s |'); expect(args.slice(-2)).toEqual(['-Q', 'SELECT 1']); expect(args).toContain('(local)'); expect(args).toContain('SmartStack'); }); it('uses -U/-P when a SQL user is configured', () => { const args = buildSqlcmdArgs(conn({ useWindowsAuth: false, user: 'sa', password: 'pw' }), 'SELECT 1'); expect(args).toContain('-U'); expect(args).toContain('sa'); expect(args).toContain('-P'); expect(args).toContain('pw'); expect(args).not.toContain('-E'); }); it('switches to FOR JSON flags (-y 0, no separator) when forJson', () => { const args = buildSqlcmdArgs(conn(), 'SELECT 1 FOR JSON PATH', { forJson: true }); expect(args.join(' ')).toContain('-y 0'); expect(args).not.toContain('-s'); }); it('adds -C only when the connection trusts the server certificate', () => { expect(buildSqlcmdArgs(conn({ trustServerCertificate: true }), 'X')).toContain('-C'); expect(buildSqlcmdArgs(conn({ trustServerCertificate: false }), 'X')).not.toContain('-C'); }); it('forces the UTF-8 client code page (-f 65001) by default, in BOTH output modes', () => { expect(buildSqlcmdArgs(conn(), 'X').join(' ')).toContain('-f 65001'); expect(buildSqlcmdArgs(conn(), 'X', { forJson: true }).join(' ')).toContain('-f 65001'); }); it('honours an explicit codePage and omits -f entirely on codePage: null', () => { expect(buildSqlcmdArgs(conn(), 'X', { codePage: 850 }).join(' ')).toContain('-f 850'); expect(buildSqlcmdArgs(conn(), 'X', { codePage: null })).not.toContain('-f'); }); }); describe('isCodePageFlagError', () => { it('detects a binary rejecting the -f flag (go-sqlcmd shapes)', () => { expect(isCodePageFlagError("unknown shorthand flag: 'f' in -f")).toBe(true); expect(isCodePageFlagError('unrecognized option: -f')).toBe(true); expect(isCodePageFlagError("invalid option -- 'f'")).toBe(true); }); it('does not fire on ordinary SQL failures', () => { expect(isCodePageFlagError("Invalid object name 'core.auth_Roles'.")).toBe(false); expect(isCodePageFlagError('Login failed for user "sa".')).toBe(false); expect(isCodePageFlagError('')).toBe(false); }); }); describe('parsePipeRows', () => { it('splits cells, drops blank and pure-dash lines, trims trailing space', () => { const raw = 'APP|1|admin|Admin||/admin|0 \n\n------\nMOD|2|users|Users|1|/users|0'; expect(parsePipeRows(raw)).toEqual([ ['APP', '1', 'admin', 'Admin', '', '/admin', '0'], ['MOD', '2', 'users', 'Users', '1', '/users', '0'], ]); }); }); describe('parseForJson', () => { it('reassembles JSON split across rows', () => { const raw = '[{"a":1},\n{"a":2}]'; expect(parseForJson<{ a: number }>(raw)).toEqual([{ a: 1 }, { a: 2 }]); }); it('returns [] for empty / null output', () => { expect(parseForJson('')).toEqual([]); expect(parseForJson('NULL')).toEqual([]); }); it('returns [] when the payload is not an array', () => { expect(parseForJson('{"a":1}')).toEqual([]); }); it('throws on malformed JSON', () => { expect(() => parseForJson('[{not json')).toThrow(/Could not parse sqlcmd JSON/); }); });