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(/