const { test } = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); function extractFunc(src, name, asyncFn) { const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); const m = re.exec(src); if (!m) throw new Error('function ' + name + ' not found'); let i = m.index + m[0].length - 1; let depth = 0; for (; i < src.length; i++) { if (src[i] === '{') depth++; else if (src[i] === '}') { depth--; if (depth === 0) break; } } return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); } function extractConst(src, name) { const m = new RegExp('const\\s+' + name + '\\s*=\\s*([^;]+);').exec(src); if (!m) throw new Error('const ' + name + ' not found'); return 'var ' + name + ' = ' + m[1] + ';\n'; } const ASYNC_FNS = ['cardsOutfileFile', 'cardsOutfilePreview', 'cardsOutfileImport']; let code = ''; for (const fn of ['cardsOutfileTarsFromDom', 'cardsOutfileTarsFromForm', 'cardsOutfileTarRowHtml', 'cardsOutfileTarRowsHtml', 'cardsOutfileTarAdd', 'cardsOutfileTarRemove', 'cardsOutfileTarRender', 'cardsOutfileKeysetOptions', 'cardsOutfileResultHtml', 'cardsOutfileControls', 'cardsOutfileClose', 'cardsOutfileRender', 'cardsOutfileFile', 'cardsOutfilePreview', 'cardsOutfileImport', 'esc', 'escHtml', 'cardsRoleDefault', 'spKeysetKvnOf']) { code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n'; } code = extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_ROLES') + extractConst(html, 'CARDS_MSL_DEFAULT') + extractConst(html, 'CARDS_TAR_ROLE_DESC') + 'var _cardsOutfile = null;\nvar _cardsOutfileTars = 0;\n' + code; // A minimal DOM: the tests set the elements they need, everything else is // absent (the code guards every getElementById result). const els = {}; const app = new Function('els', ` var sandbox = { calls: [], io: [], alerts: [], fetched: 0, reply: null, replyError: null, confirmResult: true }; var document = { getElementById: id => els[id] || null }; var t = k => k; var alert = m => { sandbox.alerts.push(String(m)); }; var confirm = m => { sandbox.confirmCalls = (sandbox.confirmCalls || []); sandbox.confirmCalls.push(String(m)); return sandbox.confirmResult; }; var pysimAlert = m => alert(m); var pysimConfirm = async m => confirm(m); var pysimPrompt = async () => ''; // not used by the outfile flow var ioStatus = (id, msg) => { sandbox.io.push([id, msg]); }; var pysimFetchOk = async (path, body) => { sandbox.calls.push({ path: path, body: body }); if (sandbox.replyError) throw new Error(sandbox.replyError); return sandbox.reply; }; var cardsFetch = async () => { sandbox.fetched++; }; ${code} sandbox.getState = () => _cardsOutfile; sandbox.setState = v => { _cardsOutfile = v; }; sandbox.getTars = () => _cardsOutfileTars; sandbox.setTars = v => { _cardsOutfileTars = v; }; Object.defineProperty(sandbox, 'state', { get: () => _cardsOutfile, set: v => { _cardsOutfile = v; } }); Object.defineProperty(sandbox, 'tars', { get: () => _cardsOutfileTars, set: v => { _cardsOutfileTars = v; } }); sandbox.cardsOutfileTarsFromDom = cardsOutfileTarsFromDom; sandbox.cardsOutfileTarsFromForm = cardsOutfileTarsFromForm; sandbox.cardsOutfileTarRowHtml = cardsOutfileTarRowHtml; sandbox.cardsOutfileTarRowsHtml = cardsOutfileTarRowsHtml; sandbox.cardsOutfileTarAdd = cardsOutfileTarAdd; sandbox.cardsOutfileTarRemove = cardsOutfileTarRemove; sandbox.cardsOutfileTarRender = cardsOutfileTarRender; sandbox.cardsOutfileKeysetOptions = cardsOutfileKeysetOptions; sandbox.cardsOutfileResultHtml = cardsOutfileResultHtml; sandbox.cardsOutfileControls = cardsOutfileControls; sandbox.cardsOutfileClose = cardsOutfileClose; sandbox.cardsOutfileRender = cardsOutfileRender; sandbox.cardsOutfileFile = cardsOutfileFile; sandbox.cardsOutfilePreview = cardsOutfilePreview; sandbox.cardsOutfileImport = cardsOutfileImport; return sandbox; `)(els); app.state = null; app.tars = 0; function setEl(id, value) { els[id] = { value: value, innerHTML: '', classList: { add() {}, remove() {} } }; } function clearEls() { for (const k of Object.keys(els)) delete els[k]; } const TEXT = 'var_out:IMSI ICCID KIC1 KID1\n250197700000000 8970119000004600098F ' + '00112233445566778899AABBCCDDEEFF FFEEDDCCBBAA99887766554433221100\n'; test('the Cards view wires the outfile button, the input and the panel', () => { assert.match(html, /onclick="cardsOutfilePick\(\)"[^>]*data-l10n="Import outfile"/); assert.match(html, /]*onchange="cardsOutfileFile\(this\)"/); assert.match(html, /
{ const preview = extractFunc(html, 'cardsOutfilePreview', true); const imp = extractFunc(html, 'cardsOutfileImport', true); assert.match(preview, /'\/api\/presets\/import-outfile'/); assert.match(imp, /'\/api\/presets\/import-outfile'/); assert.match(imp, /mode: o\.mode/); // the keysets travel per TAR row (v3.29.1): no global tar_keyset assert.ok(!preview.includes('tar_keyset'), 'the preview must not send a global keyset'); assert.ok(!imp.includes('tar_keyset'), 'the import must not send a global keyset'); assert.match(preview, /tars: cardsOutfileTarsFromForm\(\), start_counter: o\.cntr/); assert.match(imp, /tars: cardsOutfileTarsFromForm\(\), start_counter: o\.cntr, mode: o\.mode/); }); test('the 16-byte algorithm values match the server map', () => { const py = fs.readFileSync( path.join(__dirname, '..', '..', 'pysim_simple_server', 'outfile.py'), 'utf8'); const m = /_KEY_ALGO_16\s*=\s*\{([^}]*)\}/.exec(py); assert.ok(m, '_KEY_ALGO_16 not found in outfile.py'); const server = [...m[1].matchAll(/'([a-z0-9]+)'\s*:/g)].map(x => x[1]).sort(); const ui = [...html.matchAll(/