Files
simple/frontend/tests/cards_iccid.test.js
T
catarrh 6360dc5bae v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4).  v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.

Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint.  The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row.  The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).

PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs).  Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.

Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
2026-10-02 08:28:46 +03:00

208 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 extractConst(src, name) {
const m = new RegExp('const\\s+' + name + '\\s*=\\s*([^;]+);').exec(src);
if (!m) throw new Error('const ' + name + ' not found');
return 'var ' + name + ' = ' + m[1] + ';\n';
}
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = '';
for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid',
'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender',
'esc', 'spKeysetList', 'spKeysetKvnOf',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar', 'cardsRoleMsl',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) {
code += extractFunc(html, fn) + '\n';
}
code += extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_ROLES')
+ extractConst(html, 'CARDS_MSL_DEFAULT');
eval(code);
// live-card style raw EF.ICCID content (nibble-swapped digits + 'F' pad)
const RAW_HEX = '980711090000640090F8';
const DIGITS = '8970119000004600098';
function fakeDoc() {
const els = { 'sp-card-sel': { value: '' }, 'ram-card-sel': { value: '' },
'tar-card-sel': { value: '' }, 'cards-iccid': { value: '' } };
globalThis.document = { getElementById: id => els[id] || null };
return els;
}
function fakeAlert() {
const seen = [];
globalThis.alert = msg => seen.push(msg);
globalThis.t = s => s;
return seen;
}
test('cardsNormIccid accepts digits, separators and raw EF hex', () => {
assert.strictEqual(cardsNormIccid(DIGITS), DIGITS);
assert.strictEqual(cardsNormIccid(' 89 70 1190-0000 4600 098 '), DIGITS);
assert.strictEqual(cardsNormIccid(RAW_HEX), DIGITS);
assert.strictEqual(cardsNormIccid(RAW_HEX.toLowerCase()), DIGITS);
assert.strictEqual(cardsNormIccid(''), '');
assert.strictEqual(cardsNormIccid(null), '');
});
test('cardsFindByIccid matches regardless of the stored form', () => {
globalThis.cards = [
{ name: 'A', iccid: '1111111111111111111' },
{ name: 'B', iccid: RAW_HEX },
{ name: 'C', iccid: '89 70 1190 0000 4600 098' },
];
assert.strictEqual(cardsFindByIccid(DIGITS), 1);
assert.strictEqual(cardsFindByIccid('1111111111111111111'), 0);
assert.strictEqual(cardsFindByIccid('2222222222222222222'), -1);
assert.strictEqual(cardsFindByIccid(''), -1);
});
test('cardsAutoSelectByIccid selects the preset in both SCP80 views', () => {
const els = fakeDoc();
const applied = [];
globalThis.cardsApply = idx => applied.push(idx);
globalThis.cards = [{ name: 'A', iccid: RAW_HEX }, { name: 'B', iccid: '' }];
globalThis._ramCardIdx = null;
globalThis._cardsAutoIccid = null;
const idx = cardsAutoSelectByIccid(DIGITS);
assert.strictEqual(idx, 0);
assert.strictEqual(els['sp-card-sel'].value, '0');
assert.strictEqual(els['ram-card-sel'].value, '0');
assert.strictEqual(els['tar-card-sel'].value, '0',
'the TAR probe view gets the equipped card\'s preset too');
assert.deepStrictEqual(applied, ['0']);
assert.strictEqual(globalThis._ramCardIdx, 0);
});
test('cardsAutoSelectByIccid does not override a manual choice for the same card', () => {
const els = fakeDoc();
const applied = [];
globalThis.cardsApply = idx => applied.push(idx);
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '' }];
globalThis._ramCardIdx = null;
globalThis._cardsAutoIccid = null;
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
// user picks the empty preset B for this card, then status polls repeat
els['sp-card-sel'].value = '1';
els['ram-card-sel'].value = '1';
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), -1);
assert.strictEqual(els['sp-card-sel'].value, '1');
assert.deepStrictEqual(applied, ['0']);
});
test('cardsAutoSelectByIccid reacts to a card swap and to unreadable ICCIDs', () => {
fakeDoc();
globalThis.cardsApply = () => {};
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '89390100000129506903' }];
globalThis._cardsAutoIccid = null;
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
assert.strictEqual(cardsAutoSelectByIccid('89390100000129506903'), 1);
// ICCID not readable: reset the guard, keep the current selection
assert.strictEqual(cardsAutoSelectByIccid(null), -1);
assert.strictEqual(globalThis._cardsAutoIccid, null);
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1);
});
test('cardsFindDuplicateIccid catches duplicates across stored formats', () => {
globalThis.cards = [
{ name: 'A', iccid: '1111111111111111111' },
{ name: 'B', iccid: RAW_HEX },
{ name: 'C', iccid: '' },
];
assert.strictEqual(cardsFindDuplicateIccid(DIGITS), 1);
assert.strictEqual(cardsFindDuplicateIccid('89 70 1190-0000 4600 098'), 1);
assert.strictEqual(cardsFindDuplicateIccid('1111111111111111111'), 0);
assert.strictEqual(cardsFindDuplicateIccid('2222222222222222222'), -1);
// empty ICCID is never a duplicate (the field is optional)
assert.strictEqual(cardsFindDuplicateIccid(''), -1);
assert.strictEqual(cardsFindDuplicateIccid(' '), -1);
});
test('cardsFindDuplicateIccid skips the row being edited', () => {
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '' }];
assert.strictEqual(cardsFindDuplicateIccid(DIGITS, 0), -1);
assert.strictEqual(cardsFindDuplicateIccid(RAW_HEX, 0), -1);
assert.strictEqual(cardsFindDuplicateIccid(RAW_HEX, 1), 0);
});
test('cardsIccidFromCard fills the field from the equipped card', () => {
const els = fakeDoc();
const seen = fakeAlert();
globalThis._pysimCardIccid = DIGITS;
cardsIccidFromCard();
assert.strictEqual(els['cards-iccid'].value, DIGITS);
assert.deepStrictEqual(seen, []);
// no readable ICCID (state raced the click): alert, leave the field alone
els['cards-iccid'].value = 'keep-me';
globalThis._pysimCardIccid = null;
cardsIccidFromCard();
assert.strictEqual(els['cards-iccid'].value, 'keep-me');
assert.deepStrictEqual(seen, ['Card equipped but its ICCID is not readable']);
});
test('the From card button and the duplicate refusal are wired into the form', () => {
assert.match(html, /<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard\(\)"/);
assert.ok(html.includes('data-l10n="From card"'));
// normalized duplicate check with the conflicting preset named
assert.ok(html.includes('cardsFindDuplicateIccid(v.iccid, _cardsEditIdx)'));
assert.ok(html.includes("t('Card with this ICCID already exists') + ': ' + (cards[dupIdx].name"));
assert.ok(html.includes("'From card': 'С карты'"));
assert.ok(html.includes("'Card with this ICCID already exists': 'Карта с таким ICCID уже есть в списке'"));
});
test('the card-state update wires the ICCID into the preset selection', () => {
assert.ok(html.includes('cardsAutoSelectByIccid(status.iccid);'));
assert.ok(html.includes('_cardsAutoIccid = null;'));
assert.ok(html.includes("getElementById('tar-card-sel')"),
'the auto-selection must cover the TAR probe view');
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
});
test('cardsRender highlights the equipped card preset', () => {
globalThis.t = s => s;
const els = { 'cards-tbody': { innerHTML: '' } };
globalThis.document = { getElementById: id => els[id] || null };
globalThis.cards = [
{ name: 'other', iccid: '1111111111111111111', keysets: [] },
{ name: 'live', iccid: DIGITS, keysets: [] },
];
globalThis._pysimCardIccid = DIGITS;
cardsRender();
let rows = els['cards-tbody'].innerHTML.split('</tr>');
assert.ok(!/bg-emerald-50/.test(rows[0]), 'the other preset must stay plain');
assert.match(rows[1], /bg-emerald-50/);
assert.match(rows[1], /The equipped card's preset/);
// the same card stored as raw EF hex matches too
globalThis.cards[1].iccid = RAW_HEX;
cardsRender();
rows = els['cards-tbody'].innerHTML.split('</tr>');
assert.match(rows[1], /bg-emerald-50/);
// no readable ICCID -> no highlight
globalThis._pysimCardIccid = null;
cardsRender();
assert.ok(!/bg-emerald-50/.test(els['cards-tbody'].innerHTML));
});