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'; } let code = ''; const ASYNC_FNS = ['cardsAdd', 'cardsImport', 'cardsRemove']; for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit', 'cardsAdd', 'cardsImport', 'cardsRemove', 'ramCardIdxAfterRemove', 'spPresetIdx', 'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead', 'spCntrLocalSync', 'ramSaveCntr', 'cardsApplyFields', 'cardsApply', 'ramApplyCard', 'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent', 'spTarKeyForPack', 'spPresetTar', 'packToSp', 'esc', 'escHtml', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', 'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard', 'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText', 'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove', 'cardsFreeTarsFromForm', 'cardsTarsFromTable', 'cardsTarsFromForm', 'cardsTarsReset', 'cardsShowList', 'cardsShowEditor', 'cardsSetFormMode', 'cardsNew', 'cardsCancelEdit']) { 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 _cardsKeysetCount = 0;\nvar _cardsTarCount = 0;\n' + 'globalThis.spPresetWarningRender = function() {};\n' + 'globalThis.spMslWarningRender = function() {};\n' + 'globalThis.pysimApplyAvailability = function() {};\n' + 'globalThis.navRecord = function() {};\n' + 'globalThis.navReplace = function() {};\n' + code; eval(code); globalThis.t = s => s; // The dialog sweep replaced the native alert/confirm with the helpers: route // them to the native stubs each test installs on globalThis. globalThis.pysimAlert = m => globalThis.alert(m); globalThis.pysimConfirm = async m => globalThis.confirm(m); globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v); const CARD_IDS = ['cards-name','cards-iccid','cards-release','cards-pin1','cards-puk1','cards-pin2','cards-puk2','cards-adm', 'cards-tars','cards-list-view','cards-editor-view','cards-editor-title','cards-new-btn', 'cards-psk-id','cards-psk-key','cards-keysets', 'cards-add-btn','cards-cancel-btn', // the keyset editor rows (two rows is enough for the tests) 'cards-ks-0-kvn','cards-ks-0-kic','cards-ks-0-kid','cards-ks-0-kic-key', 'cards-ks-0-kid-key','cards-ks-0-cntr', 'cards-ks-1-kvn','cards-ks-1-kic','cards-ks-1-kid','cards-ks-1-kic-key', 'cards-ks-1-kid-key','cards-ks-1-cntr', // the TAR table rows: 0-2 are the fixed role rows, 3+ the free rows 'cards-tr-0-tar','cards-tr-0-msl','cards-tr-1-tar','cards-tr-1-msl', 'cards-tr-2-tar','cards-tr-2-msl', 'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc','cards-tr-3-keyset', 'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc','cards-tr-4-keyset']; const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex', 'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex', 'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex']; const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview', 'ber-result','hota-preview']; function ksRow(els, i, ks) { for (const field of ['kic', 'kid', 'kic-key', 'kid-key', 'cntr']) { const id = 'cards-ks-' + i + '-' + field; if (!els[id]) els[id] = fakeEl(); els[id].value = ks[field === 'kic-key' ? 'kicKey' : (field === 'kid-key' ? 'kidKey' : field)] || ''; } } function tarRow(els, i, e) { for (const field of ['tar', 'msl', 'desc', 'keyset']) { const id = 'cards-tr-' + i + '-' + field; if (!els[id]) els[id] = fakeEl(); els[id].value = (e[field] === undefined || e[field] === null) ? '' : String(e[field]); } } function presetFixture(extra) { return Object.assign({ name: 'C', keysets: [{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }], tars: [{ role: 'isd', tar: '000000', msl: '16' }, { role: 'uiccRfm', tar: 'B00000', msl: '16' }, { role: 'usimRfm', tar: 'B00001', msl: '16' }], }, extra || {}); } // A ]*value=""/); assert.match(els['cards-tars'].innerHTML, /ISD compact format/); assert.match(els['cards-tars'].innerHTML, /cards-tr-3-tar"[^>]*value="AF4D01"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-4-tar"[^>]*value="AF4D02"/); assert.match(els['cards-tars'].innerHTML, /cards-tr-4-desc"[^>]*value="two"/); assert.match(els['cards-tars'].innerHTML, /cardsTarRemove\(4\)/); // simulate the DOM the innerHTML produced tarRow(els, 3, { tar: 'AF4D01', msl: '0A', desc: 'one' }); tarRow(els, 4, { tar: 'AF4D02', msl: '0A', desc: 'two' }); cardsTarRemove(3); assert.strictEqual(_cardsTarCount, 1); assert.match(els['cards-tars'].innerHTML, /value="AF4D02"/); assert.doesNotMatch(els['cards-tars'].innerHTML, /value="AF4D01"/); tarRow(els, 3, { tar: 'AF4D02', msl: '0A', desc: 'two' }); cardsTarAdd(); assert.strictEqual(_cardsTarCount, 2); // the new row renders empty tarRow(els, 4, { tar: '', msl: '', desc: '' }); assert.deepStrictEqual(cardsFreeTarsFromForm(), [{ tar: 'AF4D02', msl: '0A', desc: 'two', keyset: null }]); // the role rows are never removable cardsTarRemove(0); assert.strictEqual(_cardsTarCount, 2); // an emptied role field survives a re-render (no silent default refill), // while the save path still fills the default tarRow(els, 0, { tar: '', msl: '' }); cardsTarRemove(3); assert.strictEqual(_cardsTarCount, 0); assert.match(els['cards-tars'].innerHTML, /cards-tr-0-tar"[^>]*value=""/); assert.strictEqual(cardsTarsFromForm()[0].tar, '000000'); assert.strictEqual(cardsTarsFromForm()[0].msl, '16'); }); test('the list view opens the editor for a new preset and Cancel returns', () => { const els = setup(); globalThis.cards = [presetFixture()]; cardsNew(); assert.ok(!els['cards-editor-view'].classList.contains('hidden'), 'the editor is shown'); assert.ok(els['cards-list-view'].classList.contains('hidden'), 'the list is hidden'); assert.strictEqual(globalThis._cardsEditIdx, null); assert.strictEqual(els['cards-editor-title'].textContent, 'Add preset'); assert.strictEqual(els['cards-add-btn'].textContent, 'Add'); assert.strictEqual(_cardsTarCount, 0); cardsCancelEdit(); assert.ok(!els['cards-list-view'].classList.contains('hidden'), 'the list is back'); assert.ok(els['cards-editor-view'].classList.contains('hidden'), 'the editor is hidden'); }); test('editing opens the editor with the preset title', () => { const els = setup(); globalThis.cards = [presetFixture({ name: 'Foo' })]; cardsEdit(0); assert.strictEqual(globalThis._cardsEditIdx, 0); assert.ok(!els['cards-editor-view'].classList.contains('hidden')); assert.strictEqual(els['cards-editor-title'].textContent, 'Edit preset: Foo'); assert.strictEqual(els['cards-add-btn'].textContent, 'Save'); }); test('the editor refuses PIN/PUK codes that are not 4-8 digits', async () => { const els = setup(); els['cards-name'].value = 'X'; ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }); globalThis._cardsKeysetCount = 1; const seen = []; globalThis.alert = msg => seen.push(msg); els['cards-pin1'].value = '123'; await cardsAdd(); assert.strictEqual(apiCalls.length, 0); assert.match(seen[0], /PIN\/PUK/); seen.length = 0; els['cards-pin1'].value = '1234'; els['cards-puk2'].value = '12A4'; await cardsAdd(); assert.strictEqual(apiCalls.length, 0); assert.match(seen[0], /PUK2/); }); test('the editor refuses a counter that is not 1-10 hex digits', async () => { const els = setup(); els['cards-name'].value = 'X'; ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: 'XYZ' }); globalThis._cardsKeysetCount = 1; const seen = []; globalThis.alert = msg => seen.push(msg); await cardsAdd(); assert.strictEqual(apiCalls.length, 0); assert.match(seen[0], /Counter must be 1-10 hex digits/); // a valid value - spaces included, the server strips them - is posted seen.length = 0; ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '00 00 00 00 01' }); await cardsAdd(); assert.strictEqual(apiCalls.length, 1, JSON.stringify(seen)); assert.strictEqual(apiCalls[0].body.keysets[0].cntr, '00 00 00 00 01'); // an empty counter is refused too (the value is required) seen.length = 0; apiCalls.length = 0; els['cards-name'].value = 'X'; globalThis._cardsKeysetCount = 1; ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '' }); await cardsAdd(); assert.strictEqual(apiCalls.length, 0); assert.match(seen[0], /Counter must be 1-10 hex digits/); });