v3.19.0: the applet-agent script batch - por objects, alpha/sms checks, scp80 format
The test scripts get the pieces an applet test needed:
- scp80 check.por accepts an object {status?, sw?, data?} asserting the
decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
the evaluation is shared with the expect-step por check
(_por_check_results). The editor's PoR select gains 'custom' with the
three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
Annex A coded) - the text check stays bound to the 8D Text string, so a
command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
file-write is documented as a direct APDU that does not fire
EVENT_REMOTE_FILE_UPDATE.
Tests: 819 frontend + 724 Python; sw cache simple-v387.
This commit is contained in:
@@ -563,8 +563,8 @@
|
||||
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Тест-скрипт</h3>
|
||||
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> — упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP 409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <strong>Очистить отчёт</strong>. Скрипты хранятся в <strong>хранилище на сервере</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки «Сохранить» нет — черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — поля редактируются как объект JSON в редакторе шагов, а для остальных событий форма события на вкладке «Телефон» показывает готовый hex), <em>Menu Selection</em> (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em>).</li>
|
||||
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
||||
<li><strong>Действия</strong> — <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника — 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: Location status, Access technology change, Network rejection, Data connection status change — поля редактируются как объект JSON в редакторе шагов, а для остальных событий форма события на вкладке «Телефон» показывает готовый hex), <em>Menu Selection</em> (item id 1–255 или текст пункта с точным/частичным совпадением — текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды — компактный или обёрнутый в шаблон Command Scripting TS 102 226 §5.2.1 <em>expanded</em> (AA) / <em>expanded AE 80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты — запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик — а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> > 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> — <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR — статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
|
||||
<li><strong>Ожидания</strong> — карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); <code class="font-mono text-sm">9000</code> означает «команды нет» и считается ошибкой — добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные — для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
|
||||
<li><strong>Уровни ошибок</strong> — несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.</li>
|
||||
<li><strong>Шаблоны</strong> — Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный (поля действия Event вводятся объектом JSON), отдельные скрипты экспортируются/импортируются как JSON.</li>
|
||||
</ul>
|
||||
|
||||
+2
-2
@@ -562,8 +562,8 @@
|
||||
<h3 id="test-script" class="text-lg font-medium mb-2">8.9 Test script</h3>
|
||||
<p class="text-sm mb-2">A <strong>test script</strong> is an ordered list of <strong>actions</strong> and proactive-command <strong>expectations</strong> executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP 409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until <strong>Clear report</strong>. Scripts live in the <strong>server-side store</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), so every browser manages the same set; edits are written to the store as you change them (there is no Save button — a draft without a valid step stays local until it is valid); JSON export/import remains for backup and for moving scripts between benches (the old localStorage export imports as-is).</p>
|
||||
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
|
||||
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or <em>Event</em> (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change — the fields are edited as a JSON object in the step editor, and the Phone tab's event form shows the built hex for the other events), <em>Menu Selection</em> (item id 1–255, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>).</li>
|
||||
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
||||
<li><strong>Actions</strong> — <em>Envelope</em> (Event Download: raw event type + data, with an optional source override — 82 terminal / 83 network) or <em>Event</em> (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change — the fields are edited as a JSON object in the step editor, and the Phone tab's event form shows the built hex for the other events), <em>Menu Selection</em> (item id 1–255, or the item's text with an exact/contains match — the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format — compact, or the TS 102 226 §5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE 80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID — the run names the stored preset, so it always uses its keysets, TARs and counter — and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> > 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards — <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR — status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
|
||||
<li><strong>Expectations</strong> — the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 §7.4.2.1 / TS 102 223 §6.3); a <code class="font-mono text-sm">9000</code> means “no command” and is an error — add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> — distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data — for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
|
||||
<li><strong>Fail levels</strong> — a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); each check is reported with expected vs actual values.</li>
|
||||
<li><strong>Templates</strong> — Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based (the event action's fields are a JSON object) and individual scripts export/import as JSON.</li>
|
||||
</ul>
|
||||
|
||||
+88
-13
@@ -1877,7 +1877,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.18.4';
|
||||
const SIMPLE_VERSION = '3.19.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -7040,9 +7040,18 @@ function testScriptProblem(script) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function testPorSpecSummary(por) {
|
||||
const parts = [];
|
||||
if (por.status) parts.push('status ' + por.status);
|
||||
if (por.sw) parts.push('SW ' + (por.sw.mode === 'mask' ? '~' : '') + por.sw.value);
|
||||
if (por.data) parts.push('data ' + (por.data.mode === 'mask' ? '~' : '') + por.data.value);
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function testCheckSummary(c) {
|
||||
if (!c || !c.kind) return '';
|
||||
if (c.kind === 'text') return 'text ' + (c.mode === 'exact' ? '=' : '~') + JSON.stringify(c.value || '');
|
||||
if (c.kind === 'alpha') return 'alpha ' + (c.mode === 'exact' ? '=' : '~') + JSON.stringify(c.value || '');
|
||||
if (c.kind === 'item') {
|
||||
let s = 'item';
|
||||
if (c.id) s += ' #' + c.id;
|
||||
@@ -7050,12 +7059,15 @@ function testCheckSummary(c) {
|
||||
return s;
|
||||
}
|
||||
if (c.kind === 'raw') return 'raw ' + (c.mode === 'mask' ? '~' : '=') + (c.value || '');
|
||||
if (c.kind === 'por') {
|
||||
if (c.kind === 'por') return 'por ' + testPorSpecSummary(c);
|
||||
if (c.kind === 'sms') {
|
||||
const parts = [];
|
||||
if (c.status) parts.push('status ' + c.status);
|
||||
if (c.sw) parts.push('SW ' + (c.sw.mode === 'mask' ? '~' : '') + c.sw.value);
|
||||
if (c.data) parts.push('data ' + (c.data.mode === 'mask' ? '~' : '') + c.data.value);
|
||||
return 'por ' + parts.join(' ');
|
||||
if (c.da) parts.push('DA ' + c.da);
|
||||
if (c.pid) parts.push('PID ' + (c.pid.mode === 'mask' ? '~' : '') + c.pid.value);
|
||||
if (c.dcs) parts.push('DCS ' + (c.dcs.mode === 'mask' ? '~' : '') + c.dcs.value);
|
||||
if (c.udl !== undefined) parts.push('UDL ' + c.udl);
|
||||
if (c.ud) parts.push('UD ' + (c.ud.mode === 'mask' ? '~' : '') + c.ud.value);
|
||||
return 'sms ' + parts.join(' ');
|
||||
}
|
||||
if (c.kind === 'files') return 'files ' + (c.files || []).join(' ');
|
||||
return '';
|
||||
@@ -7090,7 +7102,7 @@ function testStepSummary(step) {
|
||||
s = 'EVENT ' + (EVENT_NAMES[code] || ('0x' + String(p.event || '?')));
|
||||
}
|
||||
else if (step.kind === 'apdu') s = 'APDU ' + (p.apdu || '?');
|
||||
else if (step.kind === 'scp80') s = 'SCP80 ' + ((p.source || (p.sp ? 'sp' : 'apdu')) === 'sp' ? (p.sp || '(secured packet)') : (p.apdu || '?')) + (p.tar ? ' TAR=' + p.tar : '') + (p.spi1 ? ' SPI1=' + p.spi1 : '') + (p.spi2 ? ' SPI2=' + p.spi2 : '');
|
||||
else if (step.kind === 'scp80') s = 'SCP80 ' + ((p.source || (p.sp ? 'sp' : 'apdu')) === 'sp' ? (p.sp || '(secured packet)') : (p.apdu || '?')) + (p.tar ? ' TAR=' + p.tar : '') + (p.spi1 ? ' SPI1=' + p.spi1 : '') + (p.spi2 ? ' SPI2=' + p.spi2 : '') + (p.format && p.format !== 'compact' ? ' fmt=' + p.format : '');
|
||||
else if (step.kind === 'file-write') s = 'UPDATE ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '') + ' = ' + (p.data || '');
|
||||
else if (step.kind === 'file-read') s = 'READ ' + (p.path || '?') + (p.mode && p.mode !== 'auto' ? ' (' + p.mode + (p.record ? ' ' + p.record : '') + ')' : '');
|
||||
else if (step.kind === 'status') s = 'STATUS' + ((p.attempts || 1) > 1 ? ' x' + p.attempts + ' (poll)' : '');
|
||||
@@ -7099,7 +7111,9 @@ function testStepSummary(step) {
|
||||
const ck = [];
|
||||
if (c.sw && c.sw.value && !(c.sw.mode !== 'mask' && c.sw.value === '9000')) ck.push('SW ' + (c.sw.mode === 'mask' ? '~' : '') + c.sw.value);
|
||||
if (c.data && c.data.value) ck.push('data ' + (c.data.mode === 'mask' ? '~' : '') + c.data.value);
|
||||
if (c.por && c.por !== 'any') ck.push('PoR ' + c.por);
|
||||
if (c.por && c.por !== 'any') {
|
||||
ck.push('PoR ' + (typeof c.por === 'string' ? c.por : testPorSpecSummary(c.por)));
|
||||
}
|
||||
if (step.on_fail === 'warning') ck.push('⚠');
|
||||
return s + (ck.length ? ' · ' + ck.join(', ') : '');
|
||||
}
|
||||
@@ -7390,7 +7404,13 @@ function testStepRender() {
|
||||
html += testFormRow('Source', testFormSelect('test-f-src', src, [
|
||||
{v: 'apdu', l: t('C-APDU (built with the preset)')}, {v: 'sp', l: t('Pre-built secured packet')}], 'testStepKindChanged()'));
|
||||
if (src === 'sp') html += testFormRow('Secured packet (hex)', testFormInput('test-f-sp', p.sp, '01 00 ...'));
|
||||
else html += testFormRow('C-APDU (hex)', testFormInput('test-f-apdu', p.apdu, '80E2900000'));
|
||||
else {
|
||||
html += testFormRow('C-APDU (hex)', testFormInput('test-f-apdu', p.apdu, '80E2900000'));
|
||||
html += testFormRow('Command format', testFormSelect('test-f-fmt', p.format || 'compact', [
|
||||
{v: 'compact', l: t('Compact')},
|
||||
{v: 'expanded', l: t('Expanded (AA template)')},
|
||||
{v: 'expanded-ae', l: t('Expanded (AE 80 template)')}]));
|
||||
}
|
||||
html += testFormRow('Keyset number (optional)',
|
||||
testFormInput('test-f-kvn', p.kvn === undefined ? '' : p.kvn, t('empty = the preset\'s first keyset')));
|
||||
html += testFormRow('TAR override (hex, optional)', testFormInput('test-f-tar', p.tar, 'B00000'));
|
||||
@@ -7410,8 +7430,15 @@ function testStepRender() {
|
||||
+ testFormSelect('test-f-cdata-mode', cd.mode || 'exact', [{v: 'exact', l: t('Exact')}, {v: 'mask', l: t('Mask ("?" = wildcard)')}])
|
||||
+ testFormInput('test-f-cdata', cd.value, 'AA??') + '</div>');
|
||||
if (step.kind === 'scp80') {
|
||||
html += testFormRow('PoR check', testFormSelect('test-f-por', c.por || 'any', [
|
||||
{v: 'any', l: t('Any')}, {v: 'none', l: t('No PoR expected')}, {v: 'ok', l: t('PoR = por_ok')}]));
|
||||
const porObj = c.por && typeof c.por === 'object' ? c.por : null;
|
||||
html += testFormRow('PoR check', testFormSelect('test-f-por', porObj ? 'custom' : (c.por || 'any'), [
|
||||
{v: 'any', l: t('Any')}, {v: 'none', l: t('No PoR expected')}, {v: 'ok', l: t('PoR = por_ok')},
|
||||
{v: 'custom', l: t('Custom (status / SW / data)')}], 'testStepKindChanged()'));
|
||||
if (porObj) {
|
||||
html += testFormRow('PoR status', testFormInput('test-f-porstatus', porObj.status, 'por_ok'));
|
||||
html += testFormRow('PoR SW (hex)', testFormInput('test-f-por-sw', porObj.sw && porObj.sw.value, '6A88'));
|
||||
html += testFormRow('PoR data (hex)', testFormInput('test-f-por-data', porObj.data && porObj.data.value, 'AA??'));
|
||||
}
|
||||
}
|
||||
html += testFormRow('On mismatch', testFormSelect('test-f-fail', step.on_fail || 'error', [
|
||||
{v: 'error', l: t('Error (stop the script)')}, {v: 'warning', l: t('Warning (continue)')}]));
|
||||
@@ -7478,7 +7505,8 @@ function testRenderChecks() {
|
||||
html += '<div class="flex items-start gap-2 mb-1">'
|
||||
+ testFormSelect('test-check-kind-' + i, c.kind || 'text', [
|
||||
{v: 'text', l: t('Text')}, {v: 'item', l: t('Item')}, {v: 'raw', l: t('Raw')},
|
||||
{v: 'por', l: t('PoR')}, {v: 'files', l: t('Files')}], 'testCheckKindChanged()');
|
||||
{v: 'alpha', l: t('Alpha')}, {v: 'por', l: t('PoR')}, {v: 'sms', l: t('SMS TPDU')},
|
||||
{v: 'files', l: t('Files')}], 'testCheckKindChanged()');
|
||||
if (c.kind === 'item') {
|
||||
html += testFormInput('test-check-id-' + i, c.id, t('id'));
|
||||
html += testFormInput('test-check-text-' + i, c.text, t('text'));
|
||||
@@ -7491,6 +7519,12 @@ function testRenderChecks() {
|
||||
html += testFormInput('test-check-porstatus-' + i, c.status, 'por_ok');
|
||||
html += testFormInput('test-check-por-sw-' + i, c.sw && c.sw.value, '6A88');
|
||||
html += testFormInput('test-check-por-data-' + i, c.data && c.data.value, 'AA??');
|
||||
} else if (c.kind === 'sms') {
|
||||
html += testFormInput('test-check-smsda-' + i, c.da, '12345');
|
||||
html += testFormInput('test-check-smspid-' + i, c.pid && c.pid.value, '7F');
|
||||
html += testFormInput('test-check-smsdcs-' + i, c.dcs && c.dcs.value, '00');
|
||||
html += testFormInput('test-check-smsudl-' + i, c.udl === undefined ? '' : c.udl, 'UDL');
|
||||
html += testFormInput('test-check-smsud-' + i, c.ud && c.ud.value, 'AA??');
|
||||
} else if (c.kind === 'files') {
|
||||
html += testFormInput('test-check-files-' + i, (c.files || []).join(', '), '3F007F106F3A');
|
||||
} else {
|
||||
@@ -7544,6 +7578,17 @@ function testChecksCollect() {
|
||||
if (sw) row.sw = {mode: sw.includes('?') ? 'mask' : 'exact', value: sw};
|
||||
const data = val('test-check-por-data-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (data) row.data = {mode: data.includes('?') ? 'mask' : 'exact', value: data};
|
||||
} else if (kind === 'sms') {
|
||||
const da = val('test-check-smsda-' + i);
|
||||
if (da) row.da = da;
|
||||
const pid = val('test-check-smspid-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (pid) row.pid = {mode: pid.includes('?') ? 'mask' : 'exact', value: pid};
|
||||
const dcs = val('test-check-smsdcs-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (dcs) row.dcs = {mode: dcs.includes('?') ? 'mask' : 'exact', value: dcs};
|
||||
const udl = val('test-check-smsudl-' + i);
|
||||
if (udl !== '' && !isNaN(parseInt(udl, 10))) row.udl = parseInt(udl, 10);
|
||||
const ud = val('test-check-smsud-' + i).replace(/[^0-9a-fA-F?]/g, '').toUpperCase();
|
||||
if (ud) row.ud = {mode: ud.includes('?') ? 'mask' : 'exact', value: ud};
|
||||
} else if (kind === 'files') {
|
||||
row.files = val('test-check-files-' + i).split(/[,\s]+/)
|
||||
.map(s => s.replace(/[^0-9a-fA-F]/g, '').toUpperCase()).filter(Boolean);
|
||||
@@ -7621,6 +7666,8 @@ function testStepCollect() {
|
||||
['tar', 'spi1', 'spi2'].forEach(k => { const v = hexNoMask('test-f-' + k); if (v) p[k] = v; });
|
||||
const kvn = (document.getElementById('test-f-kvn') || {}).value;
|
||||
if (kvn !== undefined && String(kvn).trim() !== '') p.kvn = parseInt(String(kvn).trim(), 10);
|
||||
const fmt = val('test-f-fmt');
|
||||
if (fmt && fmt !== 'compact') p.format = fmt;
|
||||
}
|
||||
else if (kind === 'status') { p.attempts = num('test-f-attempts', 1); p.interval_ms = num('test-f-interval', 200); }
|
||||
step.params = p;
|
||||
@@ -7630,7 +7677,19 @@ function testStepCollect() {
|
||||
const dataVal = hex('test-f-cdata');
|
||||
if (dataVal) check.data = {mode: val('test-f-cdata-mode') || 'exact', value: dataVal};
|
||||
const por = val('test-f-por');
|
||||
if (kind === 'scp80' && por && por !== 'any') check.por = por;
|
||||
if (kind === 'scp80' && por === 'custom') {
|
||||
// the decoded-PoR assertion: status / SW / data, at least one
|
||||
const spec = {};
|
||||
const st = val('test-f-porstatus');
|
||||
if (st) spec.status = st;
|
||||
const psw = hex('test-f-por-sw');
|
||||
if (psw) spec.sw = {mode: psw.includes('?') ? 'mask' : 'exact', value: psw};
|
||||
const pdata = hex('test-f-por-data');
|
||||
if (pdata) spec.data = {mode: pdata.includes('?') ? 'mask' : 'exact', value: pdata};
|
||||
check.por = spec;
|
||||
} else if (kind === 'scp80' && por && por !== 'any') {
|
||||
check.por = por;
|
||||
}
|
||||
step.check = check;
|
||||
step.on_fail = val('test-f-fail') || 'error';
|
||||
const label = val('test-f-label');
|
||||
@@ -7645,6 +7704,7 @@ function testStepCollect() {
|
||||
if (c.kind === 'text') return !!(c.value || '').trim();
|
||||
if (c.kind === 'item') return c.id || (c.text || '').trim();
|
||||
if (c.kind === 'por') return !!(c.status || c.sw || c.data);
|
||||
if (c.kind === 'sms') return !!(c.da || c.pid || c.dcs || c.udl !== undefined || c.ud);
|
||||
if (c.kind === 'files') return !!(c.files && c.files.length);
|
||||
return !!(c.value || '').trim();
|
||||
});
|
||||
@@ -7669,6 +7729,10 @@ function testStepFormError() {
|
||||
if (step.kind === 'scp80') {
|
||||
const src = p.source || (p.sp ? 'sp' : 'apdu');
|
||||
if (src === 'sp' ? !p.sp : !p.apdu) return t('SCP80 needs an APDU or a secured packet');
|
||||
const pc = (step.check || {}).por;
|
||||
if (pc && typeof pc === 'object' && !pc.status && !pc.sw && !pc.data) {
|
||||
return t('PoR check: status, SW or data is required');
|
||||
}
|
||||
}
|
||||
if (step.kind === 'menu-select') {
|
||||
const problem = testMenuSelectProblem(p);
|
||||
@@ -21116,6 +21180,17 @@ const LANG_RU = {
|
||||
'default: 9000 (91?? when polling)': 'по умолчанию: 9000 (91?? при опросе)',
|
||||
'Expected data (hex, optional)': 'Ожидаемые данные (hex, опционально)',
|
||||
'PoR check': 'Проверка PoR',
|
||||
'Custom (status / SW / data)': 'Своя проверка (status / SW / data)',
|
||||
'PoR status': 'Статус PoR',
|
||||
'PoR SW (hex)': 'SW PoR (hex)',
|
||||
'PoR data (hex)': 'Данные PoR (hex)',
|
||||
'PoR check: status, SW or data is required': 'Проверка PoR: нужен status, SW или data',
|
||||
'Command format': 'Формат команды',
|
||||
'Compact': 'Компактный',
|
||||
'Expanded (AA template)': 'Расширенный (шаблон AA)',
|
||||
'Expanded (AE 80 template)': 'Расширенный (шаблон AE 80)',
|
||||
'Alpha': 'Alpha',
|
||||
'SMS TPDU': 'TPDU SMS',
|
||||
'PoR ok': 'PoR: успешно',
|
||||
'no PoR': 'PoR: нет',
|
||||
'PoR error': 'Ошибка PoR',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v386';
|
||||
const CACHE = 'simple-v387';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -34,6 +34,7 @@ eval(extractBlock('const TEST_ACTION_KINDS = [', 'let _testScripts').replace(/^c
|
||||
eval(extractFunc(html, 'testTemplate'));
|
||||
eval(extractFunc(html, 'testMenuSelectProblem'));
|
||||
eval(extractFunc(html, 'testScriptProblem'));
|
||||
eval(extractFunc(html, 'testPorSpecSummary'));
|
||||
eval(extractFunc(html, 'testCheckSummary'));
|
||||
eval(extractFunc(html, 'testStepSummary'));
|
||||
eval(extractFunc(html, 'testCommandOptions'));
|
||||
@@ -194,6 +195,97 @@ test('the SCP80 source switch sticks and preserves the other value', () => {
|
||||
assert.strictEqual(_testEditStep.params.sp, 'AABBCC');
|
||||
});
|
||||
|
||||
test('the SCP80 step collects the format and the custom PoR object', () => {
|
||||
_testEditStep = { type: 'action', kind: 'scp80', params: { apdu: '80E2900000' } };
|
||||
_testEditChecks = [];
|
||||
_testEditStepIndex = 0;
|
||||
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
|
||||
'test-f-fmt': 'expanded', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
|
||||
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom',
|
||||
'test-f-porstatus': 'por_ok', 'test-f-por-sw': '6a8?', 'test-f-por-data': 'aa??',
|
||||
'test-f-fail': 'error' });
|
||||
testStepCollect();
|
||||
assert.strictEqual(_testEditStep.params.format, 'expanded');
|
||||
assert.deepStrictEqual(_testEditStep.check.por,
|
||||
{ status: 'por_ok', sw: { mode: 'mask', value: '6A8?' },
|
||||
data: { mode: 'mask', value: 'AA??' } });
|
||||
// a custom PoR check with no fields refuses to save
|
||||
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
|
||||
'test-f-fmt': 'compact', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
|
||||
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom',
|
||||
'test-f-fail': 'error' });
|
||||
assert.match(testStepFormError(), /PoR check/);
|
||||
assert.ok(!('format' in _testEditStep.params)); // compact is the default
|
||||
// the plain string forms stay and their object is dropped on switch
|
||||
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
|
||||
'test-f-fmt': '', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
|
||||
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'none',
|
||||
'test-f-fail': 'error' });
|
||||
testStepCollect();
|
||||
assert.strictEqual(_testEditStep.check.por, 'none');
|
||||
// the summary renders the effective format and the object's fields
|
||||
const s = testStepSummary({ type: 'action', kind: 'scp80',
|
||||
params: { apdu: '80E2', format: 'expanded-ae' },
|
||||
check: { por: { status: 'por_ok', sw: { mode: 'mask', value: '6A8?' } } } });
|
||||
assert.ok(s.includes('fmt=expanded-ae'), s);
|
||||
assert.ok(s.includes('PoR status por_ok SW ~6A8?'), s);
|
||||
});
|
||||
|
||||
test('the SCP80 step form renders the format select and the custom PoR fields', () => {
|
||||
const els = {
|
||||
'test-step-modal': { classList: { add: () => {}, remove: () => {} } },
|
||||
'test-step-title': {}, 'test-step-body': {},
|
||||
'test-step-error': { classList: { add: () => {}, remove: () => {} } },
|
||||
};
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
_testEditStep = { type: 'action', kind: 'scp80',
|
||||
params: { apdu: '80E2900000', format: 'expanded' },
|
||||
check: { por: { status: 'por_ok' } } };
|
||||
_testEditChecks = [];
|
||||
_testEditStepIndex = 0;
|
||||
testStepRender();
|
||||
const body = els['test-step-body'].innerHTML;
|
||||
assert.match(body, /id="test-f-fmt"/);
|
||||
assert.match(body, /<option value="expanded" selected>/);
|
||||
assert.match(body, /<option value="custom" selected>/);
|
||||
assert.match(body, /id="test-f-porstatus" value="por_ok"/);
|
||||
assert.match(body, /id="test-f-por-sw" value=""/);
|
||||
// the pre-built packet source has no format select (the wrapper applies
|
||||
// to a C-APDU only)
|
||||
_testEditStep = { type: 'action', kind: 'scp80', params: { source: 'sp', sp: 'AABB' } };
|
||||
testStepRender();
|
||||
assert.ok(!els['test-step-body'].innerHTML.includes('id="test-f-fmt"'));
|
||||
});
|
||||
|
||||
test('the alpha and sms checks collect and summarise', () => {
|
||||
assert.strictEqual(testCheckSummary({ kind: 'alpha', mode: 'exact', value: 'Alfa' }),
|
||||
'alpha ="Alfa"');
|
||||
assert.strictEqual(testCheckSummary({ kind: 'sms', da: '12345',
|
||||
pid: { mode: 'exact', value: '7F' }, dcs: { mode: 'exact', value: '00' },
|
||||
udl: 5, ud: { mode: 'mask', value: 'AA??' } }),
|
||||
'sms DA 12345 PID 7F DCS 00 UDL 5 UD ~AA??');
|
||||
// the sms fields collect with hex masks and a decimal UDL (0 included)
|
||||
_testEditChecks = [{ kind: 'sms' }];
|
||||
fakeForm({ 'test-check-kind-0': 'sms', 'test-check-smsda-0': '12345',
|
||||
'test-check-smspid-0': '7f', 'test-check-smsdcs-0': '00',
|
||||
'test-check-smsudl-0': '0', 'test-check-smsud-0': 'aa??',
|
||||
'test-check-fail-0': 'error' });
|
||||
testChecksCollect();
|
||||
assert.deepStrictEqual(_testEditChecks, [{ kind: 'sms', on_fail: 'error', da: '12345',
|
||||
pid: { mode: 'exact', value: '7F' }, dcs: { mode: 'exact', value: '00' }, udl: 0,
|
||||
ud: { mode: 'mask', value: 'AA??' } }]);
|
||||
// an empty sms check is dropped on save; an alpha with a value survives
|
||||
_testEditStep = { type: 'expect', command: 'SEND SHORT MESSAGE', checks: [],
|
||||
respond: { result: 'ok' } };
|
||||
_testEditChecks = [{ kind: 'sms' }, { kind: 'alpha', mode: 'contains', value: 'Alfa' }];
|
||||
fakeForm({ 'test-check-kind-0': 'sms', 'test-check-fail-0': 'error',
|
||||
'test-check-kind-1': 'alpha', 'test-check-mode-1': 'contains',
|
||||
'test-check-value-1': 'Alfa', 'test-check-fail-1': 'error' });
|
||||
testStepCollect();
|
||||
assert.deepStrictEqual(_testEditStep.checks,
|
||||
[{ kind: 'alpha', on_fail: 'error', mode: 'contains', value: 'Alfa' }]);
|
||||
});
|
||||
|
||||
test('the menu-select editor collects the item id or the text', () => {
|
||||
// by text (the id varies with the applet's install parameters)
|
||||
_testEditStep = { type: 'action', kind: 'menu-select', params: {} };
|
||||
|
||||
Reference in New Issue
Block a user