import { describe, expect, it } from 'vitest'; import { generateKontenCsv, parseKontenCsv } from './handelsware-konten-csv'; const csv = (text: string, enc: BufferEncoding = 'utf8') => Buffer.from(text, enc); describe('parseKontenCsv', () => { it('liest CRLF und LF gleich', () => { const a = parseKontenCsv(csv('Kaffee;2010;4000\r\nTee;2011;4001\r\n'), 4711); const b = parseKontenCsv(csv('Kaffee;2010;4000\nTee;2011;4001'), 4711); expect(a.accounts).toEqual(b.accounts); expect(a.accounts).toHaveLength(2); expect(a.errors).toEqual([]); }); it('dekodiert Windows-1252 und UTF-8 mit BOM', () => { expect(parseKontenCsv(csv('Käse;2010;4000', 'latin1'), null).accounts[0].name).toBe('Käse'); const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), csv('Käse;2010;4000')]); expect(parseKontenCsv(bom, null).accounts[0].name).toBe('Käse'); }); it('ueberspringt eine Kopfzeile, wenn die zweite Spalte keine Zahl ist', () => { const r = parseKontenCsv(csv('Name;Gegenkonto;Konto\nKaffee;2010;4000'), null); expect(r.accounts).toEqual([{ name: 'Kaffee', gegenkonto: 2010, erloeskonto: 4000 }]); expect(r.errors).toEqual([]); }); it('nimmt fuer eine fehlende dritte Spalte das Standard-Erloeskonto', () => { const r = parseKontenCsv(csv('Kaffee;2010'), 4711); expect(r.accounts[0].erloeskonto).toBe(4711); }); it('meldet missingErloeskonto, wenn das Standard-Erloeskonto leer ist', () => { const r = parseKontenCsv(csv('Kaffee;2010'), null); expect(r.errors).toEqual([expect.objectContaining({ line: 1, code: 'missingErloeskonto' })]); }); it('meldet ungueltige Zahlen und leere Namen mit Zeilennummer', () => { const r = parseKontenCsv(csv('ok;1;2\n;5;6\nx;abc;6\ny;5;-1\nz;0;6'), null); expect(r.errors.map((e) => [e.line, e.code])).toEqual([ [2, 'nameEmpty'], [3, 'gegenkontoInvalid'], [4, 'erloeskontoInvalid'], [5, 'gegenkontoInvalid'], ]); }); it('meldet doppelte Namen', () => { const r = parseKontenCsv(csv('Kaffee;1;2\nKaffee;3;4'), null); expect(r.errors).toEqual([expect.objectContaining({ line: 2, code: 'duplicateName' })]); }); it('meldet zu lange Namen', () => { const r = parseKontenCsv(csv(`${'x'.repeat(121)};1;2`), null); expect(r.errors[0].code).toBe('nameTooLong'); }); it('erlaubt Semikolons im Namen', () => { const r = parseKontenCsv(csv('Tee; gruen;2010;4000'), null); expect(r.accounts[0]).toEqual({ name: 'Tee; gruen', gegenkonto: 2010, erloeskonto: 4000 }); }); }); describe('generateKontenCsv', () => { it('beginnt mit BOM, nutzt Semikolon und CRLF', () => { const out = generateKontenCsv([ { name: 'Käse', gegenkonto: 2010, erloeskonto: 4000 }, { name: 'Tee', gegenkonto: 2011, erloeskonto: 4001 }, ]); expect(out.startsWith('')).toBe(true); expect(out.slice(1)).toBe('Käse;2010;4000\r\nTee;2011;4001\r\n'); }); it.each([ '=SUMME(A1)', '+1', '-5 % Aktion', '@cmd', ])('schuetzt %j mit einem Apostroph', (name) => { const out = generateKontenCsv([{ name, gegenkonto: 1, erloeskonto: 2 }]); expect(out.slice(1).startsWith(`'${name};`)).toBe(true); }); it('Export und Import ergeben dieselbe Liste (Rundlauf)', () => { const accounts = [ { name: '=1+1', gegenkonto: 2010, erloeskonto: 4000 }, { name: '-5 % Aktion', gegenkonto: 2011, erloeskonto: 4000 }, { name: 'Käse', gegenkonto: 2012, erloeskonto: 4001 }, ]; const back = parseKontenCsv(Buffer.from(generateKontenCsv(accounts), 'utf8'), null); expect(back.errors).toEqual([]); expect(back.accounts).toEqual(accounts); }); it('leere Liste ergibt nur das BOM', () => { expect(generateKontenCsv([])).toBe(''); }); });