131 lines
4.5 KiB
TypeScript
131 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* Integration test: commit runner + undo.
|
||
|
|
*
|
||
|
|
* Commits a 3-row companies "file" (skip existing / insert new / error blank)
|
||
|
|
* and asserts counts, per-row ledger, and the actual inserted entity. Then
|
||
|
|
* undoes the batch and asserts the inserted company is gone. Real test DB.
|
||
|
|
*/
|
||
|
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||
|
|
import { and, eq, sql } from 'drizzle-orm';
|
||
|
|
|
||
|
|
import { db } from '@/lib/db';
|
||
|
|
import { importBatches, importBatchRows } from '@/lib/db/schema/imports';
|
||
|
|
import { companies } from '@/lib/db/schema/companies';
|
||
|
|
import { commitBatch, undoBatch } from '@/lib/import/commit';
|
||
|
|
import { companiesAdapter } from '@/lib/import/adapters/companies';
|
||
|
|
import type { ConflictPolicy, RawRow } from '@/lib/import/types';
|
||
|
|
|
||
|
|
let makePort: typeof import('../helpers/factories').makePort;
|
||
|
|
let makeCompany: typeof import('../helpers/factories').makeCompany;
|
||
|
|
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
const f = await import('../helpers/factories');
|
||
|
|
makePort = f.makePort;
|
||
|
|
makeCompany = f.makeCompany;
|
||
|
|
makeAuditMeta = f.makeAuditMeta;
|
||
|
|
});
|
||
|
|
|
||
|
|
async function makeBatch(portId: string, policy: ConflictPolicy): Promise<string> {
|
||
|
|
const [b] = await db
|
||
|
|
.insert(importBatches)
|
||
|
|
.values({
|
||
|
|
portId,
|
||
|
|
entityType: 'companies',
|
||
|
|
filename: 'companies.csv',
|
||
|
|
storageKey: 'unused-in-direct-commit',
|
||
|
|
mappingJson: { name: 'Name' },
|
||
|
|
conflictPolicy: policy,
|
||
|
|
createdBy: 'tester',
|
||
|
|
})
|
||
|
|
.returning({ id: importBatches.id });
|
||
|
|
return b!.id;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('commitBatch + undoBatch', () => {
|
||
|
|
const rows: RawRow[] = [{ Name: 'Acme Marine' }, { Name: 'Fresh Imports Ltd' }, { Name: '' }];
|
||
|
|
|
||
|
|
it('commits (skip/insert/error), records the ledger, then undoes the insert', async () => {
|
||
|
|
const port = await makePort();
|
||
|
|
await makeCompany({ portId: port.id, overrides: { name: 'Acme Marine' } });
|
||
|
|
const batchId = await makeBatch(port.id, 'skip-matches');
|
||
|
|
const ctx = { portId: port.id, meta: makeAuditMeta({ portId: port.id }) };
|
||
|
|
|
||
|
|
const counts = await commitBatch({
|
||
|
|
batchId,
|
||
|
|
adapter: companiesAdapter,
|
||
|
|
rawRows: rows,
|
||
|
|
mapping: { name: 'Name' },
|
||
|
|
policy: 'skip-matches',
|
||
|
|
ctx,
|
||
|
|
});
|
||
|
|
expect(counts).toEqual({ inserted: 1, updated: 0, skipped: 1, errored: 1 });
|
||
|
|
|
||
|
|
const [batch] = await db.select().from(importBatches).where(eq(importBatches.id, batchId));
|
||
|
|
expect(batch!.status).toBe('completed');
|
||
|
|
expect(batch!.inserted).toBe(1);
|
||
|
|
|
||
|
|
const ledger = await db
|
||
|
|
.select()
|
||
|
|
.from(importBatchRows)
|
||
|
|
.where(eq(importBatchRows.batchId, batchId))
|
||
|
|
.orderBy(importBatchRows.rowNumber);
|
||
|
|
expect(ledger.map((r) => r.action)).toEqual(['skipped', 'inserted', 'errored']);
|
||
|
|
expect(ledger[2]!.error).toBeTruthy();
|
||
|
|
|
||
|
|
// The new company really exists.
|
||
|
|
const fresh = await db
|
||
|
|
.select({ id: companies.id })
|
||
|
|
.from(companies)
|
||
|
|
.where(
|
||
|
|
and(eq(companies.portId, port.id), sql`lower(${companies.name}) = 'fresh imports ltd'`),
|
||
|
|
);
|
||
|
|
expect(fresh).toHaveLength(1);
|
||
|
|
|
||
|
|
// Undo removes exactly the inserted row.
|
||
|
|
const undo = await undoBatch(batchId, port.id);
|
||
|
|
expect(undo.deleted).toBe(1);
|
||
|
|
expect(undo.blocked).toBe(0);
|
||
|
|
|
||
|
|
const afterUndo = await db
|
||
|
|
.select({ id: companies.id })
|
||
|
|
.from(companies)
|
||
|
|
.where(
|
||
|
|
and(eq(companies.portId, port.id), sql`lower(${companies.name}) = 'fresh imports ltd'`),
|
||
|
|
);
|
||
|
|
expect(afterUndo).toHaveLength(0);
|
||
|
|
|
||
|
|
const [undoneBatch] = await db
|
||
|
|
.select({ status: importBatches.status })
|
||
|
|
.from(importBatches)
|
||
|
|
.where(eq(importBatches.id, batchId));
|
||
|
|
expect(undoneBatch!.status).toBe('undone');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('update-matches overwrites the matched company', async () => {
|
||
|
|
const port = await makePort();
|
||
|
|
await makeCompany({
|
||
|
|
portId: port.id,
|
||
|
|
overrides: { name: 'Acme Marine', legalName: 'OLD' },
|
||
|
|
});
|
||
|
|
const batchId = await makeBatch(port.id, 'update-matches');
|
||
|
|
const ctx = { portId: port.id, meta: makeAuditMeta({ portId: port.id }) };
|
||
|
|
|
||
|
|
const counts = await commitBatch({
|
||
|
|
batchId,
|
||
|
|
adapter: companiesAdapter,
|
||
|
|
rawRows: [{ Name: 'Acme Marine', Legal: 'NEW Ltd' }],
|
||
|
|
mapping: { name: 'Name', legalName: 'Legal' },
|
||
|
|
policy: 'update-matches',
|
||
|
|
ctx,
|
||
|
|
});
|
||
|
|
expect(counts.updated).toBe(1);
|
||
|
|
|
||
|
|
const [c] = await db
|
||
|
|
.select({ legalName: companies.legalName })
|
||
|
|
.from(companies)
|
||
|
|
.where(and(eq(companies.portId, port.id), sql`lower(${companies.name}) = 'acme marine'`));
|
||
|
|
expect(c!.legalName).toBe('NEW Ltd');
|
||
|
|
});
|
||
|
|
});
|