Files
insforge--insforge/backend/tests/unit/records-empty-string-stripping.test.ts
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

133 lines
4.4 KiB
TypeScript

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<string, unknown>,
columnTypeMap: Record<string, string>
): Record<string, unknown> {
const filtered: Record<string, unknown> = {};
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<string, string> = {
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');
});
});
});