fix: Explore delete - real SPI2, no-PoR verdict, local removal (v3.6.13)
"Failed: 9000" although the delete executed: the RAM helpers sent `sp-spi2` - the base SPI2 select, whose default is "00 - No PoR" - so the card answered the envelope 9000 with no PoR and the verdict treated the missing PoR as a failure. `cardsApplyFields` also set `sp-spi2-hex` from the preset and then `updateSp()` recomputed it from the selects (which the preset never updated), clobbering the preset byte for anything reading it. - `getRamSpParams()` reads the computed `sp-spi2-hex`; `cardsApplyFields` syncs the SPI2 selects from the preset byte (base = low 5 bits, bit 0x20 = PoR via SMS-SUBMIT, dynamic option for unlisted bases) and the base select gains the missing cipher + RC/CC/DS combinations (15/19/1D). - a missing PoR is no longer a failure (the SPI may request none, and the card sometimes refuses one): `OK` with the SW, or `OK (no PoR)`. - a successful delete drops the object from the Explore result locally (no re-explore): an applet row goes away; a cascade package delete also drops the applets whose AID starts with the package AID. The consumed counter is still saved. - tests: ramRemoveFromExplorer (app/cascade/prefix), the no-PoR flow, the SPI2 source guard, and the updated delete-flow stubs. 641 frontend / 496 Python green; version 3.6.13; sw simple-v286.
This commit is contained in:
+56
-9
@@ -555,6 +555,9 @@
|
|||||||
<option value="05">05 — PoR required, RC</option>
|
<option value="05">05 — PoR required, RC</option>
|
||||||
<option value="09">09 — PoR required, CC</option>
|
<option value="09">09 — PoR required, CC</option>
|
||||||
<option value="0D">0D — PoR required, DS</option>
|
<option value="0D">0D — PoR required, DS</option>
|
||||||
|
<option value="15">15 — PoR required, RC, ciphered</option>
|
||||||
|
<option value="19">19 — PoR required, CC, ciphered</option>
|
||||||
|
<option value="1D">1D — PoR required, DS, ciphered</option>
|
||||||
<option value="11">11 — PoR required, no security, ciphered</option>
|
<option value="11">11 — PoR required, no security, ciphered</option>
|
||||||
<option value="02">02 — PoR on error, no security</option>
|
<option value="02">02 — PoR on error, no security</option>
|
||||||
<option value="06">06 — PoR on error, RC</option>
|
<option value="06">06 — PoR on error, RC</option>
|
||||||
@@ -1679,7 +1682,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.6.12';
|
const SIMPLE_VERSION = '3.6.13';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -8331,7 +8334,7 @@ function scriptsCapFileChanged() {
|
|||||||
function getRamSpParams() {
|
function getRamSpParams() {
|
||||||
return {
|
return {
|
||||||
spi1: document.getElementById('sp-spi1').value,
|
spi1: document.getElementById('sp-spi1').value,
|
||||||
spi2: document.getElementById('sp-spi2').value,
|
spi2: document.getElementById('sp-spi2-hex').value,
|
||||||
kic: document.getElementById('sp-kic-hex').value,
|
kic: document.getElementById('sp-kic-hex').value,
|
||||||
kid: document.getElementById('sp-kid-hex').value,
|
kid: document.getElementById('sp-kid-hex').value,
|
||||||
tar: (document.getElementById('sp-tar').value || '000000').replace(/[^0-9a-fA-F]/g, ''),
|
tar: (document.getElementById('sp-tar').value || '000000').replace(/[^0-9a-fA-F]/g, ''),
|
||||||
@@ -8903,6 +8906,34 @@ function ramDeleteApdu(aid, withCascade) {
|
|||||||
return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data + '00';
|
return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data + '00';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A successful delete drops the object from the Explore result locally (the
|
||||||
|
// re-explore is not re-run). The registry has no package-to-applet link, so
|
||||||
|
// a cascade package delete also drops the applets whose AID starts with the
|
||||||
|
// package AID.
|
||||||
|
function ramRemoveFromExplorer(aid, cascade) {
|
||||||
|
const d = _ramExplorerData;
|
||||||
|
if (!d) return false;
|
||||||
|
const a = (aid || '').toUpperCase();
|
||||||
|
const elfs = d.elfs || [];
|
||||||
|
const apps = d.apps || [];
|
||||||
|
const isElf = elfs.some(e => (e.aid || '').toUpperCase() === a);
|
||||||
|
const isApp = apps.some(x => (x.aid || '').toUpperCase() === a);
|
||||||
|
if (!isElf && !isApp) return false;
|
||||||
|
if (isElf) {
|
||||||
|
d.elfs = elfs.filter(e => (e.aid || '').toUpperCase() !== a);
|
||||||
|
if (cascade) {
|
||||||
|
d.apps = apps.filter(x => {
|
||||||
|
const xa = (x.aid || '').toUpperCase();
|
||||||
|
return xa !== a && !xa.startsWith(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d.apps = apps.filter(x => (x.aid || '').toUpperCase() !== a);
|
||||||
|
}
|
||||||
|
ramRenderExplorer();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function ramDeleteFromExplorer(aid, withCascade) {
|
async function ramDeleteFromExplorer(aid, withCascade) {
|
||||||
// The preset is the source of truth (see ramExecute): re-read it before
|
// The preset is the source of truth (see ramExecute): re-read it before
|
||||||
// the operation so a stale form copy cannot send an already-consumed
|
// the operation so a stale form copy cannot send an already-consumed
|
||||||
@@ -8930,7 +8961,7 @@ async function ramDeleteFromExplorer(aid, withCascade) {
|
|||||||
const resultEl = document.getElementById('ram-result');
|
const resultEl = document.getElementById('ram-result');
|
||||||
const stepsEl = document.getElementById('ram-steps');
|
const stepsEl = document.getElementById('ram-steps');
|
||||||
const sw = res.por && res.por.decoded ? res.por.decoded.last_status_word : '';
|
const sw = res.por && res.por.decoded ? res.por.decoded.last_status_word : '';
|
||||||
if (!res.success || !res.por || !spPorAccepted(res.por) ||
|
if (!res.success || !spPorAccepted(res.por) ||
|
||||||
(sw && !ramRemoteSwOk(sw))) {
|
(sw && !ramRemoteSwOk(sw))) {
|
||||||
resultEl.textContent = t('Failed') + ': ' +
|
resultEl.textContent = t('Failed') + ': ' +
|
||||||
(res.por ? res.por.response_status : (res.error || res.sw)) +
|
(res.por ? res.por.response_status : (res.error || res.sw)) +
|
||||||
@@ -8938,14 +8969,13 @@ async function ramDeleteFromExplorer(aid, withCascade) {
|
|||||||
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const removed = ramRemoveFromExplorer(aid, withCascade);
|
||||||
stepsEl.classList.remove('hidden');
|
stepsEl.classList.remove('hidden');
|
||||||
stepsEl.textContent = label + ' ' + aid + (sw ? ' -> ' + sw : '');
|
stepsEl.textContent = label + ' ' + aid + ' -> ' + (sw || t('no PoR'));
|
||||||
resultEl.textContent = t('OK');
|
resultEl.textContent = t('OK') + (removed ? '' : ' (' + t('not in the list') + ')');
|
||||||
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
|
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
|
||||||
// Continue from the counter the delete consumed: replaying the old one
|
// The record is dropped locally (no re-explore); the counter the card
|
||||||
// gets cntr_low on every page and the failed run writes it back over the
|
// consumed was saved above.
|
||||||
// preset (the counter reset reported after Delete All).
|
|
||||||
await ramExplore(sp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One RAM install step line: the PoR verdict plus the remote command's
|
// One RAM install step line: the PoR verdict plus the remote command's
|
||||||
@@ -9512,6 +9542,21 @@ function cardsApplyFields(idx) {
|
|||||||
const c = cards[parseInt(idx)];
|
const c = cards[parseInt(idx)];
|
||||||
if (!c) return false;
|
if (!c) return false;
|
||||||
document.getElementById('sp-spi1').value = c.spi1;
|
document.getElementById('sp-spi1').value = c.spi1;
|
||||||
|
// Sync the SPI2 selects with the preset byte (low 5 bits = PoR/security,
|
||||||
|
// bit 0x20 = PoR via SMS-SUBMIT): updateSp() recomputes sp-spi2-hex from
|
||||||
|
// them, so without this the preset value was clobbered on every refresh.
|
||||||
|
const spi2Val = parseInt(c.spi2, 16) || 0;
|
||||||
|
const baseHex = (spi2Val & 0x1F).toString(16).padStart(2, '0').toUpperCase();
|
||||||
|
const baseSel = document.getElementById('sp-spi2');
|
||||||
|
if (baseSel) {
|
||||||
|
if (baseSel.options && typeof Option === 'function' &&
|
||||||
|
!Array.prototype.some.call(baseSel.options, o => o.value === baseHex)) {
|
||||||
|
baseSel.appendChild(new Option(baseHex + ' — preset', baseHex));
|
||||||
|
}
|
||||||
|
baseSel.value = baseHex;
|
||||||
|
}
|
||||||
|
const smSel = document.getElementById('sp-spi2-sm');
|
||||||
|
if (smSel) smSel.value = String((spi2Val >> 5) & 1);
|
||||||
document.getElementById('sp-spi2-hex').value = c.spi2;
|
document.getElementById('sp-spi2-hex').value = c.spi2;
|
||||||
const kicByte = parseInt(c.kic, 16);
|
const kicByte = parseInt(c.kic, 16);
|
||||||
document.getElementById('sp-kic-idx').value = ((kicByte >> 4) & 0x0F).toString(16).toUpperCase();
|
document.getElementById('sp-kic-idx').value = ((kicByte >> 4) & 0x0F).toString(16).toUpperCase();
|
||||||
@@ -16537,6 +16582,8 @@ const LANG_RU = {
|
|||||||
'no data': 'нет данных',
|
'no data': 'нет данных',
|
||||||
'(no data)': '(нет данных)',
|
'(no data)': '(нет данных)',
|
||||||
'response via SMS not captured': 'ответ по SMS не перехвачен',
|
'response via SMS not captured': 'ответ по SMS не перехвачен',
|
||||||
|
'no PoR': 'нет PoR',
|
||||||
|
'not in the list': 'нет в списке',
|
||||||
'free NV': 'свободно NV',
|
'free NV': 'свободно NV',
|
||||||
'Failed': 'Ошибка',
|
'Failed': 'Ошибка',
|
||||||
'cascade': 'каскадно',
|
'cascade': 'каскадно',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v285';
|
const CACHE = 'simple-v286';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
|
|||||||
|
|
||||||
// Extract chain builder functions and dependencies
|
// Extract chain builder functions and dependencies
|
||||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
||||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2',
|
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer',
|
||||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
||||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
||||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||||
@@ -440,6 +440,33 @@ test('ramRemoteSwOk mirrors the server success set', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
|
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
|
||||||
// Apps (40) and ELF (20/10) listings can exceed the ENVELOPE response;
|
// 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
|
// with SPI2=0x01 the card answers actual_response_sms_submit and the data
|
||||||
@@ -451,7 +478,7 @@ test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () =>
|
|||||||
function stubDeleteEnv(sendResult) {
|
function stubDeleteEnv(sendResult) {
|
||||||
// ramShowProgress/ramHideProgress are the real extracted helpers
|
// ramShowProgress/ramHideProgress are the real extracted helpers
|
||||||
const els = fakeRamDocument(['ram-result', 'ram-steps', 'ram-progress', 'ram-progress-text']);
|
const els = fakeRamDocument(['ram-result', 'ram-steps', 'ram-progress', 'ram-progress-text']);
|
||||||
const calls = { refresh: [], saved: [], sent: [], explored: null };
|
const calls = { refresh: [], saved: [], sent: [], explored: null, removed: null };
|
||||||
globalThis.t = s => s;
|
globalThis.t = s => s;
|
||||||
globalThis.spRefreshFromPreset = sel => { calls.refresh.push(sel); };
|
globalThis.spRefreshFromPreset = sel => { calls.refresh.push(sel); };
|
||||||
globalThis.getRamSpParams = () => ({ cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' });
|
globalThis.getRamSpParams = () => ({ cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' });
|
||||||
@@ -475,10 +502,13 @@ function unstubDeleteEnv() {
|
|||||||
globalThis.spRefreshFromPreset = () => ''; // the top-level stub
|
globalThis.spRefreshFromPreset = () => ''; // the top-level stub
|
||||||
}
|
}
|
||||||
|
|
||||||
test('ramDeleteFromExplorer refreshes the preset and continues from the consumed counter', async () => {
|
test('ramDeleteFromExplorer refreshes the preset and drops the record locally', async () => {
|
||||||
const { els, calls } = stubDeleteEnv({ success: true,
|
const { els, calls } = stubDeleteEnv({ success: true,
|
||||||
por: { response_status: 'por_ok', decoded: { last_status_word: '9000' } } });
|
por: { response_status: 'por_ok', decoded: { last_status_word: '9000' } } });
|
||||||
|
_ramExplorerData = { apps: [{ aid: 'F0414C4641610101' }], elfs: [{ aid: 'F0414C46416101' }] };
|
||||||
|
globalThis.ramRenderExplorer = () => {};
|
||||||
await ramDeleteFromExplorer('F0414C46416101', true);
|
await ramDeleteFromExplorer('F0414C46416101', true);
|
||||||
|
delete globalThis.ramRenderExplorer;
|
||||||
assert.deepStrictEqual(calls.refresh, ['ram-card-sel'],
|
assert.deepStrictEqual(calls.refresh, ['ram-card-sel'],
|
||||||
'the preset must be re-read before the operation');
|
'the preset must be re-read before the operation');
|
||||||
assert.strictEqual(calls.sent.length, 1);
|
assert.strictEqual(calls.sent.length, 1);
|
||||||
@@ -486,8 +516,11 @@ test('ramDeleteFromExplorer refreshes the preset and continues from the consumed
|
|||||||
assert.strictEqual(calls.sent[0].cntr, '0000000005');
|
assert.strictEqual(calls.sent[0].cntr, '0000000005');
|
||||||
assert.deepStrictEqual(calls.saved, ['0000000006'],
|
assert.deepStrictEqual(calls.saved, ['0000000006'],
|
||||||
'the accepted packet advances the saved counter');
|
'the accepted packet advances the saved counter');
|
||||||
assert.strictEqual(calls.explored, '0000000006',
|
assert.deepStrictEqual(_ramExplorerData.elfs, [],
|
||||||
'the re-explore must start from the consumed counter, never replay it');
|
'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');
|
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
||||||
unstubDeleteEnv();
|
unstubDeleteEnv();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ function extractFunc(src, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The real functions behind the Explore Delete flow: the counter the card
|
// The real functions behind the Explore Delete flow: the counter the card
|
||||||
// consumed must be persisted into the preset, or the next operation starts
|
// consumed must be persisted into the preset, a successful delete drops the
|
||||||
// one behind and is rejected with cntr_low.
|
// record locally (no re-explore), and a delete with no PoR (the SPI may
|
||||||
|
// request none) is not a failure - live 2026-09-28 showed "Failed: 9000"
|
||||||
|
// although the delete executed.
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
|
for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr',
|
||||||
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
'getRamSpParams', 'spPorAccepted', 'ramRemoteSwOk', 'ramShowProgress',
|
||||||
@@ -36,7 +38,7 @@ function fakeEnv(por, success) {
|
|||||||
const els = {};
|
const els = {};
|
||||||
const mk = () => ({ value: '', textContent: '',
|
const mk = () => ({ value: '', textContent: '',
|
||||||
classList: { add() {}, remove() {}, toggle() {} } });
|
classList: { add() {}, remove() {}, toggle() {} } });
|
||||||
for (const id of ['sp-spi1', 'sp-spi2', 'sp-kic-hex', 'sp-kid-hex', 'sp-tar',
|
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-hex', 'sp-kid-hex', 'sp-tar',
|
||||||
'sp-cntr', 'sp-kic-key', 'sp-kid-key', 'ram-result', 'ram-steps',
|
'sp-cntr', 'sp-kic-key', 'sp-kid-key', 'ram-result', 'ram-steps',
|
||||||
'ram-progress', 'ram-progress-text']) els[id] = mk();
|
'ram-progress', 'ram-progress-text']) els[id] = mk();
|
||||||
els['ram-card-sel'] = { value: '0' };
|
els['ram-card-sel'] = { value: '0' };
|
||||||
@@ -44,74 +46,83 @@ function fakeEnv(por, success) {
|
|||||||
// the form (the delete flow aborts without them)
|
// the form (the delete flow aborts without them)
|
||||||
els['sp-kic-key'].value = 'AA';
|
els['sp-kic-key'].value = 'AA';
|
||||||
els['sp-kid-key'].value = 'BB';
|
els['sp-kid-key'].value = 'BB';
|
||||||
|
els['sp-spi2-hex'].value = '01';
|
||||||
globalThis.document = { getElementById: id => els[id] || null };
|
globalThis.document = { getElementById: id => els[id] || null };
|
||||||
globalThis.cards = [{ name: 'C', cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' }];
|
globalThis.cards = [{ name: 'C', cntr: '0000000005', kicKey: 'AA', kidKey: 'BB' }];
|
||||||
const calls = { saved: 0, explored: null, sent: null };
|
const calls = { saved: 0, explored: null, removed: null, sent: null };
|
||||||
globalThis.cardsSave = () => { calls.saved++; };
|
globalThis.cardsSave = () => { calls.saved++; };
|
||||||
globalThis.cardsRender = () => {};
|
globalThis.cardsRender = () => {};
|
||||||
globalThis.ramRender = () => {};
|
globalThis.ramRender = () => {};
|
||||||
globalThis.t = s => s;
|
globalThis.t = s => s;
|
||||||
globalThis.alert = () => {};
|
globalThis.alert = () => {};
|
||||||
globalThis.confirm = () => true;
|
globalThis.confirm = () => true;
|
||||||
// the preset re-read (cardsApply -> the sp-* form fields); the real
|
|
||||||
// spRefreshFromPreset is covered by cards_counter.test.js
|
|
||||||
globalThis.spRefreshFromPreset = () => {
|
globalThis.spRefreshFromPreset = () => {
|
||||||
els['sp-cntr'].value = cards[0].cntr;
|
els['sp-cntr'].value = cards[0].cntr;
|
||||||
return cards[0].cntr;
|
return cards[0].cntr;
|
||||||
};
|
};
|
||||||
globalThis.ramSendOta = async (apdu, sp) => {
|
globalThis.ramSendOta = async (apdu, sp) => {
|
||||||
calls.sent = { apdu: apdu, cntr: sp.cntr };
|
calls.sent = { apdu: apdu, cntr: sp.cntr, spi2: sp.spi2 };
|
||||||
return { success: success !== false, por: por };
|
return { success: success !== false, por: por };
|
||||||
};
|
};
|
||||||
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
|
globalThis.ramExplore = async sp => { calls.explored = sp.cntr; };
|
||||||
|
globalThis.ramRemoveFromExplorer = (aid, cascade) => {
|
||||||
|
calls.removed = { aid: aid, cascade: cascade };
|
||||||
|
return true;
|
||||||
|
};
|
||||||
return { els, calls };
|
return { els, calls };
|
||||||
}
|
}
|
||||||
|
|
||||||
test('accepted delete persists the consumed counter and re-explores from it', async () => {
|
test('accepted delete persists the consumed counter and drops the record', async () => {
|
||||||
const { els, calls } = fakeEnv({ response_status: 'por_ok',
|
const { els, calls } = fakeEnv({ response_status: 'por_ok',
|
||||||
decoded: { last_status_word: '9000' } });
|
decoded: { last_status_word: '9000' } });
|
||||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||||
assert.strictEqual(calls.sent.cntr, '0000000005');
|
assert.strictEqual(calls.sent.cntr, '0000000005');
|
||||||
assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C4641610100');
|
assert.strictEqual(calls.sent.apdu, '80E40000094F07F0414C4641610100');
|
||||||
|
assert.strictEqual(calls.sent.spi2, '01', 'the computed SPI2 byte must be used');
|
||||||
assert.strictEqual(cards[0].cntr, '0000000006',
|
assert.strictEqual(cards[0].cntr, '0000000006',
|
||||||
'the preset must carry the counter the card consumed');
|
'the preset must carry the counter the card consumed');
|
||||||
assert.strictEqual(els['sp-cntr'].value, '0000000006');
|
assert.strictEqual(els['sp-cntr'].value, '0000000006');
|
||||||
assert.ok(calls.saved > 0, 'cardsSave() must persist it');
|
assert.ok(calls.saved > 0, 'cardsSave() must persist it');
|
||||||
assert.strictEqual(calls.explored, '0000000006',
|
assert.deepStrictEqual(calls.removed, { aid: 'F0414C46416101', cascade: false });
|
||||||
'the re-explore must start at N+1, never replay N');
|
assert.strictEqual(calls.explored, null, 'no re-explore: the record is dropped locally');
|
||||||
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a delete with no PoR is accepted (the envelope 9000 is the result)', async () => {
|
||||||
|
const { els, calls } = fakeEnv(undefined);
|
||||||
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||||
|
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||||
|
assert.deepStrictEqual(calls.removed, { aid: 'F0414C46416101', cascade: false });
|
||||||
|
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
||||||
|
assert.ok(els['ram-steps'].textContent.includes('no PoR'), els['ram-steps'].textContent);
|
||||||
|
});
|
||||||
|
|
||||||
test('cntr_low leaves the preset untouched (the card did not consume the packet)', async () => {
|
test('cntr_low leaves the preset untouched (the card did not consume the packet)', async () => {
|
||||||
const { calls } = fakeEnv({ response_status: 'cntr_low' });
|
const { calls } = fakeEnv({ response_status: 'cntr_low' });
|
||||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||||
assert.strictEqual(cards[0].cntr, '0000000005');
|
assert.strictEqual(cards[0].cntr, '0000000005');
|
||||||
assert.strictEqual(calls.saved, 0);
|
assert.strictEqual(calls.saved, 0);
|
||||||
assert.strictEqual(calls.explored, null);
|
assert.strictEqual(calls.removed, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a refused DELETE still advances the counter but does not re-explore', async () => {
|
test('a refused DELETE still advances the counter but drops nothing', async () => {
|
||||||
const { calls } = fakeEnv({ response_status: 'por_ok',
|
const { calls } = fakeEnv({ response_status: 'por_ok',
|
||||||
decoded: { last_status_word: '6A88' } });
|
decoded: { last_status_word: '6A88' } });
|
||||||
await ramDeleteFromExplorer('F0414C46416101', true);
|
await ramDeleteFromExplorer('F0414C46416101', true);
|
||||||
assert.strictEqual(cards[0].cntr, '0000000006');
|
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||||
assert.strictEqual(calls.explored, null);
|
assert.strictEqual(calls.removed, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a delete accepted via actual_response_sms_submit advances and re-explores', async () => {
|
test('a delete accepted via actual_response_sms_submit advances and drops the record', async () => {
|
||||||
// The card consumed the packet and will deliver the remote result as an
|
const { calls } = fakeEnv({ response_status: 'actual_response_sms_submit' });
|
||||||
// SMS-SUBMIT; the counter must advance and the re-explore must run (the
|
|
||||||
// remote SW is unknown, so no SW is shown).
|
|
||||||
const { els, calls } = fakeEnv({ response_status: 'actual_response_sms_submit' });
|
|
||||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||||
assert.strictEqual(cards[0].cntr, '0000000006');
|
assert.strictEqual(cards[0].cntr, '0000000006');
|
||||||
assert.strictEqual(calls.explored, '0000000006');
|
assert.deepStrictEqual(calls.removed, { aid: 'F0414C46416101', cascade: false });
|
||||||
assert.strictEqual(els['ram-result'].textContent, 'OK');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a send failure leaves the preset untouched', async () => {
|
test('a send failure leaves the preset untouched', async () => {
|
||||||
const { calls } = fakeEnv({ response_status: 'por_ok' }, false);
|
const { calls } = fakeEnv({ response_status: 'por_ok' }, false);
|
||||||
await ramDeleteFromExplorer('F0414C46416101', false);
|
await ramDeleteFromExplorer('F0414C46416101', false);
|
||||||
assert.strictEqual(cards[0].cntr, '0000000005');
|
assert.strictEqual(cards[0].cntr, '0000000005');
|
||||||
assert.strictEqual(calls.explored, null);
|
assert.strictEqual(calls.removed, null);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.12"
|
version = "3.6.13"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.6.12'
|
VERSION = '3.6.13'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user