// CSV import parsing tests — parseCSV (RFC-ish tokenizer), findColumn // (header heuristics), and parseEntriesFromCSV (multi-format mapping). // These are pure functions with no state/DOM coupling. const test = require('node:test'); const assert = require('node:assert/strict'); const { loadApp } = require('./harness.js'); const T = loadApp().__test; // Values returned from the vm sandbox carry the sandbox realm's prototypes, // so deepStrictEqual's prototype check trips. Normalize through JSON to // compare by structure (fine for plain data). const eqDeep = (actual, expected, msg) => assert.deepEqual(JSON.parse(JSON.stringify(actual)), expected, msg); test('parseCSV: simple rows', () => { eqDeep(T.parseCSV('a,b,c\n1,2,3'), [['a', 'b', 'c'], ['1', '2', '3']]); }); test('parseCSV: quoted fields with commas and newlines', () => { const rows = T.parseCSV('name,note\n"Smith, John","line1\nline2"'); eqDeep(rows, [['name', 'note'], ['Smith, John', 'line1\nline2']]); }); test('parseCSV: escaped double-quotes ("")', () => { const rows = T.parseCSV('v\n"say ""hi"""'); eqDeep(rows, [['v'], ['say "hi"']]); }); test('parseCSV: CRLF line endings', () => { eqDeep(T.parseCSV('a,b\r\n1,2\r\n'), [['a', 'b'], ['1', '2']]); }); test('parseCSV: trailing field with no final newline', () => { eqDeep(T.parseCSV('a,b\n1,2'), [['a', 'b'], ['1', '2']]); }); test('parseCSV: blank lines are dropped', () => { eqDeep(T.parseCSV('a,b\n\n1,2\n'), [['a', 'b'], ['1', '2']]); }); test('findColumn: case/underscore/dash-insensitive matching', () => { const headers = ['Login_URI', 'User Name', 'PASSWORD']; assert.equal(T.findColumn(headers, ['login_uri']), 0); assert.equal(T.findColumn(headers, ['username', 'user_name']), 1); assert.equal(T.findColumn(headers, ['password']), 2); assert.equal(T.findColumn(headers, ['nope']), null); }); test('parseEntriesFromCSV: Bitwarden-style header maps site/user/pwd/totp', () => { const csv = [ 'folder,name,login_uri,login_username,login_password,login_totp,notes', 'Work,GitHub,https://github.com,octocat,s3cret,JBSWY3DPEHPK3PXP,hello', ].join('\n'); const { entries, skipped } = T.parseEntriesFromCSV(csv); assert.equal(skipped, 0); assert.equal(entries.length, 1); const e = entries[0]; assert.equal(e.title, 'GitHub'); assert.equal(e.site, 'https://github.com'); assert.equal(e.username, 'octocat'); assert.equal(e.password, 's3cret'); assert.equal(e.totp_secret, 'JBSWY3DPEHPK3PXP'); assert.equal(e.folder, 'Work'); }); test('parseEntriesFromCSV: KeePass-style header (Title/URL/Username/Password/Group)', () => { const csv = [ 'Title,URL,Username,Password,Group,Notes', 'Bank,https://bank.example,alice,pw123,Finance,note', ].join('\n'); const { entries } = T.parseEntriesFromCSV(csv); assert.equal(entries[0].title, 'Bank'); assert.equal(entries[0].site, 'https://bank.example'); assert.equal(entries[0].username, 'alice'); assert.equal(entries[0].folder, 'Finance'); }); test('parseEntriesFromCSV: note heuristic — empty site+pwd but notes present → kind=note', () => { const csv = [ 'name,url,username,password,notes', 'My Note,,,,"just some text"', ].join('\n'); const { entries } = T.parseEntriesFromCSV(csv); assert.equal(entries.length, 1); assert.equal(entries[0].kind, 'note'); }); test('parseEntriesFromCSV: explicit kind=note wins even with site+password present', () => { // A row with site + password normally classifies as a login; an explicit // kind=note must override that (precedence: explicit column > heuristic). const csv = [ 'name,url,username,password,kind', 'Recovery Codes,https://example.com,user,BACKUP-CODES,note', ].join('\n'); const { entries } = T.parseEntriesFromCSV(csv); assert.equal(entries.length, 1); assert.equal(entries[0].kind, 'note'); assert.equal(entries[0].site, ''); // notes never carry a site assert.equal(entries[0].password, 'BACKUP-CODES'); // body preserved }); test('parseEntriesFromCSV: throws on missing password column', () => { assert.throws(() => T.parseEntriesFromCSV('name,url\nfoo,bar'), /password column/i); }); test('parseEntriesFromCSV: throws when no header + data rows', () => { assert.throws(() => T.parseEntriesFromCSV('name,password'), /header row and at least one data row/i); }); test('parseEntriesFromCSV: throws when no title/url/username column present', () => { assert.throws(() => T.parseEntriesFromCSV('password,foo\npw,x'), /title\/url or username/i); });