1473396e5b
A RAM install that fails at a LOAD step is typically the library-version gate: the card verifies the CAP's import list when the Import component completes (JC VM 4.5.2). The failure hint now says so and points at the CAP compatibility test (Set all 0.0, then restore the lines one at a time to find the missing package and version), replacing the old "retry with a smaller LOAD block" advice, which is a red herring for that gate. The failure popup gains a one-click "Run the CAP compatibility test" button: it closes the popup, switches the RAM operation selector to compat and runs it with the same .cap (the file input still holds it and the probe list is prefilled with its imports), so the diagnostic is one click away. An INSTALL [for load] refusal now gets its own advice instead of the LOAD hint: the card refused to open the load file - usually a duplicate load file from an earlier run (delete the package first, Explore -> Delete), or rejected load parameters. Tests: the hint's wording and branches (LOAD vs INSTALL [for load]), the button handler's op switch. 862 frontend + 842 Python; sw cache simple-v422.
1772 lines
90 KiB
JavaScript
1772 lines
90 KiB
JavaScript
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) {
|
|
const re = new RegExp('(?:async\\s+)?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 src.slice(m.index, i + 1);
|
|
}
|
|
|
|
// Extract chain builder functions and dependencies
|
|
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
|
|
'ramFormatIsExpanded', 'ramFmtLadder',
|
|
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
|
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
|
|
'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'ramElfAppMatch',
|
|
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
|
|
'ramCntrLowBox', 'ramCntrLowClear',
|
|
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
|
|
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
|
|
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
|
|
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
|
|
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
|
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
|
let code = '';
|
|
for (const f of FNS) {
|
|
code += extractFunc(html, f) + '\n';
|
|
}
|
|
const m = html.match(/const _chains = \{\};/);
|
|
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
|
|
const swm = html.match(/const SW_MAP = \{[\s\S]*?\n\};/);
|
|
if (swm) code += swm[0].replace(/^const /, 'var ') + '\n';
|
|
const ef = html.match(/const RAM_EXPANDED_FORMS = \[[^\]]*\];/);
|
|
if (ef) code += ef[0].replace(/^const /, 'var ') + '\n';
|
|
const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
|
|
if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
|
|
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
|
|
if (an) code += an[0].replace(/^const /, 'var ') + '\n';
|
|
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
|
|
if (ar) code += ar[0].replace(/^const /, 'var ') + '\n';
|
|
const et = html.match(/const ETSI_TOOLKIT_RELEASE = \{[\s\S]*?\n\};/);
|
|
if (et) code += et[0].replace(/^const /, 'var ') + '\n';
|
|
const ua = html.match(/const UICC_TOOLKIT_AID = '[^']*';/);
|
|
if (ua) code += ua[0].replace(/^const /, 'var ') + '\n';
|
|
globalThis.spPresetWarningRender = () => {};
|
|
globalThis.pysimApplyAvailability = () => {};
|
|
eval(code);
|
|
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
|
|
eval(code);
|
|
|
|
const els = {};
|
|
const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } };
|
|
global.document = doc;
|
|
|
|
// ramApplyCard/ramExecute delegate to the SP-form helpers; their real
|
|
// behaviour (and the preset re-read before an operation) is covered by
|
|
// cards_counter.test.js, so keep them as global stubs here.
|
|
globalThis.cardsApply = () => {};
|
|
globalThis.spRefreshFromPreset = () => '';
|
|
|
|
function reset() { for (const id of Object.keys(els)) delete els[id]; }
|
|
|
|
function genRamResult(fields) {
|
|
const row = { cmd: fields.cmd, fields: fields };
|
|
return chainRamBuildRowHex(0, row);
|
|
}
|
|
|
|
function genRamApdu() {
|
|
return genRamResult({
|
|
cmd: 'install-install',
|
|
aid: 'A000000151000000',
|
|
elfAid: '',
|
|
modAid: '',
|
|
priv: '00',
|
|
tkEnabled: true,
|
|
tkMode: 'ea',
|
|
tkPriority: '0',
|
|
tkTimers: '0',
|
|
tkTextlen: '0',
|
|
tkMenus: '2',
|
|
tkFirstpos: '1',
|
|
tkFirstid: '01',
|
|
tkLastpos: '2',
|
|
tkLastid: '02',
|
|
tkChannels: '0',
|
|
tkMsl: '16',
|
|
tkTar: 'AF4D01',
|
|
tkAd: '',
|
|
tkServices: '0',
|
|
});
|
|
}
|
|
|
|
test('UICC toolkit nested inside EA (m=2, services 0)', () => {
|
|
const apdu = genRamApdu();
|
|
assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0100'), apdu);
|
|
});
|
|
|
|
test('UICC m=1 emits single pair', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '1', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '0', tkLastid: '00',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EA11800F0000000101010002011603AF4D0100'), apdu);
|
|
});
|
|
|
|
test('UICC m=3 fills middle pair with 0000', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EA158013000000030101000003030002011603AF4D0100'), apdu);
|
|
});
|
|
|
|
test('UICC services 7 appended as final byte', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '7',
|
|
});
|
|
assert.ok(apdu.includes('EA13801100000002010102020002011603AF4D0107'), apdu);
|
|
});
|
|
|
|
test('SIM (CA) access domain FIRST, no services byte', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '5A', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603AF4D01'), apdu);
|
|
});
|
|
|
|
test('SIM (CA) blank access domain emits length byte 00', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '2', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '2', tkLastid: '02',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EF13CA110000000002010102020002011603AF4D01'), apdu);
|
|
});
|
|
|
|
test('SIM (CA) m=3, no TAR, blank access domain', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ca', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '3', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '3', tkLastid: '03',
|
|
tkChannels: '0', tkMsl: '16', tkTar: '', tkAd: '', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu);
|
|
});
|
|
|
|
test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => {
|
|
const apdu = genRamResult({
|
|
cmd: 'install-install', aid: 'A000000151000000', priv: '00',
|
|
tkEnabled: true, tkMode: 'ea', tkPriority: '0', tkTimers: '0', tkTextlen: '0',
|
|
tkMenus: '60', tkFirstpos: '1', tkFirstid: '01', tkLastpos: '60', tkLastid: '3C',
|
|
tkChannels: '0', tkMsl: '16', tkTar: 'AF4D01', tkAd: '', tkServices: '0',
|
|
});
|
|
assert.ok(apdu.includes('EA8188808185'), apdu);
|
|
});
|
|
|
|
test('LOAD P1 fixed to 80 (last block)', () => {
|
|
const apdu = genRamResult({ cmd: 'load', data: 'AABBCC', block: '0' });
|
|
assert.ok(apdu.startsWith('80E88000'), apdu);
|
|
});
|
|
|
|
test('DELETE: P1=00, mode in P2', () => {
|
|
let apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '00' });
|
|
assert.ok(apdu.startsWith('80E40000'), apdu);
|
|
|
|
apdu = genRamResult({ cmd: 'delete', aid: 'AA1902BC225501', delMode: '80' });
|
|
assert.ok(apdu.startsWith('80E40080'), apdu);
|
|
});
|
|
|
|
test('STORE DATA ram-enc P1 values 00/40/80/C0/E0', () => {
|
|
for (const [enc, p1] of [['00','00'],['40','40'],['80','80'],['C0','C0'],['E0','E0']]) {
|
|
const apdu = genRamResult({ cmd: 'store-data', data: 'AABB', enc: enc, block: '0' });
|
|
assert.ok(apdu.startsWith('80E2' + p1 + '00'), enc + ' -> P1 ' + p1);
|
|
}
|
|
});
|
|
|
|
test('ramRenderExploreHtml puts the command format after the resource lines', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml({ appCount: 5, freeNV: 100, freeV: 50 }, [], [], [], 'compact');
|
|
delete global.t;
|
|
const iNv = out.indexOf('Free NV:');
|
|
const iV = out.indexOf('Free Volatile:');
|
|
const iFmt = out.indexOf('RAM command format:');
|
|
assert.ok(iNv >= 0 && iV >= 0 && iFmt > iV, out);
|
|
});
|
|
|
|
test('explorer action buttons share a fixed-width column, Install is green', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml(
|
|
{ appCount: 1, freeNV: 10, freeV: 5 },
|
|
[],
|
|
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
|
|
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'] }]
|
|
);
|
|
delete global.t;
|
|
// every action cell is the same fixed-width column
|
|
assert.ok(out.includes('w-28 shrink-0'), out);
|
|
// rows are visible chips (background + hover) so the button belongs to its row
|
|
assert.ok(out.includes('bg-gray-50 dark:bg-slate-800/60'), out);
|
|
assert.ok(out.includes('hover:bg-gray-100 dark:hover:bg-slate-700/50'), out);
|
|
// the Install button (module without an instance) is green, not blue
|
|
const inst = /<button onclick="ramInstallFromExplorer[^>]*class="([^"]*)"/.exec(out);
|
|
assert.ok(inst, out);
|
|
assert.ok(inst[1].includes('bg-emerald-100'), inst[1]);
|
|
assert.ok(!inst[1].includes('bg-blue-'), inst[1]);
|
|
// the delete buttons keep the danger tone
|
|
const del = /<button onclick="ramDeleteFromExplorer[^>]*class="([^"]*)"/.exec(out);
|
|
assert.ok(del && del[1].includes('bg-red-100'), del && del[1]);
|
|
});
|
|
|
|
test('ramRenderExploreHtml localizes every label and button', () => {
|
|
const seen = [];
|
|
global.t = s => { seen.push(s); return 'XX' + s; };
|
|
const out = ramRenderExploreHtml(
|
|
{ appCount: 5, freeNV: 100, freeV: 50 },
|
|
[{ aid: 'A000000151000000', lifecycle: '07', privileges: '', sdAid: 'A000000151000000' }],
|
|
[{ aid: 'A1130001180001', lifecycle: '07', privileges: '80', implicitSel: '00', elfAid: 'ELF1',
|
|
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
|
|
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'], sdAid: null }]
|
|
);
|
|
delete global.t;
|
|
assert.ok(out.includes('XXDelete'), out);
|
|
assert.ok(out.includes('XXDelete All'), out);
|
|
assert.ok(out.includes('XXApplications:'), out);
|
|
assert.ok(out.includes('XXFree NV:'), out);
|
|
assert.ok(out.includes('XXFree Volatile:'), out);
|
|
assert.ok(out.includes('XXAID:'), out);
|
|
assert.ok(out.includes('XXLifecycle:'), out);
|
|
assert.ok(out.includes('XXPrivileges:'), out);
|
|
assert.ok(out.includes('XXSD AID:'), out);
|
|
assert.ok(out.includes('XXImplicit sel:'), out);
|
|
assert.ok(out.includes('XXVersion:'), out);
|
|
assert.ok(seen.includes('Application / Instance AID:'));
|
|
assert.ok(seen.includes('Load File AID / Package AID:'));
|
|
assert.ok(seen.includes('Executable Module AIDs / Applet Class AIDs:'));
|
|
assert.ok(seen.includes('STK menu entry:'));
|
|
assert.ok(seen.includes('position'));
|
|
assert.ok(seen.includes('Other registry data:'));
|
|
assert.ok(!out.includes('data-l10n'), out);
|
|
// the well-known ISD AID is annotated (GP Card Spec v2.3.1 H.1.3)
|
|
assert.ok(out.includes('A000000151000000 <span class="text-gray-400 dark:text-slate-500">(GlobalPlatform Issuer Security Domain)</span>'), out);
|
|
});
|
|
|
|
test('ramRenderExploreHtml annotates standard package AIDs, vendor AIDs stay bare', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml(null, [], [], [
|
|
{ aid: 'A0000000871005FFFFFFFF8913200000', lifecycle: '01', version: '1.0',
|
|
moduleAids: ['A000000062010101'], sdAid: 'A0000001515350' },
|
|
]);
|
|
delete global.t;
|
|
assert.ok(out.includes('(uicc.usim.toolkit)'), out);
|
|
assert.ok(out.includes('(javacard.framework.service)'), out);
|
|
assert.ok(out.includes('(GlobalPlatform RID)'), out);
|
|
});
|
|
|
|
test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () => {
|
|
global.t = s => s;
|
|
// GP Table 11-7: 0x9E = Security Domain + Card Lock + Card Terminate +
|
|
// Card Reset + CVM Management (the live ISD privilege byte)
|
|
assert.strictEqual(ramFmtPrivileges('9E'),
|
|
'Security Domain, Card Lock, Card Terminate, Card Reset, CVM Management');
|
|
assert.strictEqual(ramFmtPrivileges('9A'),
|
|
'Security Domain, Card Lock, Card Terminate, CVM Management');
|
|
// DAP Verification (b8+b7) vs Mandated DAP Verification (b8+b7+b1)
|
|
assert.strictEqual(ramFmtPrivileges('C0'), 'Security Domain, DAP Verification');
|
|
assert.strictEqual(ramFmtPrivileges('C1'), 'Security Domain, Mandated DAP Verification');
|
|
// byte 2 / byte 3 spot checks
|
|
assert.strictEqual(ramFmtPrivileges('0001'), 'Global Service');
|
|
assert.strictEqual(ramFmtPrivileges('000080'), 'Receipt Generation');
|
|
delete global.t;
|
|
});
|
|
|
|
test('ramOpProgressText renders the step headline', () => {
|
|
global.t = s => s;
|
|
assert.strictEqual(
|
|
ramOpProgressText({ step: 3, total: 11, name: 'LOAD (2/9)', elapsed: 4.2 }),
|
|
'Step 3/11 \u2014 LOAD (2/9) \u2014 4.2 s');
|
|
assert.strictEqual(ramOpProgressText({ step: 0, total: 0, name: '' }), 'Working...');
|
|
assert.strictEqual(ramOpProgressText(null), '');
|
|
delete global.t;
|
|
});
|
|
|
|
test('ramCapToolkitMode picks CA/EA from the CAP linked libraries', () => {
|
|
// sim.toolkit / sim.access (A0000000090003...) -> SIM Toolkit (CA field)
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710002' }]), 'ca');
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710001' }]), 'ca');
|
|
// uicc.toolkit / uicc.access (A0000000090005...) -> UICC Toolkit (EA field)
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8912000000' }]), 'ea');
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8911000000' }]), 'ea');
|
|
// uicc.usim.* (A0000000871005...) -> UA family wins over a SIM library
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
|
|
assert.strictEqual(ramCapToolkitMode([
|
|
{ aid: 'A0000000090003FFFFFFFF8910710002' },
|
|
{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
|
|
// no toolkit/access library: leave the user's choice alone
|
|
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000620101' }, { aid: 'A0000000620001' }]), null);
|
|
assert.strictEqual(ramCapToolkitMode([]), null);
|
|
assert.strictEqual(ramCapToolkitMode(undefined), null);
|
|
});
|
|
|
|
test('the toolkit block is an accented bordered fieldset', () => {
|
|
const m = /id="rc-toolkit-row"[^>]*class="([^"]*)"/.exec(html);
|
|
assert.ok(m, 'rc-toolkit-row not found');
|
|
assert.ok(m[1].includes('border'), m[1]);
|
|
assert.ok(m[1].includes('p-3'), m[1]);
|
|
assert.ok(/<legend[^>]*>[\s\S]*?rc-toolkit-enable[\s\S]*?<\/legend>/.test(html),
|
|
'the enable checkbox lives in the fieldset legend');
|
|
});
|
|
|
|
test('ramFmtPrivileges uses the translated (none) placeholder', () => {
|
|
global.t = s => 'XX' + s;
|
|
assert.strictEqual(ramFmtPrivileges(''), 'XX(none)');
|
|
assert.strictEqual(ramFmtPrivileges('00'), 'XX(none)');
|
|
delete global.t;
|
|
});
|
|
|
|
function fakeClassList() {
|
|
const set = new Set();
|
|
return {
|
|
add: (...cs) => cs.forEach(c => set.add(c)),
|
|
remove: (...cs) => cs.forEach(c => set.delete(c)),
|
|
contains: c => set.has(c),
|
|
toggle: (c, on) => { if (on === undefined ? !set.has(c) : on) set.add(c); else set.delete(c); },
|
|
};
|
|
}
|
|
|
|
function fakeEl(id) {
|
|
return {
|
|
id,
|
|
value: '',
|
|
innerHTML: '',
|
|
textContent: '',
|
|
classList: fakeClassList(),
|
|
options: [],
|
|
appendChild(opt) { this.options.push(opt); },
|
|
};
|
|
}
|
|
|
|
function fakeRamDocument(ids) {
|
|
const els = {};
|
|
for (const id of ids) els[id] = fakeEl(id);
|
|
const sel = els['ram-card-sel'];
|
|
if (sel) {
|
|
Object.defineProperty(sel, 'innerHTML', {
|
|
get() { return this._html || ''; },
|
|
set(v) { this._html = v; this.value = ''; },
|
|
});
|
|
}
|
|
globalThis.document = {
|
|
getElementById: id => els[id] || null,
|
|
createElement: () => fakeEl('option'),
|
|
};
|
|
return els;
|
|
}
|
|
|
|
test('ramGetStatusApdu builds the compact chain, never the expanded form', () => {
|
|
// GP 11.4.2.2: compact listings (P2.b2=0) carry the chained GET RESPONSE
|
|
assert.strictEqual(ramGetStatusApdu('80', '00'), '80F28000024F0000C0000000');
|
|
assert.strictEqual(ramGetStatusApdu('20', '01'), '80F22001024F0000C0000000');
|
|
// the expanded command format must not carry a GET RESPONSE (TS 102 226
|
|
// 5.2.1.1): the reference TCA loader sends the same query wrapped unchained
|
|
// (`AA0A2208 80F24000024F0000`), Le='00' returns the listing directly
|
|
assert.strictEqual(ramGetStatusApdu('40', '00', 'expanded'), '80F24000024F0000');
|
|
assert.strictEqual(ramGetStatusApdu('40', '01', 'expanded-ae'), '80F24001024F0000');
|
|
assert.ok(!ramGetStatusApdu('40', '00', 'expanded').includes('C0000000'),
|
|
'no GET RESPONSE in the expanded format');
|
|
// the expanded TLV form (P2=02/03) takes no GET RESPONSE and is not built:
|
|
// its GET STATUS bytes would be `80F2<P1>02 04 4F00 5C.. 00`
|
|
assert.ok(!ramGetStatusApdu('20', '01').startsWith('80F2200204'),
|
|
'expanded form must not be generated by this builder');
|
|
assert.ok(!html.includes("for (const p2Init of ['02', '00'])"),
|
|
'the P2=02 attempt must be gone');
|
|
// the ISD-only query is a single occurrence: no next-occurrence paging
|
|
assert.ok(/if \(p1 === '80'\) break;/.test(html), 'ISD next-occurrence guard missing');
|
|
});
|
|
|
|
test('ramStepLine shows the PoR verdict and the remote status word', () => {
|
|
globalThis.t = s => s;
|
|
globalThis.lookupSw = (a, b) => (a + b === '6700' ? 'Wrong length in Lc' : '');
|
|
// successful steps are compact: verdict symbol, remote SW (no name),
|
|
// size; the SMS count only for concatenated packets
|
|
const okLine = ramStepLine({ name: 'INSTALL [for load]', por_status: 'por_ok',
|
|
por_sw: '9000', sw: '9000', bytes: 50, segments: 1 }, 0);
|
|
assert.strictEqual(okLine, '\u2705 Step 1: INSTALL [for load] \u2014 9000 \u00b7 50 B');
|
|
const multi = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok',
|
|
por_sw: '6101', bytes: 274, segments: 3 }, 1);
|
|
assert.strictEqual(multi, '\u2705 Step 2: LOAD (1/64) \u2014 6101 \u00b7 274 B / 3 SMS');
|
|
const badLine = ramStepLine({ name: 'LOAD (1/9)', por_status: 'por_ok', por_sw: '6700',
|
|
por_error: 'remote SW 6700', sw: '9000', bytes: 274, segments: 3 }, 1);
|
|
assert.ok(badLine.startsWith('\u274c'), badLine);
|
|
assert.ok(badLine.includes('remote SW 6700 (Wrong length in Lc)'), badLine);
|
|
const porLine = ramStepLine({ name: 'LOAD', por_status: 'por_error_cntr_low' }, 2);
|
|
assert.ok(porLine.includes('PoR error cntr_low'), porLine);
|
|
const noPor = ramStepLine({ name: 'LOAD', por_status: 'no_por' }, 3);
|
|
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
|
|
});
|
|
|
|
test('ramStepComponents maps a LOAD step to the CAP components it covers', () => {
|
|
// RemMobileID-like layout: sizes include each component's 3-byte header
|
|
const comps = [
|
|
{ name: 'Header', size: 20 }, { name: 'Directory', size: 34 },
|
|
{ name: 'Applet', size: 15 }, { name: 'Import', size: 101 },
|
|
{ name: 'ConstantPool', size: 1121 }, { name: 'Method', size: 8506 },
|
|
];
|
|
// 240 B block 1 (0..239) completes the Import component (its end is 170)
|
|
let sc = ramStepComponents('LOAD (1/64)', comps, 240);
|
|
assert.deepStrictEqual(sc.completed, ['Header', 'Directory', 'Applet', 'Import']);
|
|
assert.deepStrictEqual(sc.names.slice(0, 2), ['Header', 'Directory']);
|
|
// 64 B blocks: 1 completes Header/Directory, 3 completes Import
|
|
assert.deepStrictEqual(ramStepComponents('LOAD (1/240)', comps, 64).completed, ['Header', 'Directory']);
|
|
sc = ramStepComponents('LOAD (2/240)', comps, 64);
|
|
assert.deepStrictEqual(sc.names, ['Applet', 'Import']);
|
|
assert.deepStrictEqual(sc.completed, ['Applet']);
|
|
sc = ramStepComponents('LOAD (3/240)', comps, 64);
|
|
assert.deepStrictEqual(sc.names, ['Import', 'ConstantPool']);
|
|
assert.deepStrictEqual(sc.completed, ['Import']);
|
|
// other step names and missing data produce nothing
|
|
assert.strictEqual(ramStepComponents('INSTALL [for load]', comps, 64), null);
|
|
assert.strictEqual(ramStepComponents('LOAD (1/64)', comps, 0), null);
|
|
assert.strictEqual(ramStepComponents('LOAD (1/64)', null, 64), null);
|
|
});
|
|
|
|
test('ramStepLine annotates a LOAD step with the completing component', () => {
|
|
globalThis.t = s => s;
|
|
globalThis.lookupSw = () => '';
|
|
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 },
|
|
{ name: 'Method', size: 8506 }];
|
|
const line = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0, comps, 240);
|
|
assert.ok(line.includes('completes Import'), line);
|
|
// block 2 of 40 B blocks (40..79) lies strictly inside Import
|
|
const inside = ramStepLine({ name: 'LOAD (2/64)', por_status: 'por_ok', por_sw: '9000' }, 1, comps, 40);
|
|
assert.ok(inside.includes('inside Import'), inside);
|
|
// without a component list the line is unchanged
|
|
const plain = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok', por_sw: '9000' }, 0);
|
|
assert.ok(!plain.includes('completes') && !plain.includes('inside'), plain);
|
|
});
|
|
|
|
test('ramInstallFailHint names the CAP import requirement for a rejected LOAD', () => {
|
|
globalThis.t = s => s;
|
|
const failedLoad = {
|
|
success: false, failed_step: 3,
|
|
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (1/240)' }, { name: 'LOAD (2/240)', por_sw: '6985' }],
|
|
};
|
|
const hint = ramInstallFailHint(failedLoad, { requires_java_card: '2.2.2' });
|
|
// v3.23.9: a rejected LOAD points at the compatibility test (the typical
|
|
// library-version gate) - the old "smaller LOAD block" advice is gone
|
|
assert.ok(hint.includes('CAP compatibility test'), hint);
|
|
assert.ok(hint.includes('(Java Card \u2265 2.2.2)'), hint);
|
|
assert.ok(!hint.includes('smaller LOAD block'), hint);
|
|
// without a CAP analysis (INSTALL [for install] ops) the level is omitted
|
|
const noMem = ramInstallFailHint(failedLoad, null);
|
|
assert.ok(noMem.includes('CAP compatibility test'), noMem);
|
|
assert.ok(!noMem.includes('Java Card'), noMem);
|
|
// an INSTALL [for load] refusal gets its own advice (a duplicate load
|
|
// file from an earlier run is the usual cause), not the LOAD hint
|
|
const installFail = ramInstallFailHint(
|
|
{ success: false, failed_step: 1,
|
|
steps: [{ name: 'INSTALL [for load]', por_sw: '6985' }] }, null);
|
|
assert.ok(installFail.includes('refused to open the load file'), installFail);
|
|
assert.ok(installFail.includes('delete it first'), installFail);
|
|
assert.ok(!installFail.includes('compatibility test'), installFail);
|
|
// only a load-related step gets the hint
|
|
assert.strictEqual(ramInstallFailHint(
|
|
{ success: false, failed_step: 2, steps: [{ name: 'INSTALL [for load]' }, { name: 'INSTALL [for install]' }] }, null), '');
|
|
assert.strictEqual(ramInstallFailHint({ success: true, failed_step: 1, steps: [] }, null), '');
|
|
// with the CAP analysis the completing component of the failing block is
|
|
// named (the card check lands on the component boundary)
|
|
const comps = [{ name: 'Header', size: 20 }, { name: 'Import', size: 101 }];
|
|
const withBoundary = ramInstallFailHint(
|
|
{ success: false, failed_step: 2, load_block_size: 64,
|
|
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (2/240)', por_sw: '6438' }] },
|
|
{ requires_java_card: '2.2.2', components: comps });
|
|
assert.ok(withBoundary.startsWith('the failed step completes Import. '), withBoundary);
|
|
});
|
|
|
|
test('the failed install offers the one-click compatibility test', () => {
|
|
// the modal carries the button for a failed LOAD; the handler switches the
|
|
// RAM operation to the compatibility test and runs it (the CAP file input
|
|
// still holds the same file)
|
|
const install = extractFunc(html, 'ramShowInstallResult');
|
|
assert.ok(install.includes('ramRunCompatTest()'), install.slice(0, 400));
|
|
assert.ok(install.includes("t('Run the CAP compatibility test')"), install);
|
|
const run = extractFunc(html, 'ramRunCompatTest');
|
|
assert.ok(run.includes("sel.value = 'compat'"), run);
|
|
assert.ok(run.includes('ramOpChanged()'), run);
|
|
assert.ok(run.includes('ramExecute()'), run);
|
|
});
|
|
|
|
test('ramOpChanged clears the executed status only on a real op change', () => {
|
|
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
|
|
_ramOpLast = null;
|
|
els['ram-op'].value = 'explore';
|
|
ramOpChanged();
|
|
assert.ok(!els['ram-result'].classList.contains('hidden'));
|
|
els['ram-result'].classList.remove('hidden');
|
|
els['ram-steps'].classList.remove('hidden');
|
|
ramOpChanged();
|
|
assert.ok(!els['ram-result'].classList.contains('hidden'), 'same op must keep the result');
|
|
els['ram-op'].value = 'install-cap';
|
|
ramOpChanged();
|
|
assert.ok(els['ram-result'].classList.contains('hidden'));
|
|
assert.ok(els['ram-steps'].classList.contains('hidden'));
|
|
assert.ok(els['ram-explorer'].classList.contains('hidden'));
|
|
assert.ok(els['ram-progress'].classList.contains('hidden'));
|
|
assert.ok(!els['ram-install-params'].classList.contains('hidden'));
|
|
});
|
|
|
|
test('ramRender keeps the selected card preset across rebuilds', () => {
|
|
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
|
|
globalThis.cards = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
|
|
_ramCardIdx = null;
|
|
_ramOpLast = 'explore';
|
|
els['ram-op'].value = 'explore';
|
|
ramRender();
|
|
assert.strictEqual(els['ram-card-sel'].value, '');
|
|
els['ram-card-sel'].value = '1';
|
|
ramRender();
|
|
assert.strictEqual(els['ram-card-sel'].value, '1');
|
|
els['ram-card-sel'].value = '';
|
|
_ramCardIdx = 2;
|
|
ramRender();
|
|
assert.strictEqual(els['ram-card-sel'].value, '2');
|
|
globalThis.cards = [{ name: 'A' }];
|
|
_ramCardIdx = 2;
|
|
ramRender();
|
|
assert.strictEqual(els['ram-card-sel'].value, '');
|
|
delete globalThis.cards;
|
|
});
|
|
|
|
test('ramApplyCard remembers a valid picked preset', () => {
|
|
globalThis.cards = [{ name: 'A' }, { name: 'B' }];
|
|
let applied = null;
|
|
globalThis.cardsApply = i => { applied = i; };
|
|
_ramCardIdx = null;
|
|
ramApplyCard('1');
|
|
assert.strictEqual(_ramCardIdx, 1);
|
|
assert.strictEqual(applied, '1');
|
|
ramApplyCard('');
|
|
assert.strictEqual(_ramCardIdx, 1, 'invalid pick must not forget the preset');
|
|
delete globalThis.cards;
|
|
globalThis.cardsApply = () => {};
|
|
delete globalThis.cards;
|
|
});
|
|
|
|
test('ramExecute commits the dropdown selection before running', async () => {
|
|
const els = fakeRamDocument(['ram-card-sel', 'ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
|
|
globalThis.cards = [{ name: 'A' }];
|
|
globalThis.getRamSpParams = () => ({ kicKey: '11', kidKey: '22' });
|
|
let explored = false;
|
|
globalThis.ramExplore = async () => { explored = true; };
|
|
globalThis.alert = () => {};
|
|
_ramCardIdx = null;
|
|
els['ram-card-sel'].value = '0';
|
|
els['ram-op'].value = 'explore';
|
|
await ramExecute();
|
|
assert.strictEqual(_ramCardIdx, 0);
|
|
assert.ok(explored);
|
|
delete globalThis.cards;
|
|
delete globalThis.getRamSpParams;
|
|
delete globalThis.ramExplore;
|
|
delete globalThis.alert;
|
|
});
|
|
|
|
test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
|
|
assert.strictEqual(ramCardIdxAfterRemove(2, 0), 1);
|
|
assert.strictEqual(ramCardIdxAfterRemove(0, 0), null);
|
|
assert.strictEqual(ramCardIdxAfterRemove(0, 2), 0);
|
|
assert.strictEqual(ramCardIdxAfterRemove(null, 1), null);
|
|
});
|
|
|
|
test('ramExpandedQueryApdu builds the expanded query forms', () => {
|
|
// the reference trace form (richest: the tag list incl. the TS 102 226 EA)
|
|
for (const p1 of ['80', '40']) {
|
|
assert.strictEqual(ramExpandedQueryApdu(p1),
|
|
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
|
|
assert.strictEqual(ramExpandedQueryApdu(p1, 'tags'), ramExpandedQueryApdu(p1));
|
|
}
|
|
// the ELF queries add '84' (Executable Module AIDs)
|
|
for (const p1 of ['20', '10']) {
|
|
assert.strictEqual(ramExpandedQueryApdu(p1),
|
|
'80F2' + p1 + '020D4F005C094F9F70C5C4CCCEEA8400');
|
|
}
|
|
// without EA: a card that errors on the requested-but-absent EA object
|
|
assert.strictEqual(ramExpandedQueryApdu('40', 'noea'), '80F240020B4F005C074F9F70C5C4CCCE00');
|
|
assert.strictEqual(ramExpandedQueryApdu('10', 'noea'), '80F210020C4F005C084F9F70C5C4CCCE8400');
|
|
// lean: only the objects valid for the kind - the load file version and
|
|
// modules for ELFs (C5 privileges / C4 ELF AID are application objects),
|
|
// pySim's field list for the ISD/apps
|
|
assert.strictEqual(ramExpandedQueryApdu('10', 'lean'), '80F21002094F005C054F9F70CE8400');
|
|
assert.strictEqual(ramExpandedQueryApdu('20', 'lean'), '80F22002094F005C054F9F70CE8400');
|
|
assert.strictEqual(ramExpandedQueryApdu('40', 'lean'), '80F24002094F005C054F9F70C5CC00');
|
|
assert.strictEqual(ramExpandedQueryApdu('80', 'lean'), '80F28002094F005C054F9F70C5CC00');
|
|
// no tag list at all (GPPro's GET STATUS data field: 4F00 only)
|
|
assert.strictEqual(ramExpandedQueryApdu('80', 'notags'), '80F28002024F0000');
|
|
assert.strictEqual(ramExpandedQueryApdu('10', 'notags'), '80F21002024F0000');
|
|
// the next-occurrence page (P2.b1=1) keeps the form and its tag list
|
|
assert.strictEqual(ramExpandedQueryApdu('40', 'tags', true), '80F240030C4F005C084F9F70C5C4CCCEEA00');
|
|
assert.strictEqual(ramExpandedQueryApdu('10', 'lean', true), '80F21003094F005C054F9F70CE8400');
|
|
assert.strictEqual(ramExpandedQueryApdu('10', 'notags', true), '80F21003024F0000');
|
|
assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'lean', 'notags']);
|
|
// an AID search qualifier (4F <len> <AID>) selects one entity: used to
|
|
// fetch a single applet's entry when the listing cannot be paged
|
|
assert.strictEqual(ramExpandedQueryApdu('40', 'tags', false, 'A1130001180001FFFFFFFF89A1003908'),
|
|
'80F240021C4F10A1130001180001FFFFFFFF89A10039085C084F9F70C5C4CCCEEA00');
|
|
assert.strictEqual(ramExpandedQueryApdu('40', 'notags', false, 'A1130001180001'),
|
|
'80F24002094F07A113000118000100');
|
|
});
|
|
|
|
test('ramChainGetResponse chains the GET RESPONSE into the query string', () => {
|
|
// the live card rejects a standalone GET RESPONSE with 6700; the compact
|
|
// command string (query + C0000000) is how the compact path fetches too
|
|
assert.strictEqual(ramChainGetResponse('80F280020C4F005C084F9F70C5C4CCCEEA00'),
|
|
'80F280020C4F005C084F9F70C5C4CCCEEA00C0000000');
|
|
assert.strictEqual(ramChainGetResponse('80F21002094F005C054F9F70CE8400'),
|
|
'80F21002094F005C054F9F70CE8400C0000000');
|
|
});
|
|
|
|
test('ramDeriveElfVersions reads a package version from its applet instance', () => {
|
|
// the app entry carries CE (the load file version) and C4 (the package
|
|
// AID), so an ELF whose own listing refused every tag list still gets one
|
|
const elfs = [
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' },
|
|
{ aid: 'F0414C46416001', lifecycle: '01', privileges: '00', version: '0100' },
|
|
{ aid: 'DEADBEEF', lifecycle: '01', privileges: '00' },
|
|
];
|
|
const apps = [
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003900' },
|
|
{ aid: 'A0000005591010FFFFFFFF8900001200', version: '0001', elfAid: 'A0000005591010FFFFFFFF8900000E00' },
|
|
];
|
|
ramDeriveElfVersions(elfs, apps);
|
|
assert.strictEqual(elfs[0].version, '0903');
|
|
assert.strictEqual(elfs[0].versionFrom, 'app');
|
|
// an own version is never overwritten, and an unmatched package stays bare
|
|
assert.strictEqual(elfs[1].version, '0100');
|
|
assert.strictEqual(elfs[1].versionFrom, undefined);
|
|
assert.strictEqual(elfs[2].version, undefined);
|
|
});
|
|
|
|
test('ramDeriveElfVersions falls back to the package module AIDs', () => {
|
|
// live 2026-10-02 (test-eSIM): the card returns C4 = the applet's own AID,
|
|
// so the C4 rule matches nothing - the package's module AIDs (the applet
|
|
// instance AIDs) are the mapping that works there
|
|
const elfs = [
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] },
|
|
{ aid: 'A1130001180002FFF7100E8904000200', moduleAids: ['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508'] },
|
|
{ aid: 'F0414C46416001', moduleAids: ['F0414C4641600101'] },
|
|
{ aid: 'A00000006203010101', moduleAids: [] },
|
|
];
|
|
const apps = [
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003908' },
|
|
{ aid: 'A1130001180002FFF7100E8904000208', version: '0101', elfAid: 'A1130001180002FFF7100E8904000208' },
|
|
{ aid: 'A1130001180002FFF7100E89494D4508', version: '0101', elfAid: 'A1130001180002FFF7100E89494D4508' },
|
|
{ aid: 'F0414C4641600101', version: '0001', elfAid: 'F0414C4641600101' },
|
|
];
|
|
ramDeriveElfVersions(elfs, apps);
|
|
assert.strictEqual(elfs[0].version, '0903');
|
|
assert.strictEqual(elfs[0].versionFrom, 'app');
|
|
assert.strictEqual(elfs[1].version, '0101');
|
|
assert.strictEqual(elfs[2].version, '0001');
|
|
// no applet instance: nothing to derive from
|
|
assert.strictEqual(elfs[3].version, undefined);
|
|
});
|
|
|
|
test('ramElfVersionHint names a package with no applet instance', () => {
|
|
globalThis.t = s => s;
|
|
const elfs = [{ aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }];
|
|
const apps = [{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903' }];
|
|
// a versionless package with a matching applet instance needs no clause
|
|
const ok = ramElfVersionHint('notags', elfs, apps);
|
|
// ...but a package with no applet instance is called out
|
|
const orphan = ramElfVersionHint('notags', [{ aid: 'A00000006203010101', moduleAids: [] }], apps);
|
|
// the base hints stay form-aware
|
|
const base = ramElfVersionHint('tags', [], []);
|
|
delete globalThis.t;
|
|
assert.match(ok, /no-tag-list form\.$/);
|
|
assert.match(orphan, /no version to derive either\.$/);
|
|
assert.match(base, /did not return them\.$/);
|
|
});
|
|
|
|
test('ramElfAppletCandidate maps a package to its applet instance', () => {
|
|
// live 2026-09-30: the applet instance AIDs are the package's module AIDs
|
|
// (they differ from the package AID in the last byte)
|
|
const apps = [
|
|
{ aid: 'A1130001180002FFF7100E8904000208' },
|
|
{ aid: 'A1130001180002FFF7100E89494D4508' },
|
|
{ aid: 'F0414C4641600101' },
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903' },
|
|
];
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'A1130001180002FFF7100E8904000200',
|
|
moduleAids: ['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508'] }, apps).aid,
|
|
'A1130001180002FFF7100E8904000208');
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'F0414C46416001',
|
|
moduleAids: ['F0414C4641600101'] }, apps).aid, 'F0414C4641600101');
|
|
// an applet that already carries a version is skipped, and a package whose
|
|
// module is that applet has no other candidate
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'A1130001180001FFFFFFFF89A1003900',
|
|
moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }, apps), null);
|
|
// a package with no applet at all
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'A00000006203010101', moduleAids: [] }, apps), null);
|
|
assert.strictEqual(ramElfAppletCandidate(null, apps), null);
|
|
// the shared-prefix rule is the fallback
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'ABCD' }, [{ aid: 'ABCDEF' }]).aid, 'ABCDEF');
|
|
// the app's C4 (Load File AID) is the authoritative mapping when the card
|
|
// reports the real package AID
|
|
assert.strictEqual(ramElfAppletCandidate({ aid: 'A0000005591010FFFFFFFF8900000E00' },
|
|
[{ aid: 'A0000005591010FFFFFFFF8900001200', elfAid: 'A0000005591010FFFFFFFF8900000E00' }]).aid,
|
|
'A0000005591010FFFFFFFF8900001200');
|
|
});
|
|
|
|
test('the explorer annotates a derived package version', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramRenderExploreHtml(null, [], [],
|
|
[{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903', versionFrom: 'app' }]);
|
|
const plain = ramRenderExploreHtml(null, [], [],
|
|
[{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903' }]);
|
|
delete globalThis.t;
|
|
assert.ok(out.includes('Version: 9.3 <span class="text-gray-500 dark:text-slate-400">(from the app instance)</span>'), out);
|
|
assert.ok(!plain.includes('from the app instance'), plain);
|
|
});
|
|
|
|
test('the expanded formats never chain a GET RESPONSE (TCA loader form)', () => {
|
|
assert.strictEqual(ramFormatIsExpanded('expanded'), true);
|
|
assert.strictEqual(ramFormatIsExpanded('expanded-ae'), true);
|
|
assert.strictEqual(ramFormatIsExpanded('compact'), false);
|
|
assert.strictEqual(ramFormatIsExpanded('auto'), false);
|
|
assert.strictEqual(ramFormatIsExpanded(undefined), false);
|
|
const src = extractFunc(html, 'ramExplore');
|
|
// the listing pass follows the run's command format and pins it per send,
|
|
// so a chain can never ride inside an expanded packet
|
|
assert.ok(src.includes('ramFormatIsExpanded(sp.ram_format)'), 'the listing format must follow the run');
|
|
assert.ok(src.includes('ramGetStatusApdu(p1, p2, listingFmt)'), 'the builder must follow the format');
|
|
assert.ok(src.includes('ram_format: listingFmt'), 'the listing format must be pinned per send');
|
|
// the registry pass: the chained retry is compact-only, the format ladder
|
|
// covers the expanded codings, and the working format is remembered
|
|
assert.ok(src.includes('!ramFormatIsExpanded(usedFmt)'), 'the chained retry must be compact-only');
|
|
assert.ok(src.includes('ramFmtLadder(runFmt, expandedFmt)'),
|
|
'the format ladder (with its remembered format) must drive the retries');
|
|
assert.ok(src.includes('expandedFmt = fmt'), 'the working format must be remembered');
|
|
assert.ok(src.includes('fmt: out.fmt'), 'the report must carry the serving format');
|
|
});
|
|
|
|
test('ramFmtLadder orders the registry command formats cautiously', () => {
|
|
// the run's format first (null = no override), then the expanded codings,
|
|
// then compact as the last resort; the run's own format is not repeated
|
|
assert.deepStrictEqual(ramFmtLadder('compact', null), [null, 'expanded', 'expanded-ae']);
|
|
assert.deepStrictEqual(ramFmtLadder('expanded', null), [null, 'expanded-ae', 'compact']);
|
|
assert.deepStrictEqual(ramFmtLadder('expanded-ae', null), [null, 'expanded', 'compact']);
|
|
assert.deepStrictEqual(ramFmtLadder('auto', null), [null, 'expanded', 'expanded-ae', 'compact']);
|
|
// a format that already served data is tried first and never repeated
|
|
assert.deepStrictEqual(ramFmtLadder('compact', 'expanded'), ['expanded', 'expanded-ae']);
|
|
assert.deepStrictEqual(ramFmtLadder('expanded-ae', 'expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
|
|
});
|
|
|
|
test('cardEtsiRelease infers the card ETSI release from uicc.toolkit', () => {
|
|
const tk = v => ({ aid: 'A0000000090005FFFFFFFF8912000000', version: v });
|
|
// the CE tag is raw hex: 010C is 1.12 (ramFmtVersion, as the ELF rows show)
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('010C')]),
|
|
{ release: 'REL-15/16/17', basis: 'uicc.toolkit 1.12' });
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('010B')]),
|
|
{ release: 'REL-12', basis: 'uicc.toolkit 1.11' });
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('0105')]),
|
|
{ release: 'REL-9', basis: 'uicc.toolkit 1.5' });
|
|
// a version outside the table: the basis is still named
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('01FF')]),
|
|
{ release: '', basis: 'uicc.toolkit 1.255' });
|
|
// a value that is not 4-hex passes through ramFmtVersion unchanged
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('1.12')]),
|
|
{ release: 'REL-15/16/17', basis: 'uicc.toolkit 1.12' });
|
|
// the compact listing carries no versions (the expanded CE tag does)
|
|
assert.deepStrictEqual(cardEtsiRelease([tk('')]), { release: '', basis: '' });
|
|
// no uicc.toolkit (SIM-only card): no label, like SIM-only CAPs
|
|
assert.strictEqual(cardEtsiRelease([{ aid: 'A0000000090003FFFFFFFF8910710002', version: '2.6' }]), null);
|
|
assert.strictEqual(cardEtsiRelease(null), null);
|
|
});
|
|
|
|
test('the explorer shows the inferred ETSI release', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [],
|
|
[{ aid: 'A0000000090005FFFFFFFF8912000000', version: '010C' }], 'compact', '', '');
|
|
delete globalThis.t;
|
|
assert.ok(out.includes('ETSI release: REL-15/16/17 (uicc.toolkit 1.12)'), out);
|
|
globalThis.t = s => s;
|
|
const none = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [], [], 'compact', '', '');
|
|
delete globalThis.t;
|
|
assert.ok(!none.includes('ETSI release:'), none);
|
|
});
|
|
|
|
test('ramExpandedReport names the expanded serving format', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramExpandedReport([
|
|
{ p1: '80', label: 'ISD', compact: false, form: 'lean', sw: '6310', entries: 3, fmt: 'expanded' },
|
|
{ p1: '40', label: 'Apps', compact: false, form: 'lean', sw: '9000', entries: 2, fmt: 'expanded-ae' },
|
|
]);
|
|
delete globalThis.t;
|
|
assert.ok(out.startsWith('Expanded registry:'), out);
|
|
assert.ok(out.includes('ISD [AA]'), out);
|
|
assert.ok(out.includes('Apps [AE 80]'), out);
|
|
});
|
|
|
|
test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
|
|
globalThis.t = s => s;
|
|
// every listing answered the reference form
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '80', label: 'ISD', compact: true, form: 'tags', sw: '9000', entries: 1 },
|
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 2 },
|
|
{ p1: '10', label: 'ELFs', compact: true, form: 'tags', sw: '9000', entries: 4 },
|
|
]), 'Expanded registry: tag list + EA (ISD, Apps, ELFs)');
|
|
// the ISD commonly has no expanded data (6A88/6A86 is normal there)
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
|
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 2 },
|
|
{ p1: '10', label: 'ELFs', compact: true, form: 'noea', sw: '9000', entries: 4 },
|
|
]), 'Expanded registry: tag list + EA (Apps) \u00b7 tag list (ELFs) \u00b7 ISD: no expanded data (6A88) \u2014 compact listing used');
|
|
// a listing the card refused entirely: the compact listing carried it
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '40', label: 'Apps', compact: true, form: null, sw: '6A88' },
|
|
{ p1: '10', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 4 },
|
|
]), 'Expanded registry: no tag list (ELFs) \u00b7 Apps: refused (6A88) \u2014 compact listing used');
|
|
// nothing answered: the headline says the versions are unavailable
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '80', label: 'ISD', compact: true, form: null, sw: '6A88' },
|
|
{ p1: '10', label: 'ELFs', compact: true, form: null, sw: '6A86' },
|
|
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ISD: no expanded data (6A88) \u2014 compact listing used \u00b7 ELFs: refused (6A86) \u2014 compact listing used');
|
|
// a listing with no compact data either reports its own status word
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: null, sw: '6A88' },
|
|
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ELFs: 6A88');
|
|
// the ELF listing queried twice (P1=10 accepted-but-empty, then the P1=20
|
|
// fallback with data): one mention, from the pass that produced entries
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 0 },
|
|
{ p1: '20', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 5 },
|
|
]), 'Expanded registry: no tag list (ELFs)');
|
|
// a refusal followed by a pass with data is not reported as a refusal
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: null, sw: '6A88' },
|
|
{ p1: '20', label: 'ELFs', compact: false, form: 'lean', sw: '9000', entries: 5 },
|
|
]), 'Expanded registry: lean tag list (ELFs)');
|
|
// the richer form wins when both produced entries
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '10', label: 'ELFs', compact: true, form: 'lean', sw: '9000', entries: 3 },
|
|
{ p1: '20', label: 'ELFs', compact: true, form: 'notags', sw: '9000', entries: 5 },
|
|
]), 'Expanded registry: lean tag list (ELFs)');
|
|
// an unfulfilled 6310 (the next-occurrence page was refused) is noted
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '6A86', entries: 2, more: true },
|
|
]), 'Expanded registry: tag list + EA (Apps (first page only))');
|
|
// a request/transport failure is not a remote status word
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '40', label: 'Apps', compact: true, form: 'tags', sw: '9000', entries: 9 },
|
|
{ p1: '10', label: 'ELFs', compact: true, form: null, sw: 'index out of range', transport: true },
|
|
]), 'Expanded registry: tag list + EA (Apps) \u00b7 ELFs: request error (index out of range) \u2014 compact listing used');
|
|
assert.strictEqual(ramExpandedReport([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: null, sw: 'index out of range', transport: true },
|
|
]), 'Expanded registry refused by the card \u2014 compact listing only (package versions unavailable) ELFs: request error (index out of range)');
|
|
assert.strictEqual(ramExpandedReport([]), '');
|
|
delete globalThis.t;
|
|
});
|
|
|
|
test('ramExpandedElfForm names the ELF listing form for the version hint', () => {
|
|
globalThis.t = s => s;
|
|
assert.strictEqual(ramExpandedElfForm([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 5 },
|
|
]), 'notags');
|
|
// an accepted-but-empty pass is no form for the hint
|
|
assert.strictEqual(ramExpandedElfForm([
|
|
{ p1: '10', label: 'ELFs', compact: false, form: 'notags', sw: '9000', entries: 0 },
|
|
]), '');
|
|
assert.strictEqual(ramExpandedElfForm([]), '');
|
|
delete globalThis.t;
|
|
});
|
|
|
|
test('the explorer hints why the package versions are missing', () => {
|
|
globalThis.t = s => s;
|
|
const elfs = [{ aid: 'E1', lifecycle: '01', privileges: '00' }];
|
|
const noForm = ramRenderExploreHtml(null, [], [], elfs);
|
|
const noTags = ramRenderExploreHtml(null, [], [], elfs, '', '', 'notags');
|
|
const withVer = ramRenderExploreHtml(null, [], [], [{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0100' }], '', '', 'notags');
|
|
delete globalThis.t;
|
|
assert.ok(noForm.includes('Package versions come from the expanded registry query'), noForm);
|
|
assert.ok(noTags.includes('only in the tag-list forms of the expanded registry query'), noTags);
|
|
assert.ok(!withVer.includes('Package versions'), withVer);
|
|
});
|
|
|
|
test('spCntrLow reads the low-counter verdict of a send response', () => {
|
|
// the server's explicit flag
|
|
assert.strictEqual(spCntrLow({ cntr_low: true }), true);
|
|
// derived from the PoR alone (a 9000/91xx ENVELOPE can carry a cntr_low PoR)
|
|
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_low', cntr: '00000000BB' } }), true);
|
|
// every other verdict (and a missing response) is not a low counter
|
|
assert.strictEqual(spCntrLow({ por: { response_status: 'por_ok', cntr: '01' } }), false);
|
|
assert.strictEqual(spCntrLow({ por: { response_status: 'cntr_high' } }), false);
|
|
assert.strictEqual(spCntrLow({}), false);
|
|
assert.strictEqual(spCntrLow(null), false);
|
|
});
|
|
|
|
test('ramCntrLowHtml states the facts without a fabricated counter', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramCntrLowHtml(2);
|
|
const noPreset = ramCntrLowHtml(-1);
|
|
// the notice follows the failed operation's view ('sp' -> its own keyset)
|
|
const spView = ramCntrLowHtml(1, 'sp');
|
|
delete globalThis.t;
|
|
assert.ok(out.includes('Low counter \u2014 the card rejected the packet'), out);
|
|
// the PoR's CNTR is a copy of the command's counter, never the card's: no
|
|
// value may be shown (v3.9.x), only the verdict and the two actions
|
|
assert.ok(!out.includes('card counter'), out);
|
|
assert.ok(out.includes('onclick="ramSyncCounter(2, \'ram\')"'), out);
|
|
assert.ok(out.includes('Sync counter'), out);
|
|
assert.ok(out.includes('onclick="ramGoToPreset(2)"'), out);
|
|
assert.ok(out.includes('Go to preset'), out);
|
|
// without a known preset the buttons still work (-1)
|
|
assert.ok(noPreset.includes('onclick="ramGoToPreset(-1)"'), noPreset);
|
|
assert.ok(noPreset.includes('onclick="ramSyncCounter(-1, \'ram\')"'), noPreset);
|
|
assert.ok(spView.includes('onclick="ramSyncCounter(1, \'sp\')"'), spView);
|
|
// v3.23.3: the probe button is amber, the preset jump green
|
|
const syncBtn = out.match(/<button[^>]*ramSyncCounter[^>]*>/)[0];
|
|
assert.ok(syncBtn.includes('border-amber-300'), syncBtn);
|
|
assert.ok(!syncBtn.includes('border-red-300'), syncBtn);
|
|
const goBtn = out.match(/<button[^>]*ramGoToPreset[^>]*>/)[0];
|
|
assert.ok(goBtn.includes('border-emerald-300'), goBtn);
|
|
// the notice targets the originating view's box; the probe renders the
|
|
// inline throbber (the RAM inline-progress spinner, no modal)
|
|
assert.match(extractFunc(html, 'ramShowCntrLow'), /ramCntrLowBox\(v\)/);
|
|
assert.match(extractFunc(html, 'ramSyncCounter'), /animate-spin/);
|
|
assert.match(extractFunc(html, 'ramSyncCounter'), /ramCntrLowBox\(/);
|
|
assert.match(html, /id="sp-cntr-low"/);
|
|
});
|
|
|
|
test('ramShowCntrLow targets the originating view box', () => {
|
|
// the notice must land in the pill the failed operation ran in: the
|
|
// Secured packet notice used to be written into the hidden RAM container,
|
|
// so the buttons were unreachable from the plain send (v3.23.3)
|
|
globalThis.t = s => s;
|
|
const els = fakeRamDocument(['ram-cntr-low', 'sp-cntr-low']);
|
|
ramShowCntrLow(2, 'sp');
|
|
assert.ok(els['sp-cntr-low'].innerHTML.includes("ramSyncCounter(2, 'sp')"),
|
|
els['sp-cntr-low'].innerHTML);
|
|
assert.ok(!els['sp-cntr-low'].classList.contains('hidden'));
|
|
assert.strictEqual(els['ram-cntr-low'].innerHTML, '');
|
|
assert.ok(els['ram-cntr-low'].classList.contains('hidden'));
|
|
ramShowCntrLow(1, 'ram');
|
|
assert.ok(els['ram-cntr-low'].innerHTML.includes("ramSyncCounter(1, 'ram')"),
|
|
els['ram-cntr-low'].innerHTML);
|
|
assert.ok(!els['ram-cntr-low'].classList.contains('hidden'));
|
|
assert.ok(els['sp-cntr-low'].classList.contains('hidden'));
|
|
delete globalThis.t;
|
|
});
|
|
|
|
test('the explorer shows versions on app and ISD rows too', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramRenderExploreHtml(null,
|
|
[{ aid: 'A000000151000000', lifecycle: '0F', privileges: '82FC80', version: '0001' }],
|
|
[{ aid: 'A1130001180001FFFFFFFF89A1003908', lifecycle: '07', privileges: '000000', version: '0903' }],
|
|
[]);
|
|
delete globalThis.t;
|
|
assert.ok(out.includes('Version: 0.1'), out);
|
|
assert.ok(out.includes('Version: 9.3'), out);
|
|
});
|
|
|
|
test('ramStepLine names a bad-format failure instead of "PoR ok"', () => {
|
|
globalThis.t = s => s;
|
|
const line = ramStepLine({ name: 'P2=02 (lean)', por_status: 'por_ok',
|
|
por_error: 'bad format 02 (wrong length)', por_bad_format: '02',
|
|
por_bad_format_name: 'wrong length' }, 0);
|
|
const plain = ramStepLine({ name: 'LOAD', por_status: 'por_ok', por_sw: '9000' }, 1);
|
|
delete globalThis.t;
|
|
assert.ok(line.includes('bad format 02 (wrong length)'), line);
|
|
assert.ok(!line.includes('PoR ok'), line);
|
|
// a healthy step is unchanged
|
|
assert.ok(plain.includes('9000') || plain.includes('PoR ok'), plain);
|
|
});
|
|
|
|
test('the Explore retries the ELF tag lists for both P1 values and per applet AID', () => {
|
|
const src = extractFunc(html, 'ramExplore');
|
|
assert.ok(src.includes('elfTagListRetry'), 'the tag-list retry must exist');
|
|
assert.ok(src.includes("for (const p1 of ['10', '20'])"), 'the retry must walk both P1 values');
|
|
assert.ok(src.includes('expandedTried[p1]'), 'already-tried forms must be skipped');
|
|
assert.ok(src.includes("['tags', 'noea', 'lean'].filter"), 'only the tag-list forms are retried');
|
|
assert.ok(src.includes('elfPerAidVersions'), 'the per-AID version queries must exist');
|
|
assert.ok(src.includes("ramExpandedQueryApdu('40', 'tags', false, app.aid)"),
|
|
'the per-AID query must use the AID filter');
|
|
assert.ok(src.includes('ramElfAppletCandidate(e, apps)'),
|
|
'the module->applet mapping must be used');
|
|
assert.ok(src.includes('ramChainGetResponse(apdu)'),
|
|
'the single-AID 61xx answer must be fetched with a chained GET RESPONSE');
|
|
// the per-AID budget counts queries, not list positions (a package with
|
|
// no applet instance is skipped before the cap)
|
|
assert.ok(src.includes('if (app) missing.push({ elf: e, app: app });'),
|
|
'the per-AID candidates must be filtered before the cap');
|
|
});
|
|
|
|
test('every SCP80/RAM flow checks the low counter and stops', () => {
|
|
// the Explore: the guarded sender, the loop guards and the final branch
|
|
const explore = extractFunc(html, 'ramExplore');
|
|
assert.ok(explore.includes('spCntrLow(res)'), 'the Explore must check every send');
|
|
assert.ok(explore.includes('if (cntrLow) break;'), 'the loops must stop');
|
|
assert.ok(explore.includes("ramShowCntrLow(undefined, 'ram')"),
|
|
'the verdict must be shown in the RAM view');
|
|
// the delete flow and the plain send check it too
|
|
assert.ok(extractFunc(html, 'ramDeleteFromExplorer').includes('spCntrLow(res)'),
|
|
'the delete flow must stop');
|
|
// the preset jump opens the Cards tab, selects the preset and focuses the counter
|
|
const go = extractFunc(html, 'ramGoToPreset');
|
|
assert.ok(go.includes("switchTab('cards')"), go);
|
|
assert.ok(go.includes('cardsEdit('), go);
|
|
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
|
|
// the notice's sync probe posts to the bounded server endpoint with the
|
|
// preset, the keyset the failed operation used and its SPI/TAR
|
|
const sync = extractFunc(html, 'ramSyncCounter');
|
|
assert.ok(sync.includes("'/api/counter-probe'"), sync);
|
|
assert.ok(sync.includes("own = view === 'sp'"), sync);
|
|
assert.ok(sync.includes('kvnOf(own)'), sync);
|
|
assert.ok(sync.includes('kvnOf(other)'), sync);
|
|
assert.ok(sync.includes('spKeysetSync()'), 'the keyset labels must refresh');
|
|
assert.ok(sync.includes('spCntrPlaceholder()'),
|
|
'the stored value must refresh the field placeholder (the field stays an override)');
|
|
assert.ok(sync.includes("ramListingSpi2('40')"),
|
|
'the RAM view must probe with the RAM listing transport (a PoR is required)');
|
|
assert.ok(sync.includes('data.counter_saved'),
|
|
'the saved-on-error case must be reported in amber');
|
|
});
|
|
|
|
test('the RAM form offers the command format choice', () => {
|
|
// auto (detect) is the default; the two expanded codings can be pinned
|
|
assert.match(html, /<select id="ram-format"[\s\S]*?<option value="auto"/,
|
|
'the RAM format selector must exist and default to auto');
|
|
for (const v of ['compact', 'expanded', 'expanded-ae']) {
|
|
assert.ok(html.includes('<option value="' + v + '"'), 'missing format ' + v);
|
|
}
|
|
assert.ok(extractFunc(html, 'getRamSpParams').includes("getElementById('ram-format')"),
|
|
'the RAM params must carry the selected format');
|
|
});
|
|
|
|
test('the Explore detects the expanded query form and reports it', () => {
|
|
const src = extractFunc(html, 'ramExplore');
|
|
assert.ok(src.includes('RAM_EXPANDED_FORMS'), 'the form ladder must be used');
|
|
assert.ok(src.includes('expandedReport.push'), 'each listing outcome must be recorded');
|
|
assert.ok(src.includes('ramExpandedReport(expandedReport)'), 'the report must reach the explorer');
|
|
// the ISD gets a single attempt: its 6A88 must not poison the detection
|
|
assert.ok(src.includes("if (p1 === '80') forms = [expandedForm || 'tags'];"),
|
|
'the ISD must not walk the ladder');
|
|
// a 61xx answer holds the data (chained GET RESPONSE) and a 6310 pages
|
|
assert.ok(src.includes('ramChainGetResponse(apdu)'), '61xx must be answered with a chained GET RESPONSE');
|
|
assert.ok(!src.includes('00C00000'), 'no standalone GET RESPONSE (it answers 6700)');
|
|
assert.ok(src.includes('ramExpandedQueryApdu(p1, form, page > 1)'), '6310 must page');
|
|
// a transport failure must not be reported as a remote status word
|
|
assert.ok(src.includes('transportError'), 'request errors are marked as such');
|
|
});
|
|
|
|
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
|
|
// Explore_NC.log, ISD: E3 { 4F AID, 9F70 LC, C5 privileges }
|
|
const isd = ramParseAppStatus('E3134F08A0000000030000009F70010FC5039AFE80');
|
|
assert.strictEqual(isd.length, 1);
|
|
assert.strictEqual(isd[0].aid, 'A000000003000000');
|
|
assert.strictEqual(isd[0].lifecycle, '0F');
|
|
assert.strictEqual(isd[0].privileges, '9AFE80');
|
|
assert.strictEqual(isd[0].type, 'app');
|
|
// Explore_ET.log, application entry: adds the ELF AID (C4) and SD AID (CC)
|
|
const apps = ramParseAppStatus('E3374F10A1130001180001FFFFFFFF89A10039089F700107C503000000C410A1130001180001FFFFFFFF89A1003900CC08A000000151000000');
|
|
assert.strictEqual(apps.length, 1);
|
|
assert.strictEqual(apps[0].aid, 'A1130001180001FFFFFFFF89A1003908');
|
|
assert.strictEqual(apps[0].elfAid, 'A1130001180001FFFFFFFF89A1003900');
|
|
assert.strictEqual(apps[0].sdAid, 'A000000151000000');
|
|
assert.strictEqual(apps[0].privileges, '000000');
|
|
});
|
|
|
|
test('ramParseElfStatus parses the expanded E3 listing (NP trace)', () => {
|
|
const elfs = ramParseElfStatus('E31B4F07A00000015153509F700101CE020100CC08A000000151000000');
|
|
assert.strictEqual(elfs.length, 1);
|
|
assert.strictEqual(elfs[0].aid, 'A0000001515350');
|
|
assert.strictEqual(elfs[0].lifecycle, '01');
|
|
assert.strictEqual(elfs[0].version, '0100');
|
|
assert.strictEqual(elfs[0].sdAid, 'A000000151000000');
|
|
assert.strictEqual(elfs[0].type, 'elf');
|
|
});
|
|
|
|
test('ramParseElfStatus lists the compact ELF and module listings (F0414C46416101)', () => {
|
|
// Exact bytes from a live RAM Explore: the P1=20 ELF page and the P1=10
|
|
// (ELF+modules) page. The old 0x10-scan walk dropped everything after the
|
|
// first entry; the deterministic AID walk lists them all.
|
|
const elfPage = '10A0000000090005FFFFFFFF8911000000010010A0000000871005FFFFFFFF8913100000010010A0000000871005FFFFFFFF8914100000010010A0000000090005FFFFFFFF8912000000010010A0000000871005FFFFFFFF8913200000010010A0000000090005FFFFFFFF8913000000010010A0000000090005FFFFFFFF8911010000010010D2760001180002FF49100A89AA060F00010010A1130001180001FFFFFFFF89A1003900010010A1130001180002FFF7100E8904000200010007F0414C464161010100';
|
|
const elfs = ramParseElfStatus(elfPage);
|
|
const f041 = elfs.find(r => r.aid === 'F0414C46416101');
|
|
assert.ok(f041, JSON.stringify(elfs.map(r => r.aid)));
|
|
assert.strictEqual(f041.lifecycle, '01');
|
|
// the 4th byte is the privileges byte (the legacy GET STATUS shape);
|
|
// the compact listing has no version - it comes from the expanded CE tag
|
|
assert.strictEqual(f041.privileges, '00');
|
|
assert.strictEqual(f041.version, undefined);
|
|
assert.ok(elfs.some(r => r.aid === 'A1130001180002FFF7100E8904000200'), 'A113 ELF missing');
|
|
assert.ok(elfs.some(r => r.aid === 'A0000000090005FFFFFFFF8912000000'), 'uicc.toolkit ELF missing');
|
|
|
|
const modulesPage = '10A1130001180001FFFFFFFF89A100390001000110A1130001180001FFFFFFFF89A100390810A1130001180002FFF7100E890400020001000210A1130001180002FFF7100E890400020810A1130001180002FFF7100E89494D450807F0414C4641610101000108F0414C4641610101';
|
|
const mods = ramParseElfStatus(modulesPage, true);
|
|
const f041Row = mods.find(r => r.aid === 'F0414C46416101');
|
|
assert.ok(f041Row, JSON.stringify(mods.map(r => r.aid)));
|
|
assert.deepStrictEqual(f041Row.moduleAids, ['F0414C4641610101']);
|
|
});
|
|
|
|
test('_parseE3Entry collects Executable Module AIDs from every 84 TLV', () => {
|
|
// GP Table 11-37: one Executable Module AID per '84' TLV (the reference
|
|
// GPPro coding), repeated per module.
|
|
const one = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039008410A1130001180001FFFFFFFF89A1003908');
|
|
assert.deepStrictEqual(one.moduleAids, ['A1130001180001FFFFFFFF89A1003908']);
|
|
const two = _parseE3Entry('4F10A1130001180002FFF7100E89040002008410A1130001180002FFF7100E89040002088410A1130001180002FFF7100E89494D4508');
|
|
assert.deepStrictEqual(two.moduleAids,
|
|
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
|
|
// a card concatenating 4F<len><aid> TLVs inside one 84 still parses
|
|
const concat = _parseE3Entry('4F10A1130001180002FFF7100E890400020084244F10A1130001180002FFF7100E89040002084F10A1130001180002FFF7100E89494D4508');
|
|
assert.deepStrictEqual(concat.moduleAids,
|
|
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
|
|
});
|
|
|
|
test('_parseE3Entry reads the SCP Registry Data menu entries (NP trace)', () => {
|
|
// Explore_NP.log app entry: ... EA 05 80 03 01 80 01 = menu entry
|
|
// position 1, identifier 0x80, enabled (TS 102 226 Tables 8.2/8.3).
|
|
const r = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039089F70020700C503000000C410A1130001180001FFFFFFFF89A1003900CE020903CC07A0000001510000EA058003018001');
|
|
assert.strictEqual(r.aid, 'A1130001180001FFFFFFFF89A1003908');
|
|
assert.strictEqual(r.lifecycle, '0700');
|
|
assert.strictEqual(r.version, '0903');
|
|
assert.strictEqual(r.sdAid, 'A0000001510000');
|
|
assert.strictEqual(r.elfAid, 'A1130001180001FFFFFFFF89A1003900');
|
|
assert.deepStrictEqual(r.menuEntries, [{ pos: '01', id: '80', state: '01' }]);
|
|
assert.strictEqual(r.rawTlvs, undefined);
|
|
// an empty EA (an ISD with no menu entries) is not an error and adds nothing
|
|
const empty = _parseE3Entry('4F08A000000151000000EA00');
|
|
assert.strictEqual(empty.menuEntries, undefined);
|
|
assert.strictEqual(empty.rawTlvs, undefined);
|
|
});
|
|
|
|
test('_parseE3Entry preserves registry data objects it does not decode', () => {
|
|
// Explore_NP.log ISD entry: the card returned 8F/90/91/92 beyond the
|
|
// requested GP tags - shown raw, never dropped.
|
|
const r = _parseE3Entry('4F07A00000015100009F70020F00C5039AFF80C407A0000001515350CE020001CC07A00000015100008F02752B9002012D910265D592020BD3');
|
|
assert.deepStrictEqual(r.rawTlvs, [
|
|
{ tag: '8F', value: '752B' }, { tag: '90', value: '012D' },
|
|
{ tag: '91', value: '65D5' }, { tag: '92', value: '0BD3' },
|
|
]);
|
|
assert.strictEqual(r.privileges, '9AFF80');
|
|
});
|
|
|
|
test('ramFmtVersion formats the 2-byte CE version as major.minor', () => {
|
|
// GP Table 11-37 Note 1: Java Card CAP major/minor attributes, in order.
|
|
assert.strictEqual(ramFmtVersion('0903'), '9.3');
|
|
assert.strictEqual(ramFmtVersion('0100'), '1.0');
|
|
assert.strictEqual(ramFmtVersion('0A0B'), '10.11');
|
|
// other lengths stay raw (the format depends on the load file format)
|
|
assert.strictEqual(ramFmtVersion('09'), '09');
|
|
assert.strictEqual(ramFmtVersion(''), '');
|
|
});
|
|
|
|
test('ramFmtLifecycle decodes per GP object kind (Tables 11-3..11-6)', () => {
|
|
// ISD inherits the card life cycle (Table 11-6)
|
|
assert.strictEqual(ramFmtLifecycle('01', 'isd'), 'OP_READY');
|
|
assert.strictEqual(ramFmtLifecycle('07', 'isd'), 'INITIALIZED');
|
|
assert.strictEqual(ramFmtLifecycle('0F', 'isd'), 'SECURED');
|
|
assert.strictEqual(ramFmtLifecycle('7F', 'isd'), 'CARD_LOCKED');
|
|
assert.strictEqual(ramFmtLifecycle('FF', 'isd'), 'TERMINATED');
|
|
// Executable Load File (Table 11-3)
|
|
assert.strictEqual(ramFmtLifecycle('01', 'elf'), 'LOADED');
|
|
// Application (Table 11-4)
|
|
assert.strictEqual(ramFmtLifecycle('03', 'app'), 'INSTALLED');
|
|
assert.strictEqual(ramFmtLifecycle('07', 'app'), 'SELECTABLE');
|
|
assert.strictEqual(ramFmtLifecycle('0F', 'app'), 'SELECTABLE (0x0F)');
|
|
assert.strictEqual(ramFmtLifecycle('83', 'app'), 'LOCKED');
|
|
// Security Domain (Table 11-5)
|
|
assert.strictEqual(ramFmtLifecycle('0F', 'sd'), 'PERSONALIZED');
|
|
assert.strictEqual(ramFmtLifecycle('8F', 'sd'), 'LOCKED');
|
|
// a card may append extra bytes (NP trace: 9F70 02 0700)
|
|
assert.strictEqual(ramFmtLifecycle('0700', 'app'), 'SELECTABLE');
|
|
assert.strictEqual(ramFmtLifecycle('0701', 'app'), 'SELECTABLE (0x01)');
|
|
// unknown values stay visible
|
|
assert.strictEqual(ramFmtLifecycle('42', 'app'), '0x42');
|
|
assert.strictEqual(ramFmtLifecycle('', 'app'), '');
|
|
});
|
|
|
|
test('ramMenuState and ramEntryKind decode menu state and SD detection', () => {
|
|
global.t = s => s;
|
|
assert.strictEqual(ramMenuState('00'), 'Disabled');
|
|
assert.strictEqual(ramMenuState('01'), 'Enabled');
|
|
assert.strictEqual(ramMenuState('02'), '0x02');
|
|
delete global.t;
|
|
// GP Table 11-7 byte 1 b8 = Security Domain privilege
|
|
assert.strictEqual(ramEntryKind({ privileges: '9AFE80' }), 'sd');
|
|
assert.strictEqual(ramEntryKind({ privileges: '04' }), 'app');
|
|
assert.strictEqual(ramEntryKind({ type: 'elf', privileges: '00' }), 'elf');
|
|
assert.strictEqual(ramEntryKind(null), 'app');
|
|
});
|
|
|
|
test('the explorer shows versions, menu entries and unknown registry data', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml(
|
|
{ appCount: 1, freeNV: 10, freeV: 5 },
|
|
[],
|
|
[{ aid: 'A1130001180001FFFFFFFF89A1003908', lifecycle: '0700', privileges: '000000',
|
|
elfAid: 'A1130001180001FFFFFFFF89A1003900', sdAid: 'A0000001510000',
|
|
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
|
|
[{ aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00', version: '0309' }]
|
|
);
|
|
delete global.t;
|
|
// the app row: SELECTABLE from the 2-byte 0700, menu entry, raw TLV
|
|
assert.ok(out.includes('SELECTABLE'), out);
|
|
assert.ok(out.includes('STK menu entry: 0x80 (position 1, Enabled)'), out);
|
|
assert.ok(out.includes('Other registry data: 8F=752B'), out);
|
|
// the ELF row: LOADED, formatted version, no privileges noise for '00'
|
|
assert.ok(out.includes('LOADED'), out);
|
|
assert.ok(out.includes('Version: 3.9'), out);
|
|
const elfSection = out.slice(out.indexOf('Executable Load Files'));
|
|
assert.ok(!elfSection.includes('Privileges:'), elfSection);
|
|
});
|
|
|
|
test('ramMergeExpanded fills only what the compact listing lacks', () => {
|
|
// the compact ELF entry has no version (v3.6.51): the expanded CE version
|
|
// and SD AID merge in - this was blocked while the bogus compact "version"
|
|
// was still set
|
|
const compact = [{ type: 'elf', aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' }];
|
|
const expanded = [{ aid: 'A1130001180001FFFFFFFF89A1003900', version: '0309', sdAid: 'A0000001510000', lifecycle: '01' }];
|
|
ramMergeExpanded(compact, expanded);
|
|
assert.strictEqual(compact[0].version, '0309');
|
|
assert.strictEqual(compact[0].sdAid, 'A0000001510000');
|
|
// a field the compact entry already carries is never overwritten
|
|
const apps = [{ aid: 'A1', privileges: '04' }];
|
|
ramMergeExpanded(apps, [{ aid: 'A1', privileges: '9AFE80', menuEntries: [{ pos: '01', id: '80', state: '01' }] }]);
|
|
assert.strictEqual(apps[0].privileges, '04');
|
|
assert.deepStrictEqual(apps[0].menuEntries, [{ pos: '01', id: '80', state: '01' }]);
|
|
// module lists come from the expanded listing when it has them
|
|
const elfs = [{ aid: 'E1' }];
|
|
ramMergeExpanded(elfs, [{ aid: 'E1', moduleAids: ['M1'] }]);
|
|
assert.deepStrictEqual(elfs[0].moduleAids, ['M1']);
|
|
});
|
|
|
|
test('ramNvLine reports the measured NV footprint', () => {
|
|
global.t = s => s;
|
|
assert.strictEqual(ramNvLine({ nv_before: 50646, nv_after: 23400 }),
|
|
'NV used: ~27246 B (free 50646 \u2192 23400)');
|
|
assert.strictEqual(ramNvLine({ nv_before: 100, nv_after: 150 }),
|
|
'NV freed: ~50 B (free 100 \u2192 150)');
|
|
assert.strictEqual(ramNvLine({ nv_before: 100 }), '');
|
|
assert.strictEqual(ramNvLine({}), '');
|
|
assert.strictEqual(ramNvLine(null), '');
|
|
delete global.t;
|
|
});
|
|
|
|
test('ramParseAppStatus keeps 16-byte AIDs (no rawLen-1 truncation)', () => {
|
|
const page = '08D276000005AA3F010704'
|
|
+ '0FD276000005AA060200000000B000000700'
|
|
+ '0FD276000005AA060200000000B00001070010A1130001180001FFFFFFFF89A10039080700'
|
|
+ '10A1130001180002FFF7100E8904000208070010A1130001180002FFF7100E89494D45080700';
|
|
const apps = ramParseAppStatus(page);
|
|
assert.deepStrictEqual(apps.map(r => r.aid), [
|
|
'D276000005AA3F01', 'D276000005AA060200000000B00000',
|
|
'D276000005AA060200000000B00001',
|
|
'A1130001180001FFFFFFFF89A1003908', 'A1130001180002FFF7100E8904000208',
|
|
'A1130001180002FFF7100E89494D4508',
|
|
]);
|
|
assert.strictEqual(apps[3].lifecycle, '07');
|
|
});
|
|
|
|
test('ramDeleteApdu builds the GP DELETE with the 4F AID TLV (F0414C46416101)', () => {
|
|
// GP Card Spec v2.3.1 Table 11-20/23: P1=00, P2.b8 = object / object+related,
|
|
// data = '4F' AID TLV, case 3 (no Le - a trailing Le byte becomes a
|
|
// phantom command on the card's SCP80 layer, live 2026-09-28). The old
|
|
// handler called an undefined helper and no APDU was ever sent.
|
|
assert.strictEqual(ramDeleteApdu('F0414C46416101', false),
|
|
'80E40000094F07F0414C46416101');
|
|
assert.strictEqual(ramDeleteApdu('F0414C46416101', true),
|
|
'80E40080094F07F0414C46416101');
|
|
assert.strictEqual(ramDeleteApdu('A1130001180002FFF7100E8904000200', true),
|
|
'80E40080124F10A1130001180002FFF7100E8904000200');
|
|
assert.strictEqual(ramDeleteApdu('', false), '');
|
|
// the dead helper reference must not come back
|
|
assert.ok(!html.includes('_ber_len('), 'undefined _ber_len() call is back');
|
|
});
|
|
|
|
test('ramRemoteSwOk mirrors the server success set', () => {
|
|
for (const sw of ['9000', '6113', '62F1', '6310', 'CAFE']) {
|
|
assert.ok(ramRemoteSwOk(sw), sw);
|
|
}
|
|
for (const sw of ['6700', '6F00', '6A88', '', null]) {
|
|
assert.ok(!ramRemoteSwOk(sw), String(sw));
|
|
}
|
|
});
|
|
|
|
test('ramHasInstance matches instances by AID prefix', () => {
|
|
const apps = [{ aid: 'F0414C4641610101' },
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003908' }];
|
|
assert.strictEqual(ramHasInstance(apps, 'f0414c46416101'), true);
|
|
assert.strictEqual(ramHasInstance(apps, 'F0414C4641610101'), true);
|
|
assert.strictEqual(ramHasInstance(apps, 'F0414C46416001'), false);
|
|
assert.strictEqual(ramHasInstance([], 'F0414C46416101'), false);
|
|
});
|
|
|
|
test('the Explore result offers Install / Make selectable actions', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml(
|
|
null,
|
|
[],
|
|
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
|
|
[{ aid: 'F0414C46416101', lifecycle: '01', version: '0.0',
|
|
moduleAids: ['F0414C4641610101'] }]
|
|
);
|
|
// the module has no instance yet -> Install; the INSTALLED instance -> Make selectable
|
|
assert.ok(out.includes("ramInstallFromExplorer('F0414C46416101','F0414C4641610101')"), out);
|
|
assert.ok(out.includes("ramMakeSelectableFromExplorer('A000000151000000')"), out);
|
|
delete global.t;
|
|
});
|
|
|
|
test('the single-APDU ops are wired', () => {
|
|
assert.ok(/value="install-app"/.test(html), 'the INSTALL [for install] op is missing');
|
|
assert.ok(/value="make-selectable"/.test(html), 'the make selectable op is missing');
|
|
assert.ok(/async function ramInstallApp\(sp, op\)/.test(html), 'ramInstallApp is missing');
|
|
assert.ok(/pysimFetch\('\/api\/ram-install-app'/.test(html), 'the endpoint call is missing');
|
|
});
|
|
|
|
test('ramRemoveFromExplorer drops the object locally (cascade drops its applets)', () => {
|
|
let renders = 0;
|
|
globalThis.ramRenderExplorer = () => { renders++; };
|
|
_ramExplorerData = {
|
|
apps: [{ aid: 'F0414C4641610101' }, { aid: 'F0414C4641610199' },
|
|
{ aid: 'A1130001180001FFFFFFFF89A1003908' }],
|
|
elfs: [{ aid: 'F0414C46416101' }, { aid: 'A1130001180001FFFFFFFF89A1003900' }],
|
|
};
|
|
assert.strictEqual(ramRemoveFromExplorer('f0414c4641610101', false), true);
|
|
assert.deepStrictEqual(_ramExplorerData.apps.map(a => a.aid),
|
|
['F0414C4641610199', 'A1130001180001FFFFFFFF89A1003908']);
|
|
assert.strictEqual(renders, 1);
|
|
assert.strictEqual(ramRemoveFromExplorer('F0414C46416101', true), true);
|
|
assert.deepStrictEqual(_ramExplorerData.elfs.map(e => e.aid),
|
|
['A1130001180001FFFFFFFF89A1003900']);
|
|
assert.deepStrictEqual(_ramExplorerData.apps.map(a => a.aid),
|
|
['A1130001180001FFFFFFFF89A1003908'], 'an unrelated applet stays');
|
|
assert.strictEqual(renders, 2);
|
|
assert.strictEqual(ramRemoveFromExplorer('DEADBEEF', false), false);
|
|
delete globalThis.ramRenderExplorer;
|
|
});
|
|
|
|
test('getRamSpParams reads the computed SPI2 byte, not the base select', () => {
|
|
const fn = extractFunc(html, 'getRamSpParams');
|
|
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
|
|
});
|
|
|
|
test('getRamSpParams normalises the packet TAR to three bytes', () => {
|
|
const fn = extractFunc(html, 'getRamSpParams');
|
|
assert.ok(/tar:[\s\S]*?padEnd\(6, '0'\)\.slice\(0, 6\),/.test(fn), fn);
|
|
});
|
|
|
|
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
|
|
// Apps (40) and ELF (20/10) listings can exceed the ENVELOPE response;
|
|
// with SPI2=0x01 the card answers actual_response_sms_submit and the data
|
|
// never arrives (the live "Partial - Apps" bug).
|
|
for (const p1 of ['40', '20', '10']) assert.strictEqual(ramListingSpi2(p1), '21', p1);
|
|
for (const p1 of ['80', '00', 'FF']) assert.strictEqual(ramListingSpi2(p1), '01', p1);
|
|
});
|
|
|
|
function stubDeleteEnv(sendResult) {
|
|
// ramShowProgress/ramHideProgress are the real extracted helpers
|
|
const els = fakeRamDocument(['ram-result', 'ram-steps', 'ram-progress', 'ram-progress-text']);
|
|
const calls = { refresh: [], saved: [], sent: [], explored: null, removed: null };
|
|
globalThis.t = s => s;
|
|
globalThis.spRefreshFromPreset = sel => { calls.refresh.push(sel); };
|
|
globalThis.getRamSpParams = () => ({ cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' });
|
|
globalThis.confirm = () => true;
|
|
globalThis.alert = () => {};
|
|
globalThis.ramShowProgress = () => {}; // not part of the extracted FNS
|
|
globalThis.ramSendOta = async (apdu, sp) => {
|
|
calls.sent.push({ apdu: apdu, cntr: sp.cntr });
|
|
return sendResult;
|
|
};
|
|
globalThis.ramSaveCntr = c => { calls.saved.push(c); };
|
|
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
|
|
return { els, calls };
|
|
}
|
|
|
|
function unstubDeleteEnv() {
|
|
for (const k of ['getRamSpParams', 'confirm', 'alert', 'ramShowProgress',
|
|
'ramSendOta', 'ramSaveCntr', 'ramExplore']) {
|
|
delete globalThis[k];
|
|
}
|
|
globalThis.spRefreshFromPreset = () => ''; // the top-level stub
|
|
}
|
|
|
|
test('ramDeleteFromExplorer refreshes the preset and drops the record locally', async () => {
|
|
const { els, calls } = stubDeleteEnv({ success: true,
|
|
por: { response_status: 'por_ok', decoded: { last_status_word: '9000' } } });
|
|
_ramExplorerData = { apps: [{ aid: 'F0414C4641610101' }], elfs: [{ aid: 'F0414C46416101' }] };
|
|
globalThis.ramRenderExplorer = () => {};
|
|
await ramDeleteFromExplorer('F0414C46416101', true);
|
|
delete globalThis.ramRenderExplorer;
|
|
assert.deepStrictEqual(calls.refresh, ['ram-card-sel'],
|
|
'the preset must be re-read before the operation');
|
|
assert.strictEqual(calls.sent.length, 1);
|
|
assert.ok(calls.sent[0].apdu.startsWith('80E40080'), calls.sent[0].apdu);
|
|
assert.strictEqual(calls.sent[0].cntr, '0000000005');
|
|
assert.deepStrictEqual(calls.saved, ['0000000006'],
|
|
'the accepted packet advances the saved counter');
|
|
assert.deepStrictEqual(_ramExplorerData.elfs, [],
|
|
'a successful cascade delete drops the package from the Explore result');
|
|
assert.deepStrictEqual(_ramExplorerData.apps, [],
|
|
'and the package applets (AID prefix) with it');
|
|
assert.strictEqual(calls.explored, null, 'no re-explore is run');
|
|
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
|
unstubDeleteEnv();
|
|
});
|
|
|
|
test('ramDeleteFromExplorer leaves the counter untouched on a rejected packet', async () => {
|
|
const { calls } = stubDeleteEnv({ success: true, por: { response_status: 'cntr_low' } });
|
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
|
assert.deepStrictEqual(calls.saved, [], 'a rejected packet must not advance the counter');
|
|
assert.strictEqual(calls.explored, null, 'no re-explore after a rejected delete');
|
|
unstubDeleteEnv();
|
|
});
|
|
|
|
test('ramDeleteFromExplorer advances but does not re-explore on a failed remote command', async () => {
|
|
const { calls } = stubDeleteEnv({ success: true,
|
|
por: { response_status: 'por_ok', decoded: { last_status_word: '6A88' } } });
|
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
|
assert.deepStrictEqual(calls.saved, ['0000000006'],
|
|
'the card consumed the packet, so the counter still advances');
|
|
assert.strictEqual(calls.explored, null, 'a failed DELETE must not re-explore');
|
|
unstubDeleteEnv();
|
|
});
|
|
|
|
test('ramProbeParse reads AID=version lines', () => {
|
|
globalThis.t = s => s;
|
|
let r = ramProbeParse('A0000000620101=0.0\n\n# comment\n0102030405 = 1.2');
|
|
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0', '0102030405': '1.2' });
|
|
// 'all' and lower-case AIDs are accepted, normalized to upper case
|
|
r = ramProbeParse('all=0.0');
|
|
assert.deepStrictEqual(r.map, { all: '0.0' });
|
|
r = ramProbeParse('a0000000620101=1.3');
|
|
assert.deepStrictEqual(r.map, { 'A0000000620101': '1.3' });
|
|
// malformed lines and an empty field are errors
|
|
assert.ok(ramProbeParse('A0000000620101').error);
|
|
assert.ok(ramProbeParse('A0000000620101=x.y').error);
|
|
// an empty list is valid: the test then runs with the CAP's own versions
|
|
assert.deepStrictEqual(ramProbeParse(''), { map: {}, additions: [] });
|
|
assert.deepStrictEqual(ramProbeParse(' \n# only comments\n'), { map: {}, additions: [] });
|
|
// '+AID=version' appends a synthetic import (a card-capability query)
|
|
r = ramProbeParse('+A0000000090005FFFFFFFF8912000000=1.11');
|
|
assert.deepStrictEqual(r.map, {});
|
|
assert.deepStrictEqual(r.additions,
|
|
[{ aid: 'A0000000090005FFFFFFFF8912000000', version: '1.11' }]);
|
|
r = ramProbeParse('A0000000620101=0.0\n+0102030405=1.0');
|
|
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0' });
|
|
assert.strictEqual(r.additions.length, 1);
|
|
assert.ok(ramProbeParse('+all=1.0').error);
|
|
});
|
|
|
|
test('ramCompatVerdict distinguishes where the test failed', () => {
|
|
globalThis.t = s => s;
|
|
const step = (name, ok, sw) => ok
|
|
? { name: name, por_status: 'por_ok', por_sw: '9000' }
|
|
: { name: name, por_status: 'por_ok', por_sw: sw || '6438', por_error: 'remote SW ' + (sw || '6438') };
|
|
assert.strictEqual(ramCompatVerdict({ imports_ok: true, boundary_block: 3, total_blocks: 240,
|
|
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', true)] }),
|
|
'Import list accepted \u2014 tested through LOAD 3/240');
|
|
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 3, total_blocks: 240,
|
|
error: 'remote SW 6438',
|
|
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (3/240)', false)] }),
|
|
'Import list rejected \u2014 tested through LOAD 3/240 \u00b7 remote SW 6438');
|
|
// the status word name is shown when the SW map knows it (GP 6985)
|
|
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 12,
|
|
error: 'LOAD (1/12): remote SW 6985',
|
|
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', true), step('LOAD (1/12)', false, '6985')] }),
|
|
'Import list rejected \u2014 tested through LOAD 1/12 \u00b7 remote SW 6985 (Conditions of use not satisfied)');
|
|
// the card never answered the format probe
|
|
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
|
|
steps: [step('FORMAT CHECK (compact)', false), step('FORMAT CHECK (expanded)', false)] }),
|
|
'The card did not answer the format probe');
|
|
// the INSTALL [for load] request itself was refused
|
|
assert.strictEqual(ramCompatVerdict({ imports_ok: false, boundary_block: 1, total_blocks: 240,
|
|
error: 'remote SW 6985',
|
|
steps: [step('FORMAT CHECK (compact)', true), step('INSTALL [for load]', false, '6985')] }),
|
|
'Load request rejected \u2014 remote SW 6985 (Conditions of use not satisfied)');
|
|
// a request error without steps
|
|
assert.strictEqual(ramCompatVerdict({ error: 'import probe: invalid version' }),
|
|
'Error: import probe: invalid version');
|
|
assert.strictEqual(ramCompatVerdict(null), '');
|
|
});
|
|
|
|
test('ramCompatProbeNote summarizes the applied probe', () => {
|
|
globalThis.t = s => s;
|
|
assert.strictEqual(ramCompatProbeNote({ probe_imports: [
|
|
{ aid: 'A1', from: '1.3', to: '0.0' }, { aid: 'A2', from: '1.4', to: '0.0' },
|
|
{ aid: 'A3', from: null, to: '1.0' }] }),
|
|
'probe: 2 imports \u2192 0.0 \u00b7 +1 appended');
|
|
assert.strictEqual(ramCompatProbeNote({ probe_imports: [
|
|
{ aid: 'A1', from: '1.3', to: '0.0' }, { aid: 'A2', from: '1.4', to: '1.0' }] }),
|
|
'probe: 2 imports \u2192 mixed');
|
|
// the unmatched AIDs are a list of their own in the modal detail, not a
|
|
// comma string in the summary line
|
|
assert.strictEqual(ramCompatProbeNote({ probe_imports: [], probe_unmatched: ['DEADBEEF'] }), '');
|
|
assert.strictEqual(ramCompatProbeNote({}), '');
|
|
});
|
|
|
|
test('ramCompatImportRows merges the CAP imports, the probe and the unmatched lines', () => {
|
|
const mem = {
|
|
imports: [
|
|
{ aid: 'A0000000620101', major: 1, minor: 3 },
|
|
{ aid: 'A0000000620102', major: 1, minor: 3 },
|
|
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
|
|
],
|
|
};
|
|
const data = {
|
|
probe_imports: [
|
|
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
|
|
{ aid: 'A0000000620201', from: null, to: '1.0' },
|
|
],
|
|
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
|
|
};
|
|
assert.deepStrictEqual(ramCompatImportRows(data, mem), [
|
|
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
|
|
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
|
|
{ aid: 'A0000000090005FFFFFFFF8911000000', capVersion: '1.2', testedVersion: '1.2', state: 'cap' },
|
|
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
|
|
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
|
|
]);
|
|
// without the CAP analysis at hand the probe entries still produce rows
|
|
assert.deepStrictEqual(ramCompatImportRows(data, null).map(r => r.aid),
|
|
['A0000000620102', 'A0000000620201', 'A0000000090005FFFFFFFF8912000000']);
|
|
});
|
|
|
|
test('ramCompatRowsHtml lists AIDs with names, versions and probe state', () => {
|
|
globalThis.t = s => s;
|
|
const out = ramCompatRowsHtml([
|
|
{ aid: 'A0000000620101', capVersion: '1.3', testedVersion: '1.3', state: 'cap' },
|
|
{ aid: 'A0000000620102', capVersion: '1.3', testedVersion: '0.0', state: 'lowered' },
|
|
{ aid: 'A0000000620201', capVersion: null, testedVersion: '1.0', state: 'appended' },
|
|
{ aid: 'A0000000090005FFFFFFFF8912000000', capVersion: null, testedVersion: '0.0', state: 'unmatched' },
|
|
]);
|
|
delete globalThis.t;
|
|
assert.ok(out.includes('A0000000620101 <span class="text-gray-400 dark:text-slate-500">(javacard.framework)</span> \u2014 1.3'), out);
|
|
assert.ok(out.includes('1.3 \u2192 0.0 <span class="text-gray-500 dark:text-slate-400">(probe)</span>'), out);
|
|
assert.ok(out.includes('(appended \u2014 not in the CAP)'), out);
|
|
assert.ok(out.includes('(not in the CAP's import list)'), out);
|
|
assert.ok(out.includes('(uicc.toolkit)'), out);
|
|
assert.strictEqual(ramCompatRowsHtml([]), '');
|
|
});
|
|
|
|
test('ramCompatFailureHtml names the failing CAP section and the import list', () => {
|
|
globalThis.t = s => s;
|
|
const mem = {
|
|
imports: [
|
|
{ aid: 'A0000000620101', major: 1, minor: 3 },
|
|
{ aid: 'A0000000620102', major: 1, minor: 3 },
|
|
{ aid: 'A0000000871005FFFFFFFF8913200000', major: 1, minor: 9 },
|
|
{ aid: 'A0000000090005FFFFFFFF8911000000', major: 1, minor: 2 },
|
|
],
|
|
components: [
|
|
{ name: 'Header', size: 100 },
|
|
{ name: 'Directory', size: 100 },
|
|
{ name: 'Import', size: 40 },
|
|
{ name: 'Method', size: 200 },
|
|
],
|
|
code: { load_file: 440 },
|
|
};
|
|
const data = {
|
|
success: false, imports_ok: false, failed_step: 3, boundary_block: 1, total_blocks: 2,
|
|
load_block_size: 240, error: 'LOAD (1/2): remote SW 6985',
|
|
steps: [
|
|
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
|
|
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
|
|
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' },
|
|
],
|
|
probe_imports: [
|
|
{ aid: 'A0000000620102', from: '1.3', to: '0.0' },
|
|
{ aid: 'A0000000090005FFFFFFFF8911000000', from: '1.2', to: '0.0' },
|
|
{ aid: 'A0000000620201', from: null, to: '1.0' },
|
|
],
|
|
probe_unmatched: [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '0.0' }],
|
|
};
|
|
const out = ramCompatFailureHtml(data, mem);
|
|
assert.ok(out.includes('Failed in: LOAD (1/2) \u2014 load-file bytes 0\u2013239 / 440 \u00b7 covers Header, Directory, Import \u00b7 completes Import'), out);
|
|
assert.ok(out.includes("The card verifies the load file's import list when the Import component is complete (JC VM spec 4.5.2)."), out);
|
|
assert.ok(out.includes('remote SW 6985 \u2014 Conditions of use not satisfied'), out);
|
|
assert.ok(out.includes('Import list under test (6):'), out);
|
|
assert.ok(out.includes('(javacard.framework)'), out);
|
|
// two imports are still at their CAP versions -> the lower-them hint
|
|
assert.ok(out.includes('lower the versions (Set all 0.0)'), out);
|
|
assert.ok(!out.includes('Every import was tested at 0.0'), out);
|
|
// a run with every import lowered: the list cannot be the cause
|
|
const allZero = Object.assign({}, data, {
|
|
probe_imports: mem.imports.map(im => ({ aid: im.aid, from: im.major + '.' + im.minor, to: '0.0' })),
|
|
probe_unmatched: [],
|
|
});
|
|
const out2 = ramCompatFailureHtml(allZero, mem);
|
|
assert.ok(out2.includes('Every import was tested at 0.0'), out2);
|
|
// a failure later in the file: the import list already passed
|
|
const later = Object.assign({}, data, {
|
|
failed_step: 4,
|
|
steps: [
|
|
{ name: 'FORMAT CHECK (compact)', por_status: 'por_ok', por_sw: '9000' },
|
|
{ name: 'INSTALL [for load]', por_status: 'por_ok', por_sw: '9000' },
|
|
{ name: 'LOAD (1/2)', por_status: 'por_ok', por_sw: '9000' },
|
|
{ name: 'LOAD (2/2)', por_status: 'por_ok', por_sw: '6581', por_error: 'remote SW 6581' },
|
|
],
|
|
});
|
|
const out3 = ramCompatFailureHtml(later, mem);
|
|
assert.ok(out3.includes('The import list already passed \u2014 the Import component completed at LOAD (1/2).'), out3);
|
|
assert.ok(out3.includes('remote SW 6581'), out3);
|
|
assert.ok(!out3.includes('4.5.2'), out3);
|
|
// a real install run (no probe): the hint points at the compatibility test
|
|
const install = { success: false, failed_step: 3, boundary_block: 1, total_blocks: 2,
|
|
load_block_size: 240, error: 'LOAD (1/2): remote SW 6985', steps: data.steps };
|
|
const out4 = ramCompatFailureHtml(install, mem);
|
|
assert.ok(out4.includes('the CAP compatibility test can confirm it'), out4);
|
|
assert.ok(!out4.includes('lower the versions (Set all 0.0)'), out4);
|
|
assert.ok(out4.includes('Import list under test (4):'), out4);
|
|
delete globalThis.t;
|
|
// not a LOAD failure at all: no block analysis
|
|
assert.strictEqual(ramCompatFailureHtml(Object.assign({}, data, {
|
|
failed_step: 1, steps: [{ name: 'FORMAT CHECK (compact)', por_status: 'por_error' }] }), mem), '');
|
|
assert.strictEqual(ramCompatFailureHtml({ success: true, imports_ok: true }, mem), '');
|
|
});
|
|
|
|
test('ramCompatDetailHtml shows the tested list on success and the analysis on failure', () => {
|
|
globalThis.t = s => s;
|
|
const mem = { imports: [{ aid: 'A0000000620101', major: 1, minor: 3 }] };
|
|
const ok = ramCompatDetailHtml({ imports_ok: true, probe_imports: [] }, mem);
|
|
assert.ok(ok.includes('Import list under test (1):'), ok);
|
|
assert.ok(ok.includes('(javacard.framework)'), ok);
|
|
assert.ok(!ok.includes('Failed in:'), ok);
|
|
const bad = ramCompatDetailHtml({ imports_ok: false, failed_step: 1, load_block_size: 240,
|
|
steps: [{ name: 'LOAD (1/1)', por_status: 'por_ok', por_sw: '6985', por_error: 'remote SW 6985' }] }, mem);
|
|
assert.ok(bad.includes('Failed in: LOAD (1/1)'), bad);
|
|
delete globalThis.t;
|
|
assert.strictEqual(ramCompatDetailHtml(null, mem), '');
|
|
});
|
|
|
|
test('the compatibility test runs in the modal like the install', () => {
|
|
const src = extractFunc(html, 'ramCompatTest');
|
|
assert.ok(src.includes('ramOpModalBegin'), 'must open the operation modal');
|
|
assert.ok(src.includes('ramOpModalFinish'), 'must finish it with the verdict');
|
|
assert.ok(src.includes('ramCompatDetailHtml'), 'the per-AID detail must reach the modal');
|
|
assert.ok(!src.includes('ramShowProgress'), 'the inline progress bar is replaced by the modal');
|
|
});
|
|
|
|
test('a failed install carries the LOAD failure analysis into the modal', () => {
|
|
const src = extractFunc(html, 'ramShowInstallResult');
|
|
assert.ok(src.includes('ramCompatDetailHtml(data, capMem'), 'the analysis must reach the modal');
|
|
assert.ok(/ramOpModalFinish\(resultEl\.textContent, !!data\.success, modal, true\)/.test(src),
|
|
'the modal content is HTML now');
|
|
assert.ok(src.includes('ramInstallFailHint'), 'the short inline hint stays');
|
|
assert.ok(src.includes('ramNvLine'), 'the NV footprint line stays');
|
|
});
|
|
|
|
test('ramExecute dispatches the compatibility test op', () => {
|
|
const src = extractFunc(html, 'ramExecute');
|
|
assert.ok(/op === 'compat'/.test(src), 'the compat op must be dispatched');
|
|
});
|
|
|
|
test('ramRenderExploreHtml counts apps and ELFs in the section headings', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml(null,
|
|
[{ aid: 'A1', lifecycle: '07' }],
|
|
[{ aid: 'A2', lifecycle: '07' }],
|
|
[{ aid: 'E1', lifecycle: '01', moduleAids: [] }]);
|
|
delete global.t;
|
|
assert.ok(out.includes('Applications / Applet Instances (1)'), out);
|
|
assert.ok(out.includes('Executable Load Files (ELFs) / Packages (1)'), out);
|
|
// the ISD is a single occurrence (GP 11.4.2.1): no count in its heading
|
|
assert.ok(out.includes('ISD (Issuer Security Domain)'), out);
|
|
assert.ok(!out.includes('ISD (Issuer Security Domain) ('), out);
|
|
});
|
|
|
|
test('the RAM command format line explains compact vs expanded', () => {
|
|
global.t = s => s;
|
|
const out = ramRenderExploreHtml({ appCount: 1, freeNV: 1, freeV: 1 }, [], [], [], 'compact');
|
|
delete global.t;
|
|
assert.ok(out.includes('title="compact = the TS 102 226 5.2.1 command string'), out);
|
|
});
|
|
|
|
test('ramExplore runs inline, without the blocking modal', () => {
|
|
const src = extractFunc(html, 'ramExplore');
|
|
assert.ok(!/ramOpModal(Begin|Finish|Status)/.test(src),
|
|
'Explore must not use the RAM modal (installs only)');
|
|
});
|
|
|
|
test('the expanded-details checkbox is a sticky preference', () => {
|
|
const els = fakeRamDocument(['ram-expanded-details']);
|
|
const store = {};
|
|
globalThis.localStorage = {
|
|
getItem: k => (k in store ? store[k] : null),
|
|
setItem: (k, v) => { store[k] = String(v); },
|
|
};
|
|
els['ram-expanded-details'].checked = false;
|
|
ramExpandedDetailsInit();
|
|
assert.strictEqual(els['ram-expanded-details'].checked, true); // default: on (versions live there)
|
|
store['simple_ram_expanded'] = '0';
|
|
ramExpandedDetailsInit();
|
|
assert.strictEqual(els['ram-expanded-details'].checked, false); // an explicit opt-out is honoured
|
|
store['simple_ram_expanded'] = '1';
|
|
ramExpandedDetailsInit();
|
|
assert.strictEqual(els['ram-expanded-details'].checked, true); // stored choice applied
|
|
els['ram-expanded-details'].checked = false;
|
|
ramExpandedDetailsChanged();
|
|
assert.strictEqual(store['simple_ram_expanded'], '0'); // unticking stores it
|
|
delete globalThis.localStorage;
|
|
});
|
|
|
|
test('SIM toolkit mode hides the UICC file-access row', () => {
|
|
// the file-access group (checkboxes + ADF AID) is one spanning row now:
|
|
// it must follow the CA/EA visibility (v3.6.50)
|
|
const src = extractFunc(html, 'updateRcTkMode');
|
|
assert.ok(src.includes('rc-tk-access-row'),
|
|
'the access row must follow the CA/EA visibility');
|
|
});
|
|
|
|
test('ramExpandedMore marks only an unfulfilled 6310', () => {
|
|
// no promise -> nothing to report
|
|
assert.strictEqual(ramExpandedMore(false, '9000', 1), false);
|
|
// the follow-up page was accepted and ended the listing
|
|
assert.strictEqual(ramExpandedMore(true, '9000', 2), false);
|
|
// the follow-up page was refused
|
|
assert.strictEqual(ramExpandedMore(true, '6A86', 2), true);
|
|
// the page cap stopped the paging
|
|
assert.strictEqual(ramExpandedMore(true, '6310', 8), true);
|
|
// a transport failure (no remote status word) leaves it unfulfilled
|
|
assert.strictEqual(ramExpandedMore(true, 'index out of range', 2), true);
|
|
// a probe after a '61xx' page (nothing promised) never flags the listing
|
|
assert.strictEqual(ramExpandedMore(false, '6A86', 2), false);
|
|
});
|
|
|
|
test('ramPageFormats tries the expanded codings first, compact last', () => {
|
|
// the compact command string is last: a compact page attempt can consume
|
|
// the page without delivering it (the live card's 61xx + failed chain)
|
|
assert.deepStrictEqual(ramPageFormats('compact'), ['expanded', 'expanded-ae', 'compact']);
|
|
assert.deepStrictEqual(ramPageFormats('expanded'), ['expanded', 'expanded-ae', 'compact']);
|
|
assert.deepStrictEqual(ramPageFormats('expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
|
|
// the run format ('auto') is never a page format: the server's detection
|
|
// would cost an extra packet per page
|
|
assert.deepStrictEqual(ramPageFormats('auto'), ['expanded', 'expanded-ae', 'compact']);
|
|
assert.deepStrictEqual(ramPageFormats(null), ['expanded', 'expanded-ae', 'compact']);
|
|
});
|
|
|
|
test('ramAppsMissingDetails picks only the entries without expanded fields', () => {
|
|
assert.deepStrictEqual(ramAppsMissingDetails([
|
|
{ aid: 'A1', version: '1.0' },
|
|
{ aid: 'A2' },
|
|
{ aid: 'A3', sdAid: 'AA' },
|
|
{ aid: 'A4', menuEntries: [{ position: 1 }] },
|
|
{ aid: 'A5', rawTlvs: ['8F'] },
|
|
{ aid: '' },
|
|
]).map(e => e.aid), ['A2']);
|
|
assert.deepStrictEqual(ramAppsMissingDetails(null), []);
|
|
});
|