101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
import { strict as assert } from 'node:assert';
|
|
import { describe, it } from 'node:test';
|
|
import { ACCOUNT_SIDES } from '@pig/core';
|
|
import type { Database } from '@pig/db';
|
|
import type { Principal } from '../src/lib/auth';
|
|
import { executeMutation } from '../src/lib/mutation';
|
|
import { createImportCommitMutationDefinition } from '../src/routes/imports';
|
|
import { convertImportRow, findDuplicateImportKeys } from '../src/services/imports';
|
|
import { parseCsv, parseWorksheetXml } from '../src/services/tabular-import';
|
|
|
|
const principal: Principal = {
|
|
userId: '00000000-0000-4000-8000-000000000001',
|
|
email: 'admin@example.com',
|
|
name: 'Import admin',
|
|
isPlatformAdmin: false,
|
|
teams: [{ team: 'demand', role: 'admin' }],
|
|
via: 'jwt',
|
|
scopes: ['read', 'write'],
|
|
};
|
|
|
|
describe('untrusted tabular parsing', () => {
|
|
it('keeps multiline CSV and formula-like cells as inert text', () => {
|
|
const parsed = parseCsv('external_id,name\n1,"Acme\nCompute"\n2,"=WEBSERVICE(""https://example.test"")"');
|
|
assert.deepEqual(parsed.headers, ['external_id', 'name']);
|
|
assert.equal(parsed.rows[0]?.[1], 'Acme\nCompute');
|
|
assert.equal(parsed.rows[1]?.[1], '=WEBSERVICE("https://example.test")');
|
|
assert.match(parsed.warnings.join(' '), /inert text/);
|
|
});
|
|
|
|
it('does not execute XLSX formulas or follow formula URLs', () => {
|
|
const parsed = parseWorksheetXml(
|
|
'<worksheet><sheetData><row>' +
|
|
'<c r="A1" t="inlineStr"><is><t>external_id</t></is></c>' +
|
|
'<c r="B1" t="inlineStr"><is><t>score</t></is></c>' +
|
|
'</row><row><c r="A2"><v>1</v></c>' +
|
|
'<c r="B2"><f>WEBSERVICE("https://example.test")</f><v>7</v></c>' +
|
|
'</row></sheetData></worksheet>',
|
|
);
|
|
assert.deepEqual(parsed.rows, [['1', '7']]);
|
|
assert.match(parsed.warnings.join(' '), /not executed/);
|
|
});
|
|
});
|
|
|
|
describe('import row decisions', () => {
|
|
it('rejects duplicate user-selected source identities', () => {
|
|
assert.deepEqual([...findDuplicateImportKeys(['vendor-1', 'vendor-2', 'vendor-1'])], ['vendor-1']);
|
|
});
|
|
|
|
it('validates ontology values from core rather than accepting invented sides', () => {
|
|
const converted = convertImportRow(
|
|
'account',
|
|
['external_id', 'name', 'side'],
|
|
['vendor-1', 'Acme', 'marketplace'],
|
|
{ name: 'name', side: 'side' },
|
|
true,
|
|
);
|
|
assert.equal(converted.values.name, 'Acme');
|
|
const message = converted.errors[0]?.message ?? '';
|
|
assert.match(message, /must be one of/);
|
|
for (const side of ACCOUNT_SIDES) assert.ok(message.includes(side));
|
|
});
|
|
});
|
|
|
|
describe('import commit mutation', () => {
|
|
it('commits imported records and their audit evidence in one transaction', async () => {
|
|
const events: string[] = [];
|
|
const tx = {
|
|
insert: () => ({ values: async () => events.push('activity') }),
|
|
};
|
|
const db = {
|
|
transaction: async (work: (transaction: unknown) => Promise<unknown>) => {
|
|
events.push('begin');
|
|
const result = await work(tx);
|
|
events.push('commit');
|
|
return result;
|
|
},
|
|
} as unknown as Database;
|
|
const definition = createImportCommitMutationDefinition((transaction) => {
|
|
assert.equal(transaction, tx);
|
|
return {
|
|
commit: async () => {
|
|
events.push('records-and-identities');
|
|
return { created: 1, updated: 0, total: 1 };
|
|
},
|
|
};
|
|
});
|
|
|
|
await executeMutation(db, principal, async () => ({
|
|
entity: 'account',
|
|
sourceName: 'accounts.csv',
|
|
headers: ['external_id', 'name', 'side'],
|
|
rows: [['vendor-1', 'Acme', 'demand']],
|
|
mapping: { name: 'name', side: 'side' },
|
|
keySourceColumn: 'external_id',
|
|
previewDigest: 'a'.repeat(64),
|
|
}), definition);
|
|
|
|
assert.deepEqual(events, ['begin', 'records-and-identities', 'activity', 'commit']);
|
|
});
|
|
});
|