import { describe, expect, it } from 'vitest'; import { TEXT_LIKE_DATA_TYPES } from '../../src/utils/constants'; /** * Tests the empty-string stripping contract used by records.routes.ts. * * Both single-record and bulk-record write paths should apply the same rule: * - Strip empty strings for non-text-like columns (uuid, integer, boolean, date, etc.) * - Preserve empty strings for text-like columns (text, character varying, character, citext) * * This matches the canonical logic in AdminRecordService.sanitizeInsertRecord(). */ /** Replicates the filtering logic from records.routes.ts for a single record. */ function filterRecord( record: Record, columnTypeMap: Record ): Record { const filtered: Record = {}; for (const key in record) { if (!TEXT_LIKE_DATA_TYPES.has(columnTypeMap[key] ?? '') && record[key] === '') { continue; } filtered[key] = record[key]; } return filtered; } describe('records empty-string stripping', () => { const columnTypeMap: Record = { id: 'uuid', name: 'text', description: 'character varying', tag: 'character', note: 'citext', count: 'integer', price: 'numeric', is_active: 'boolean', created_at: 'timestamp with time zone', metadata: 'jsonb', }; describe('strips empty strings for non-text-like columns', () => { it.each([ ['uuid', 'id'], ['integer', 'count'], ['numeric', 'price'], ['boolean', 'is_active'], ['timestamp with time zone', 'created_at'], ['jsonb', 'metadata'], ])('strips empty string for %s column (%s)', (_type, column) => { const result = filterRecord({ [column]: '' }, columnTypeMap); expect(result).not.toHaveProperty(column); }); }); describe('preserves empty strings for text-like columns', () => { it.each([ ['text', 'name'], ['character varying', 'description'], ['character', 'tag'], ['citext', 'note'], ])('preserves empty string for %s column (%s)', (_type, column) => { const result = filterRecord({ [column]: '' }, columnTypeMap); expect(result).toHaveProperty(column, ''); }); }); it('strips empty string for unknown column (safe default)', () => { const result = filterRecord({ unknown_col: '' }, columnTypeMap); expect(result).not.toHaveProperty('unknown_col'); }); it('preserves non-empty values regardless of column type', () => { const record = { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Test', count: '42', is_active: 'true', }; const result = filterRecord(record, columnTypeMap); expect(result).toEqual(record); }); it('applies the same filtering for single and bulk records', () => { const record = { id: '', name: '', description: '', count: '', is_active: '', }; // Single-record path const singleResult = filterRecord(record, columnTypeMap); // Bulk-record path (same logic applied via Array.map) const bulkResult = [record].map((item) => filterRecord(item, columnTypeMap))[0]; expect(singleResult).toEqual(bulkResult); expect(singleResult).toEqual({ name: '', description: '', }); }); describe('TEXT_LIKE_DATA_TYPES set', () => { it('contains exactly the expected types', () => { expect([...TEXT_LIKE_DATA_TYPES].sort()).toEqual( ['character', 'character varying', 'citext', 'text'].sort() ); }); }); describe('USER-DEFINED type normalization', () => { it('citext resolved via udt_name is preserved (matches DatabaseManager normalization)', () => { // DatabaseManager.getColumnTypeMap now normalizes USER-DEFINED → udt_name, // so the columnTypeMap will contain 'citext' not 'USER-DEFINED'. const normalizedMap = { note: 'citext' }; const result = filterRecord({ note: '' }, normalizedMap); expect(result).toHaveProperty('note', ''); }); it('raw USER-DEFINED without normalization would incorrectly strip (regression guard)', () => { // If normalization were missing, the map would contain 'USER-DEFINED' // which is NOT in TEXT_LIKE_DATA_TYPES, so empty strings would be stripped. const rawMap = { note: 'USER-DEFINED' }; const result = filterRecord({ note: '' }, rawMap); expect(result).not.toHaveProperty('note'); }); }); });