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:
2026-09-28 03:03:53 +03:00
parent daaddf21cb
commit 138760f75c
6 changed files with 129 additions and 38 deletions
+56 -9
View File
@@ -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
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v285'; const CACHE = 'simple-v286';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+38 -5
View File
@@ -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();
}); });
+32 -21
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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